Glossary helps newbies understand the jargon which always crops up when specialists talk about a topic. Doctors discuss CBC and EKG and CCs. Web developers keep talking about CSS, P2P, XSLT, etc. This is all intimidating for newbies.
The Glossary module scans posts for glossary terms (and their synonyms) in the body. If found, the glossary indicator is inserted after the term, or the term is turned into an indicator link depending on the settings. By hovering over the indicator, users may learn the definition of that term. Clicking leads the user to that term presented within the whole glossary.
Glossary terms are managed as vocabularies within the taxonomy.module. To get started with glossary, create a new vocabulary on the !taxonomy_admin page. The vocabulary need not be associated with any modules, although you can add detailed description to terms by attaching (story or other type of) nodes to them. Add a few terms to the vocabulary. The term title should be the glossary entry and the description should be its definition. You can make use of the hierarchy, synonym, and related terms features. These features impact the display of the glossary when viewed in an overview.
Next, you have to setup the input formats you want to use the glossary with. At the !input_formats page select an input format to configure. Select the Glossary filter checkbox and press Save configuration. Now select the configure filters tab and select the vocabulary and apply other settings.
You can see how a vocabulary would function as a glossary by going to the !glossaries page and selecting the vocabulary to view.
Administration of glossary requires %permissions permissions.
',
array("%permissions" => join(", ",
array(t("administer taxonomy"), t("access administration pages"))),
"!taxonomy_admin" => l(t('administer') .' » '. t('content') .' » '. t('categories'), 'admin/content/taxonomy', array(), NULL, NULL, FALSE, TRUE),
"!input_formats" => l(t('administer') .' » '. t('site configuration') .' » '. t('input formats'), 'admin/settings/filters', array(), NULL, NULL, FALSE, TRUE),
"!glossaries" => l(t('glossaries'), 'glossary')));
break;
case 'admin/modules#description':
return t('Maintain a glossary on your site.');
break;
case 'filter#long-tip':
case 'filter#short-tip':
return t('Glossary terms will be automatically marked with links to their descriptions');
}
}
/**
* Implementation of hook_block().
*/
function glossary_block($op = 'list', $delta = 0, $edit = array()) {
if ($op == 'list') {
$blocks = array();
$blocks[0]['info'] = t('Search for glossary terms');
return $blocks;
}
else if ($op == 'view') {
if ($delta == 0) {
$block['subject'] = t('Search Glossary');
$block['content'] = drupal_get_form('glossary_search_form');
}
return $block;
}
}
/**
* Implementation of hook_menu().
*
* This is also the place where we add a link to the CSS sheet.
* We need the glossary filter to be a caching filter, because it is
* relatively slow. If content is cached, no calls at all are made to
* this module for that content. That means we can not detect when the
* CSS sheet is actually needed. Therefore, we simply link it in always.
* This should really happen always, so we do it when $may_cache == FALSE.
*/
function glossary_menu($may_cache) {
if ($may_cache) {
$items[] = array(
'path' => 'glossary',
'title' => t('Glossary'),
'callback' => 'glossary_page',
'access' => user_access('access content'),
'type' => MENU_SUGGESTED_ITEM,
);
$items[] = array(
'path' => 'glossary/search',
'title' => t('Glossary'),
'callback' => 'glossary_search_results',
'access' => user_access('access content'),
'type' => MENU_CALLBACK,
);
}
else {
drupal_add_css(drupal_get_path('module', 'glossary') .'/glossary.css');
if (arg(2) ) {
$items[] = array(
'path' => 'glossary/term/'. arg(2),
'title' => t('Glossary'),
'callback' => 'glossary_term',
'callback arguments' => array(arg(2)),
'access' => user_access('access content'),
'type' => MENU_CALLBACK,
);
}
}
return $items;
}
function glossary_search_form($keys = '') {
$form['#action'] = url('glossary/search');
$form['#attributes'] = array('class' => 'glossary search-form');
$form['keys'] = array(
'#type' => 'textfield',
'#title' => '',
'#default_value' => $keys,
'#size' => 20,
'#maxlength' => 255,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Search'),
);
return $form;
}
function glossary_search_form_submit($form_id, $form_values) {
$keys = trim($form_values['keys']);
return "glossary/search/$keys";
}
function glossary_search_results($keys = NULL) {
$vids = _glossary_get_filter_vids();
$output = '';
$sql = "SELECT tid FROM {term_data} WHERE vid IN (%s) AND (description LIKE '%%%s%%' OR name LIKE '%%%s%%')";
$result = db_query($sql, implode(',', $vids), $keys, $keys);
while ($row = db_fetch_object($result)) {
$term = taxonomy_get_term($row->tid);
$output .= theme('glossary_overview_item', $term);
}
return $output;
}
function theme_glossary_search_form($form) {
return ''. drupal_render($form) .'
';
}
function glossary_term($tid) {
$result = db_query('SELECT * FROM {term_data} WHERE tid = %d', $tid);
$output .= "\n";
while ($row = db_fetch_object($result)) {
$term = taxonomy_get_term($row->tid);
$output .= theme('glossary_overview_item', $term);
$vid = $row->vid;
}
$output .= "
\n";
$tree = taxonomy_get_tree($vid);
$alphabar = _glossary_alphabar($vid, $tree);
$output = $alphabar ."
". $output;
return $output;
}
function _glossary_filter_settings($format) {
$options[0] = t('');
foreach (taxonomy_get_vocabularies() as $vocabulary) {
$options[$vocabulary->vid] = $vocabulary->name;
}
$form = array();
$form['glossary_filter'] = array(
'#type' => 'fieldset',
'#title' => t('Glossary filter'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$form['glossary_filter']['general'] = array(
'#type' => 'fieldset',
'#title' => t('General settings'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$form['glossary_filter']['general']["glossary_vids_$format"] = array(
'#type' => 'select',
'#title' => t('Select Vocabulary'),
'#default_value' => variable_get("glossary_vids_$format", array()),
'#options' => $options,
'#description' => t('Select one or more vocabularies which hold all terms for your glossary. When enabled, posts will be scanned for glossary terms. An icon or a superscripted link is inserted beside each term. Hover over the icon or link to learn the meaning of that term.'),
'#multiple' => TRUE,
'#required' => TRUE,
);
$form['glossary_filter']['general']["glossary_absolute_$format"] = array(
'#type' => 'checkbox',
'#title' => t('Make Glossary links absolute'),
'#default_value' => variable_get("glossary_absolute_$format", FALSE),
'#description' => t('RSS feeds need absolute links to ensure they point back to this site. If you are not providing RSS feeds, it is better to leave this turned off.'),
);
// this next setting cannot vary by format since we glossary overview doesn't care or know about input formats
$form['glossary_filter']['general']["glossary_page_per_letter"] = array(
'#type' => 'checkbox',
'#title' => t('Show glossary across many smaller pages'),
'#default_value' => variable_get("glossary_page_per_letter", FALSE),
'#description' => t('Do you want to show all terms on one glossary page or break up the glossary into a page for each first letter (i.e. many pages).'),
);
$form['glossary_filter']['general']["glossary_match_$format"] = array(
'#type' => 'select',
'#title' => t('Match type'),
'#default_value' => variable_get("glossary_match_$format", 'match'),
'#options' => array(
'b' => t('word'),
'lr' => t('right or left substring'),
'l' => t('left substring'),
'r' => t('right substring'),
's' => t('any substring'),
),
'#description' => t('Choose the match type of glossary links.'),
);
$form['glossary_filter']['general']["glossary_case_$format"] = array(
'#type' => 'select',
'#title' => t('Case sensitivity'),
'#default_value' => variable_get("glossary_case_$format", '1'),
'#options' => array(
t('case insensitive'),
t('case sensitive')
),
'#description' => t('Match either case sensitive or not. Case sensitive matches are not very resource intensive.'),
);
$form['glossary_filter']['general']["glossary_replace_all_$format"] = array(
'#type' => 'select',
'#title' => t('Replace matches'),
'#default_value' => variable_get("glossary_replace_all_$format", 0),
'#options' => array(
t('only the first match'),
t('all matches')
),
'#description' => t('Whether only the first match should be replaced or all matches.'),
);
$form['glossary_filter']['indicator'] = array(
'#type' => 'fieldset',
'#title' => t('Indicator settings'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['glossary_filter']['indicator']["glossary_replace_$format"] = array(
'#type' => 'select',
'#title' => t('Term Indicator'),
'#default_value' => variable_get("glossary_replace_$format", 'superscript'),
'#options' => array(
'superscript' => t('superscript'),
'icon' => t('icon'),
'acronym link' => t('replace with acronym link'),
),
'#description' => t('Display terms using either a superscript formatted link, an icon, or an <acronym> tag.'),
);
$form['glossary_filter']['indicator']["glossary_superscript_$format"] = array(
'#type' => 'textfield',
'#title' => t('Superscript'),
'#default_value' => variable_get("glossary_superscript_$format", 'i'),
'#size' => 15,
'#maxlength' => 255,
'#description' => t('If you choose !superscript above, enter the superscript text.',
array('!superscript' => ''. t('superscript') .'')),
);
$mypath = '/'. drupal_get_path('module', 'glossary');
$form['glossary_filter']['indicator']["glossary_icon_$format"] = array(
'#type' => 'textfield',
'#title' => t('Glossary Icon URL'),
'#default_value' => variable_get("glossary_icon_$format", $mypath .'/glossary.gif'),
'#size' => 50,
'#maxlength' => 255,
'#description' => t('If you choose !icon above, enter the URL of the glossary icon relative to the root of your Drupal site.',
array('!icon' => ''. t('icon') .'')),
);
return $form;
}
function glossary_filter_tips($delta, $format, $long = FALSE) {
return $long ? glossary_help('filter#long-tip') : glossary_help('filter#short-tip');
}
function glossary_filter($op, $delta = 0, $format = -1, $text = "") {
switch ($op) {
case 'list':
return array(0 => t('Glossary filter'));
case 'description':
return glossary_help('admin/modules#description');
case 'process':
return _glossary_filter_process($format, $text);
case 'settings':
return _glossary_filter_settings($format);
default:
return $text;
}
}
function glossary_taxonomy($op, $type, $object = NULL) {
if ($object) {
_glossary_clear_cache($object->vid);
}
}
/**
* Implementation of hook_user().
*/
function glossary_user($type, $edit, $user) {
switch ($type) {
case 'form':
// Note: this requires a setting. Do we also need to clear cache if selected?
if (variable_get('glossary_disable_indicator', FALSE)) {
$form['content_glossary'] = array(
'#type' => 'fieldset',
'#title' => t('Glossary Indicators'),
);
$form['content_glossary']['glossary_disable_indicator'] = array(
'#type' => 'checkbox',
'#title' => t('Disable Glossary indicators'),
'#return_value' => 1,
'#default_value' => $user->glossary_disable_indicator,
'#description' => t('Check this box to disable the display of Glossary indicators.'),
);
return $form;
}
break;
}
}
function _glossary_filter_process($format, $text) {
global $user;
if ($user->glossary_disable_indicator) {
return $text;
}
if (variable_get("glossary_vids_$format", 0)) {
$text = ' '. $text .' ';
$replace_mode = variable_get("glossary_replace_$format", 'superscript');
$absolute_link = variable_get("glossary_absolute_$format", FALSE);
$terms = _glossary_get_terms($format);
$vids = _glossary_get_filter_vids();
foreach ($terms as $term) {
$term_title = $term->name .': '. strip_tags($term->description);
if ($term->nodes > 0) {
$linkto = "taxonomy/term/$term->tid";
}
elseif (count($vids) > 1) {
$linkto = 'glossary/'. $term->vid;
}
else {
$linkto = 'glossary/term/'. $term->tid;
$hash = NULL;
}
$ins_before = $ins_after = '';
switch ($replace_mode) {
case 'superscript':
$ins_after = l(variable_get("glossary_superscript_$format", 'i'),
$linkto, array('title' => $term_title, 'class' => 'glossary-indicator'),
NULL, NULL, $absolute_link);
break;
case 'acronym link':
$ins_before = '';
$ins_before .= "";
$ins_after = '';
break;
default:
$img = "";
$ins_after = l($img, $linkto, array('title' => $term_title, 'class' => 'glossary-icon'),
NULL, NULL, $absolute_link, TRUE);
break;
}
// replace term and synonyms with the desired new HTML code
foreach ($term->synonyms as $candidate) {
$text = _glossary_insertlink($format, $text, $candidate, $ins_before, $ins_after);
}
}
}
return $text;
}
/**
* Insert glossary links to $text after every $match that is not inside a link.
* $ins_before is prepended to the matches, $_insafter is appended to them.
* Match type and replace mode all depend on user settings.
*
* TODO: improve performance with not keeping *2.5 copies* of the string in memory:
* $text - original
* $newtext - transformed
* $before . $this_match - for checking stuff
*/
function _glossary_insertlink($format, &$text, $match, $ins_before, $ins_after) {
$findfunc = (variable_get("glossary_case_$format", "1") ? 'strpos' : 'stripos');
$next = $findfunc($text, $match);
if ($next === FALSE) { // no match at all
return $text;
}
else { // at least one match
$prevend = 0;
$newtext = '';
$matchlen = strlen($match);
$textlen = strlen($text);
$replaceall = variable_get("glossary_replace_all_$format", 0);
while ($next && ($next <= $textlen)) {
// get parts of the match for further investigation
$before = substr($text, 0, $next);
$this_match = substr($text, $next, $matchlen);
// see if we have a proper match or not
$open = substr_count($before, '<');
$close = substr_count($before, '>');
$opena = substr_count($before, '');
$openacro = substr_count($before, '');
$proper_match = FALSE;
if ($opena <= $closea && $open <= $close && $openacro <= $closeacro) { // Not in an open link
switch (variable_get("glossary_match_$format", 'b')) {
case 'lr': // require word break left or right
$proper_match = (_glossary_is_boundary($text {$next - 1}) ||
_glossary_is_boundary($text {$next + $matchlen}));
break;
case 'b': // require word break left and right
$proper_match = (_glossary_is_boundary($text {$next - 1}) &&
_glossary_is_boundary($text {$next + $matchlen}));
break;
case 'l': // require word break left
$proper_match = _glossary_is_boundary($text {$next - 1});
break;
case 'r': // require word break right
$proper_match = _glossary_is_boundary($text {$next + $matchlen});
break;
case 's': // match any substring
default:
$proper_match = TRUE;
break;
}
}
if ($proper_match) { // found match
$newtext .= substr($text, $prevend, ($next - $prevend)) . $ins_before . $this_match . $ins_after;
if ($replaceall == 0) {
return $newtext . substr($text, $next + $matchlen);
}
}
else { // not applicable match
$newtext .= substr($text, $prevend, ($next - $prevend)) . $this_match;
}
// Step further in finding the next match
$prevend = $next + $matchlen;
$next = $findfunc($text, $match, $prevend);
}
// Append remaining part
return $newtext . substr($text, $prevend);
}
}
function glossary_page($vid = NULL, $letter = NULL) {
$vids = _glossary_get_filter_vids();
$found = FALSE;
if (!$vid) {
if (count($vids) == 1) {
$vid = $vids[0];
$found = TRUE;
}
}
else {
$found = array_search($vid, _glossary_get_filter_vids());
}
if (!$vid || $found === FALSE) {
$breadcrumb = array(l(t('Home'), NULL));
drupal_set_title(t('Glossaries'));
drupal_set_breadcrumb($breadcrumb);
return _glossary_list();
}
else {
$voc = taxonomy_get_vocabulary($vid);
$breadcrumb = array(l(t('Home'), NULL));
if (count($vids) > 1) {
$breadcrumb[] = l(t('Glossaries'), 'glossary');
}
drupal_set_title(ucwords($voc->name));
drupal_set_breadcrumb($breadcrumb);
return glossary_overview($voc, $letter);
}
}
function _glossary_alphabar($vid, &$tree) {
$blocks = array(range('0', '9'), range('a', 'z'));
$vids = _glossary_get_filter_vids();
$found_letters = array();
foreach ($tree as $key => $term) {
if ($term->depth == 0) {
$firstletter = strtolower($term->name[0]);
if (! array_key_exists($firstletter, $found_letters)) {
$found_letters[$firstletter] = 1;
$tree[$key]->firstletter = $firstletter;
}
}
}
foreach ($blocks as $block) {
$found = FALSE;
foreach ($block as $entry) {
if (array_key_exists($entry, $found_letters)) {
$found = TRUE;
break;
}
}
if ($found) {
foreach ($block as $entry) {
if (! array_key_exists($entry, $found_letters)) {
$found_letters[$entry] = 0;
}
}
}
}
$output = "\n";
$letters = array_keys($found_letters);
sort($letters, SORT_STRING);
$href = "glossary/$vid";
foreach ($letters as $letter) {
$letter_link = 'letter'. $letter;
if (variable_get('glossary_page_per_letter', FALSE)) {
$href_append .= "/$letter_link";
}
else {
$fragment = $letter_link;
}
if ($found_letters[$letter]) {
$links[] = l($letter, $href . $href_append, NULL, NULL, $fragment, FALSE, TRUE);
}
else {
$links[] = $letter;
}
unset($href_append);
}
if (count($links)) {
$output .= join(' ', $links); // theme_links was used, but messes up display - this is a quick fix
}
$output .= "
\n";
return $output;
}
function glossary_overview($vocab, $letter = NULL) {
if ($vocab->description) { $output = ''. $vocab->description .'
'; }
else { $output = NULL; }
$vid = $vocab->vid;
$tree = taxonomy_get_tree($vid);
$output .= _glossary_alphabar($vid, $tree);
if ($tree) {
if (variable_get("glossary_page_per_letter", FALSE)) {
foreach ($tree as $term) {
if (!$letter || strtolower($term->name[0]) == substr($letter, 6)) {
$term_title = $term->name .': '. strip_tags($term->description);
$links[] = l($term->name, "glossary/term/$term->tid", array('title' => $term_title));
}
}
$output .= theme('item_list', $links);
}
else {
$output .= "\n";
foreach ($tree as $term) {
if (!$letter || strtolower($term->name[0]) == substr($letter, 6)) {
$output .= theme('glossary_overview_item', $term);
}
}
$output .= "
\n";
}
}
$output .= glossary_admin_links($vid);
return $output;
}
function theme_glossary_overview_item($term) {
if (isset($term->firstletter)) {
$output .= "firstletter ."\">\n";
}
$output .= "tid}\">\n";
if (module_exists('taxonomy_image')) {
$img = taxonomy_image_display($term->tid);
}
else {
$img = NULL;
}
$output .= ''. $img . $term->name;
if (user_access('administer taxonomy')) {
$output .= l(t('edit term'), "admin/content/taxonomy/edit/term/$term->tid",
array('title' => t('edit this term and definition.'), 'class' => 'glossary-edit-term'));
}
$output .= "depth\">";
$detailed_exists = db_result(db_query('SELECT COUNT(t.nid) FROM {term_node} t JOIN {node} n USING (nid) WHERE t.tid=%d AND n.status=1', $term->tid));
if ($detailed_exists) {
$output .= l(t('Detailed definition'), "taxonomy/term/$term->tid");
}
else {
$output .= $term->description;
}
if ($relations = taxonomy_get_related($term->tid, "name")) {
$output .= "". t("See also") .": ";
foreach ($relations as $related) {
$items[] .= l($related->name, 'glossary/'. $term->vid, NULL, NULL, "term". $related->tid);
}
$output .= implode(', ', $items) ."\n"; // strip trailing comma
unset($items);
}
$output .= "\n";
return $output;
}
function glossary_admin_links($vid) {
$output = "";
if (user_access('administer taxonomy')) {
$links['glossary_add_term'] = array(
'title' => t('add term'),
'href' => "admin/content/taxonomy/$vid/add/term",
);
$links['glossary_edit'] = array(
'title' => t('edit glossary'),
'href' => "admin/content/taxonomy/$vid",
);
return theme('links', $links);
}
}
function _glossary_list() {
$output = "";
$vids = _glossary_get_filter_vids();
$vocs = array();
foreach ($vids as $vid) {
$voc = taxonomy_get_vocabulary($vid);
$vocs[$voc->name] = $voc;
}
uksort($vocs, _glossary_cmp_strcase);
$header = array(t("Glossary"), t('Operations'));
$rows = array();
foreach ($vocs as $voc) {
$row = array();
$row[0] = $voc->name;
$row[1] = l(t('view'), "glossary/". $voc->vid);
if (user_access('administer taxonomy')) {
$row[1] .= " ". l(t('edit'), "admin/content/taxonomy");
}
$rows[] = $row;
}
$output = theme('table', $header, $rows);
return $output;
}
function _glossary_get_terms($format) {
static $terms = FALSE;
if ($terms === FALSE) {
$terms = $synonyms = array();
$vids = variable_get("glossary_vids_$format", 0);
foreach ($vids as $vid) {
$synonyms = _glossary_get_synonyms($vid);
// Get all glossary terms and attach synonyms.
// omit terms without a description. those are usually container terms.
$result = db_query("SELECT t.name, t.description, t.tid, COUNT(tn.nid) as nodes FROM {term_data} t LEFT JOIN {term_node} tn ON t.tid = tn.tid WHERE t.vid = %d GROUP BY t.tid, t.name, t.description ORDER BY LENGTH(t.name) DESC", $vid);
while ($term = db_fetch_object($result)) {
if ($term->description) {
$term->synonyms = $synonyms[$term->tid];
$term->synonyms[] = $term->name;
$term->vid = $vid;
$terms[] = $term;
}
}
}
}
return $terms;
}
// Get all synonyms for all glossary terms
function _glossary_get_synonyms($vid) {
$result = db_query("SELECT ts.tid, ts.name FROM {term_synonym} ts, {term_data} t WHERE ts.tid = t.tid AND t.vid = %d", $vid);
while ($synonym = db_fetch_object($result)) {
$synonyms[$synonym->tid][] = $synonym->name;
}
return $synonyms;
}
// This seems to be 1.2 times faster in fine-grained testing then
// the ereg solution used before. The chars used here are from the
// grep info page.
function _glossary_is_boundary($char) {
return (strpos("!\"#\$%&'()*+,-./:;<=>?@[\]^_`{|}~ \t\n\r", $char) !== FALSE);
}
// Natively only available in PHP 5+
// WARNING: Eats a tremendous amount of memory!
if (!function_exists("stripos")) {
function stripos($haystack, $needle, $offset = 0) {
return strpos(strtoupper($haystack), strtoupper($needle), $offset);
}
}
function _glossary_clear_cache($vid) {
// We could throw less things away if we checked which filter formats
// used the glossary filter, and we only threw those away. In practice,
// most if not all formats would use the glossary filter, so we just
// get rid of them all.
cache_clear_all(NULL, 'cache_filter', TRUE);
}
function _glossary_get_filter_vids() {
// We can't use filter_formats() here, because we need all input formats,
// not just those we are allowed to post in.
$vids = array();
$result = db_query('SELECT format FROM {filter_formats}');
while ($format = db_fetch_object($result)) {
$filters = filter_list_format($format->format);
foreach ($filters as $filter) {
if ($filter->module == "glossary") {
$vids = array_merge($vids, variable_get("glossary_vids_". $format->format, array()));
}
}
}
return array_unique($vids);
}
function _glossary_cmp_strcase($a, $b) {
return strcmp(strtolower($a), strtolower($b));
}