<?php if (!defined('PmWiki')) exit();

/**************************************************************************

    Copyright 2009-2014 Kory Roberts (kory@windypinesstudio.com).
    
    This file is glossaryplus.php; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published
    by the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.  

    This recipe creates a glossary processor with the following features:
        * A separate wiki page used for the glossary list.
        * Glossary terms in other pages with tooltip definition.
        * Fully customizable.

    This recipe requires the JS/DHTML Tooltips Library by Walter Zorn:
        http://www.walterzorn.com/tooltip/tooltip_e.htm
 
    For more information, please see the online documentation at
        http://www.pmwiki.org/wiki/Cookbook/GlossaryPlus

**************************************************************************/

##### Version #####
$RecipeInfo['GlossaryPlus']['Version'] = '2015-01-01';

##### Variables #####
SDVA($GlossaryPlus, array(
	'PageName' => 'Main.Glossary',
	'TermsAndDefinitions' => array(),
	'CaseSensitive' => false,
	'MaxToolTipsPerTerm' => 5,
	'LinkToTerm' => true,
	'TermClass' => 'glossterm',
	'DefClass' => 'glossdef',
	'DirectiveWarnings' => true,
	'ToolTipTermPattern' => 
		'<a href="{GP_URL}" class="{GP_TERM_CLASS}" onmouseover="Tip({GP_TT}{GP_COMMANDS})" onmouseout="UnTip()">{GP_TERM}</a>',
	'ToolTipDefPattern' => 
		'&lt;div class="{GP_DEF_CLASS}"&gt;{GP_DEF}&lt;/div&gt;',
));
SDVA($GlossaryPlus['ToolTipCommands'], array(
	'BGCOLOR'	=> '',
	'BORDERWIDTH' 	=> 0,
	'PADDING'	=> 0,
));
SDVA($GlossaryPlus['PreTermMatch'], array(
	# MATCH if preceeded by newline OR any non-word character.
	'\W'	=> '(?<=^|\\W)',
	# DO NOT match if preceeded by (%) or (%div )...should catch %TERM% and >>TERM<<
	'%'	=> '(?<!(%)|(%div ))',
	# DO NOT match if preceeded by (=), (='), or (=")...should catch class=TERM, id=TERM, etc.
	'='	=> '(?<!(=)|(=\')|(="))',
	# DO NOT match if preceeded by (:) or (: )...should catch style="...color:TERM...", (:TERM:), etc.
	':'	=> '(?<!(:)|(:\\s))',
	# DO NOT match if preceeded by (/) or (\)...should catch term in url
	'/'	=> '(?<!(\\/)|(\\\))',
	# DO NOT match if preceeded by (\w.)...should catch Group.TERM
	'\w.'	=> '(?<!\\w\\.)',
	# DO NOT match if preceeded by (#), (?), (&), or (&amp;)...should catch URL#TERM, URL?TERM, URL&TERM, and URL&amp;TERM
	'#?&'	=> '(?<!([#\\?&])|(&amp;)|(&AMP;))',
));
SDVA($GlossaryPlus['PostTermMatch'], array(
	# MATCH if followed by any non-word character OR end of line.
	'\W'	=> '(?=\\W|$)',
	# DO NOT match if followed by (=)...should catch TERM=
	'='	=> '(?!=)',
	# DO NOT match if followed by (:)...should catch style="...TERM:red...", etc.
	':'	=> '(?!:)',
	# DO NOT match if followed by (/) or (\)...should catch term used in url
	'/'	=> '(?!(\\/)|(\\\))',
	# DO NOT match if followed by (.\w)...should catch TERM.Group
	'.\w'	=> '(?!\\.\\w)',
));

##### Initialize #####
GlossaryInit();

##### Functions #####
function GlossaryInit() {
	global $GlossaryPlus, $pagename;
	# If current page is glossary page, process <dt> to include named anchors and return.
	$pagename = ResolvePageName($pagename);
	$g_pagename = ResolvePageName($GlossaryPlus['PageName']);
	if ($pagename == $g_pagename) {
		if (@$GlossaryPlus['LinkToTerm']) {
			Markup_e('>^::','>^::',
				'/(<dt>)(.*?)(<\\/dt>)/',
				"GlossaryListWithNamedAnchors(PSS(\$m[2]))"
			);
		}
		return;
	}
	# Prepare a few variables.
	$g_group = PageVar($g_pagename, '$Group');
	$g_page = PageVar($g_pagename, '$Name');
	$g_url = FmtPageName('$PageUrl', $g_pagename);
	$GlossaryPlus['CaseSensitive'] ? $g_cs = '' : $g_cs = 'i';
	$g_max_tt = &$GlossaryPlus['MaxToolTipsPerTerm'];
	foreach ($GlossaryPlus['ToolTipCommands'] as $c => $v) {
		@$tt_com .= ', ' . $c . ", &#039;" . $v . "&#039;";
	}
	$t_class = &$GlossaryPlus['TermClass'];
	$def_class = &$GlossaryPlus['DefClass'];
	$t_pat = &$GlossaryPlus['ToolTipTermPattern'];
	$def_pat = preg_replace(array("/'/", '/"/'), array("\'", '&quot;'), $GlossaryPlus['ToolTipDefPattern']);
	$t_pre = $GlossaryPlus['PreTermMatchImp'] = implode('', array_values($GlossaryPlus['PreTermMatch']));
	$t_post = $GlossaryPlus['PostTermMatchImp'] = implode('', array_values($GlossaryPlus['PostTermMatch']));
	# If glossary page is valid, add terms:defs to array.
	if (PageExists($g_pagename)) {
		$g_page_ret = RetrieveAuthPage($g_pagename, 'read', 0, READPAGE_CURRENT);
		# Deal with multi-line situations.
		$g_page_parse = preg_replace_callback("/\\\\(?>(\\\\*))\n/", function($matches) {
			return Keep(str_repeat('<br />',strlen($matches[1])));			
		}, $g_page_ret['text']);
		$g_page_parse = preg_replace_callback("/\\[\\[&lt;&lt;\\]\\]/", function($matches) {
			return Keep("<br clear='all' />");
		}, $g_page_parse);
		preg_match_all('/^(:+)(\s*)([^:]+):(.*)$/m', $g_page_parse, $matches);
		foreach ($matches[3] as $k => $v) {
			if (!@$GlossaryPlus['TermsAndDefinitions'][$v]) {
				$GlossaryPlus['CaseSensitive'] ? $v_cs = &$v : $v_cs = strtolower($v);
				$v_cs = htmlspecialchars($v_cs, ENT_NOQUOTES);
				$GlossaryPlus['TermsAndDefinitions'][$v_cs] = trim($matches[4][$k]);
			}
		}
	}
	# Process directives.
	Markup_e(	"glossdir", ">directives",
		"/\\(:gloss\\s+.*?:\\)/",
		"GlossaryDirective(PSS(\$m[0]), '', '$tt_com', '$g_url', '$t_class', '$def_class', '$t_pat', '$def_pat', '$g_max_tt', '$g_cs', '$g_pagename', '$g_group', '$g_page')"
	);
	# Process each raw term found, adding in code for tooltip.
	foreach (array_keys($GlossaryPlus['TermsAndDefinitions']) as $t) {
		$t_san = preg_quote($t, '/');
		Markup_e(	"glossterm" . @$n++, ">links",
			"/$t_pre$t_san$t_post/$g_cs",
			"GlossaryTermToolTip('raw', PSS(\$m[0]), '', '$tt_com', '$g_url', '$t_class', '$def_class', '$t_pat', '$def_pat', '$g_max_tt', '$g_cs', '$g_pagename', '$g_group', '$g_page')"
		);
	}
}

function GlossaryDirective ($inpt, $t_alt, $tt_com = '', $g_url, $t_class, $def_class, $t_pat, $def_pat, $g_max_tt, $g_cs, $g_pagename, $g_group, $g_page) {
	global $GlossaryPlus, $pagename;
	# Parse arguments from directive.
	$inpt_tr = preg_replace("/\\(:gloss\\s+(.*?):\\)/", "$1", $inpt);
	$args = ParseArgs($inpt_tr);
	# Get term.
	$args['term'] ? $t_arg = &$args['term'] : $t_arg = &$args[''][0];
	# Warn if no term.
	if ($t_arg == '' && @$GlossaryPlus['DirectiveWarnings']) {
		return Keep(
			'<a style="color:red;font-weight:bold;text-decoration:none;" href="'
			. $g_url .
			'">Glossary Plus Warning</a><span style="color:red">: No term supplied in directive</span>'
		);
	}
	# Look for definition and warn if not found.
	$g_cs == '' ? $t_cs = &$t_arg : $t_cs = strtolower($t_arg);
	if (!@$GlossaryPlus['TermsAndDefinitions'][$t_cs]) {
		if (@$GlossaryPlus['DirectiveWarnings']) {
			return Keep(
				'<a style="color:red;font-weight:bold;text-decoration:none;" href="'
				. $g_url .
				'">Glossary Plus Warning</a><span style="color:red">: Missing glossary entry " <code>'
				. $t_arg .
				'</code> "</span>'
			);
		}
		return Keep($t_arg);
	}
	# Set alternate text if present.
	if (@$args['text']) { $t_alt = &$args['text']; }
	# Set alternate url if present.
	if (@$args['link']) {
		$g_url = FmtPageName('$PageUrl', MakePageName($pagename, $args['link']));
	}
	# Set term class if present.
	if (@$args['termclass']) {
		$t_class = &$args['termclass'];
	}
	elseif (@$args['termclass'] === '') {
		$t_class = '';
	}
	# Set def class if present.
	if (@$args['defclass']) {
		$def_class = &$args['defclass'];
	}
	elseif (@$args['defclass'] === '') {
		$def_class = '';
	}
	# Set commands if present.
	if (@$args['commands']) {
		$tt_com = ', ' . $args['commands'];
	}
	# Call and return glossary term tooltip.
	return GlossaryTermToolTip('dir', $t_arg, $t_alt, $tt_com, $g_url, $t_class, $def_class, $t_pat, $def_pat, $g_max_tt, $g_cs, $g_pagename, $g_group, $g_page);
}


function GlossaryListWithNamedAnchors($t_arg) {
	# Return term with named anchor.
	$t_enc = GlossaryUrlEncode($t_arg);
	return "<dt><a name='" . $t_enc ."' id='". $t_enc ."'></a>" . $t_arg . "</dt>";
}

function GlossaryTermToolTip ($typ, $t_arg, $t_alt, $tt_com, $g_url, $t_class, $def_class, $t_pat, $def_pat, $g_max_tt, $g_cs, $g_pagename, $g_group, $g_page) {
	global $GlossaryPlus, $MarkupFrame, $pagename;
	# Limit how many times a term is processed for a tooltip.
	$g_cs == 'i' ? $t_cs = strtolower($t_arg) : $t_cs = &$t_arg;
	if (!isset($MarkupFrame[0]['glosscount'][$t_cs])) {
		$MarkupFrame[0]['glosscount'][$t_cs] = $g_max_tt;
	}
	if ($typ == 'raw' && $MarkupFrame[0]['glosscount'][$t_cs]-- < 1) {
		return Keep($t_arg);
	}
	# Prepare a few variables.
	$t_alt == '' ? true : $t_arg = &$t_alt;
	$d = $GlossaryPlus['TermsAndDefinitions'][$t_cs];
	$d_mrkup = GlossaryMarkupToHTML($d, $g_cs, $g_group, $g_page);
	$typ == 'raw' ? $g_url = GlossaryUrlEncode($t_arg, $g_url) : false;
	$t_pat = preg_replace ('/{GP_TT}/', "'" . $def_pat . "'", $t_pat);
	# Return tooltipped term.
	return Keep(preg_replace(
		array(
			'/{GP_TERM}/', '/{GP_DEF}/',
			'/{GP_COMMANDS}/', '/{GP_URL}/',
			'/{GP_TERM_CLASS}/', '/{GP_DEF_CLASS}/',
			'/{GP_PAGENAME}/', '/{GP_GROUP}/', '/{GP_PAGE}/'),
		array(
			$t_arg, $d_mrkup,
			$tt_com, $g_url,
			$t_class, $def_class,
			$g_pagename, $g_group, $g_page),
		$t_pat)
	);
}

function GlossaryMarkupToHTML ($d_arg, $g_cs, $g_group, $g_page) {
	global $GlossaryPlus, $pagename;
	# Protect against looping when terms repeat in definitions...first deal with page text variables.
	foreach ($GlossaryPlus['TermsAndDefinitions'] as $t => $d) {
		$t_san = preg_quote($t, '/');
		$d_arg = preg_replace("/(.*?)(\\{)(".$g_group."[.\\/]".$g_page.")*?(\\$:)($t_san)(\\})(.*?)/$g_cs", '$1' . $d . '$7', $d_arg);
	}
	# Then take care of the rest.
	$t_pre = &$GlossaryPlus['PreTermMatchImp'];
	$t_post = &$GlossaryPlus['PostTermMatchImp'];
	foreach ($GlossaryPlus['TermsAndDefinitions'] as $t => $d) {
		$t_san = preg_quote($t, '/');
		$d_arg = preg_replace("/$t_pre($t_san)$t_post/$g_cs e", "Keep('$0')", $d_arg);
	}	
	# Return marked up definition.
	$d_arg = MarkupToHTML($pagename, $d_arg);
	$d_arg = preg_replace("/\\\\/", '\\\\\\\\\\', $d_arg);
	$d_arg = preg_replace(array("/'/", '/"/', '/</', '/>/', "/\\n/"), array("\'", '&quot;', '&lt;', '&gt;', ''), $d_arg);
	$d_arg = preg_replace("/\\n/", '', $d_arg);
	return $d_arg;
}

function GlossaryUrlEncode ($t_arg, $g_url='') {
	global $GlossaryPlus;
	# Url processing.
	$t_arg = str_replace(' ', '', $t_arg);
	$t_arg = htmlspecialchars_decode($t_arg, ENT_NOQUOTES);
	$t_arg = urlencode($t_arg);
	# Return for named anchor.
	if ($g_url == '') {
		return $t_arg;
	}
	# Return url.
	if (@$GlossaryPlus['LinkToTerm']) {
		$g_url = $g_url . '#' . $t_arg;
	}
	return $g_url;
}