http://www.mediawiki.org/wiki/Special:Code/MediaWiki/89140
Revision: 89140
Author: foxtrott
Date: 2011-05-29 23:03:25 +0000 (Sun, 29 May 2011)
Log Message:
-----------
Improved markup algorithm (handles special chars and spaces, optimized)
Modified Paths:
--------------
trunk/extensions/SemanticGlossary/SemanticGlossary.php
trunk/extensions/SemanticGlossary/SemanticGlossaryElement.php
trunk/extensions/SemanticGlossary/SemanticGlossaryParser.php
Added Paths:
-----------
trunk/extensions/SemanticGlossary/SemanticGlossaryTree.php
Modified: trunk/extensions/SemanticGlossary/SemanticGlossary.php
===================================================================
--- trunk/extensions/SemanticGlossary/SemanticGlossary.php 2011-05-29
22:49:54 UTC (rev 89139)
+++ trunk/extensions/SemanticGlossary/SemanticGlossary.php 2011-05-29
23:03:25 UTC (rev 89140)
@@ -53,6 +53,7 @@
// register class files with the Autoloader
$wgAutoloadClasses[ 'SemanticGlossarySettings' ] = $dir .
'/SemanticGlossarySettings.php';
$wgAutoloadClasses[ 'SemanticGlossaryParser' ] = $dir .
'/SemanticGlossaryParser.php';
+$wgAutoloadClasses[ 'SemanticGlossaryTree' ] = $dir .
'/SemanticGlossaryTree.php';
$wgAutoloadClasses[ 'SemanticGlossaryElement' ] = $dir .
'/SemanticGlossaryElement.php';
$wgAutoloadClasses[ 'SemanticGlossaryMessageLog' ] = $dir .
'/SemanticGlossaryMessageLog.php';
$wgAutoloadClasses[ 'SpecialSemanticGlossaryBrowser' ] = $dir .
'/SpecialSemanticGlossaryBrowser.php';
Modified: trunk/extensions/SemanticGlossary/SemanticGlossaryElement.php
===================================================================
--- trunk/extensions/SemanticGlossary/SemanticGlossaryElement.php
2011-05-29 22:49:54 UTC (rev 89139)
+++ trunk/extensions/SemanticGlossary/SemanticGlossaryElement.php
2011-05-29 23:03:25 UTC (rev 89140)
@@ -19,38 +19,35 @@
* @ingroup SemanticGlossary
*/
class SemanticGlossaryElement {
+ const SG_TERM = 0;
const SG_DEFINITION = 1;
const SG_SOURCE = 2;
const SG_LINK = 3;
- private $mTerm;
private $mFullDefinition = null;
private $mDefinitions = array( );
static private $mLinkTemplate = null;
- public function __construct ( $term=null, $definition=null, $link=null,
$source=null ) {
- $this -> mTerm = $term;
- $this -> addDefinition( $definition, $link, $source );
+ public function __construct ( &$definition=null ) {
+ if ( $definition ) {
+ $this -> addDefinition( $definition );
+ }
}
- public function addDefinition ( $definition=null, $link=null,
$source=null ) {
+ public function addDefinition ( &$definition ) {
- $this -> mDefinitions[ ] = array(
- self::SG_DEFINITION => $definition,
- self::SG_SOURCE => $source,
- self::SG_LINK => $link,
- );
+ $this ->mDefinitions[] = $definition;
}
public function getFullDefinition ( DOMDocument &$doc ) {
// only create if not yet created
- if ( $this -> mFullDefinition == null ) {
+ if ( $this -> mFullDefinition == null || $this ->
mFullDefinition -> ownerDocument !== $doc ) {
$this -> mFullDefinition = $doc -> createElement(
'span' );
foreach ( $this -> mDefinitions as $definition ) {
- $element = $doc -> createElement( 'span',
html_entity_decode( $definition[ self::SG_DEFINITION ], ENT_COMPAT, 'UTF-8' ) .
' ' );
+ $element = $doc -> createElement( 'span',
htmlentities( $definition[ self::SG_DEFINITION ], ENT_COMPAT, 'UTF-8' ) . ' ' );
if ( $definition[ self::SG_LINK ] ) {
$linkedTitle = Title::newFromText(
$definition[ self::SG_LINK ] );
if ( $linkedTitle ) {
@@ -70,15 +67,19 @@
return key( $this -> mDefinitions );
}
- public function getSource ( $key ) {
+// public function getTerm ( $key ) {
+// return $this -> mDefinitions[ $key ][ self::SG_TERM ];
+// }
+//
+ public function getSource ( &$key ) {
return $this -> mDefinitions[ $key ][ self::SG_SOURCE ];
}
- public function getDefinition ( $key ) {
+ public function getDefinition ( &$key ) {
return $this -> mDefinitions[ $key ][ self::SG_DEFINITION ];
}
- public function getLink ( $key ) {
+ public function getLink ( &$key ) {
return $this -> mDefinitions[ $key ][ self::SG_LINK ];
}
@@ -87,7 +88,7 @@
}
private function getLinkTemplate ( DOMDocument &$doc ) {
-
+
// create template if it does not yet exist
if ( !self::$mLinkTemplate || ( self::$mLinkTemplate ->
ownerDocument !== $doc ) ) {
Modified: trunk/extensions/SemanticGlossary/SemanticGlossaryParser.php
===================================================================
--- trunk/extensions/SemanticGlossary/SemanticGlossaryParser.php
2011-05-29 22:49:54 UTC (rev 89139)
+++ trunk/extensions/SemanticGlossary/SemanticGlossaryParser.php
2011-05-29 23:03:25 UTC (rev 89140)
@@ -22,6 +22,10 @@
*/
class SemanticGlossaryParser {
+ private $mGlossaryArray = null;
+ private $mGlossaryTree = null;
+ private static $parserSingleton = null;
+
/**
*
* @param $parser
@@ -31,10 +35,14 @@
static function parse ( &$parser, &$text ) {
wfProfileIn( __METHOD__ );
+// echo( __METHOD__ );
- $sl = new SemanticGlossaryParser();
- $sl -> realParse( $parser, $text );
+ if ( !self::$parserSingleton ) {
+ self::$parserSingleton = new SemanticGlossaryParser();
+ }
+ self::$parserSingleton -> realParse( $parser, $text );
+
wfProfileOut( __METHOD__ );
return true;
@@ -42,15 +50,48 @@
/**
* Returns the list of terms applicable in the current context
- *
+ *
* @return Array an array mapping terms (keys) to descriptions (values)
*/
function getGlossaryArray ( SemanticGlossaryMessageLog &$messages =
null ) {
- global $smwgQDefaultNamespaces;
+ wfProfileIn( __METHOD__ );
+ // build glossary array only once per request
+ if ( !$this -> mGlossaryArray ) {
+ $this -> buildGlossary( $messages );
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return $this -> mGlossaryArray;
+ }
+
+ /**
+ * Returns the list of terms applicable in the current context
+ *
+ * @return Array an array mapping terms (keys) to descriptions (values)
+ */
+ function getGlossaryTree ( SemanticGlossaryMessageLog &$messages = null
) {
+
wfProfileIn( __METHOD__ );
+ // build glossary array only once per request
+ if ( !$this -> mGlossaryTree ) {
+ $this -> buildGlossary( $messages );
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return $this -> mGlossaryTree;
+ }
+
+ protected function buildGlossary ( SemanticGlossaryMessageLog
&$messages = null ) {
+
+ wfProfileIn( __METHOD__ );
+
+ $this -> mGlossaryTree = new SemanticGlossaryTree();
+
$store = smwfGetStore(); // default store
// Create query
$desc = new SMWSomeProperty( new SMWDIProperty( '___glt' ), new
SMWThingDescription() );
@@ -69,7 +110,7 @@
$queryresult = $store -> getQueryResult( $query );
// assemble the result array
- $result = array( );
+ $this -> mGlossaryArray = array( );
while ( ( $resultline = $queryresult -> getNext() ) ) {
$term = $resultline[ 0 ] -> getNextText(
SMW_OUTPUT_HTML );
@@ -98,18 +139,25 @@
continue;
}
- $source = array( $subject -> getDBkey(), $subject ->
getNamespace(), $subject -> getInterwiki(), $subject -> getDBkey() );
+ $source = array( $subject -> getDBkey(), $subject ->
getNamespace(), $subject -> getInterwiki() );
- if ( array_key_exists( $term, $result ) ) {
- $result[ $term ] -> addDefinition( $definition,
$link, $source );
+ $elementData = array(
+ SemanticGlossaryElement::SG_TERM => $term,
+ SemanticGlossaryElement::SG_DEFINITION =>
$definition,
+ SemanticGlossaryElement::SG_LINK => $link,
+ SemanticGlossaryElement::SG_SOURCE => $source
+ );
+
+ if ( array_key_exists( $term, $this -> mGlossaryArray )
) {
+ $this -> mGlossaryArray[ $term ] ->
addDefinition( $elementData );
} else {
- $result[ $term ] = new SemanticGlossaryElement(
$term, $definition, $link, $source );
+ $this -> mGlossaryArray[ $term ] = new
SemanticGlossaryElement( $elementData );
}
+
+ $this -> mGlossaryTree -> addTerm( $term, $elementData
);
}
wfProfileOut( __METHOD__ );
-
- return $result;
}
/**
@@ -126,83 +174,220 @@
global $wgRequest, $sggSettings;
wfProfileIn( __METHOD__ );
+// echo( __METHOD__ );
$action = $wgRequest -> getVal( 'action', 'view' );
- if ( $text == null || $text == '' || $action == "edit" ||
$action == "ajax" || isset( $_POST[ 'wpPreview' ] ) )
+
+ if ( $text == null ||
+ $text == '' ||
+ $action == "edit" ||
+ $action == "ajax" ||
+ isset( $_POST[ 'wpPreview' ] )
+ ) {
+
+ wfProfileOut( __METHOD__ );
return true;
+ }
+
// Get array of terms
- $terms = $this -> getGlossaryArray();
+ $glossary = $this -> getGlossaryTree();
+//
+// if ( empty( $terms ) ) {
+ if ( $glossary == null ) {
- if ( empty( $terms ) )
+ wfProfileOut( __METHOD__ );
return true;
+ }
//Get the minimum length abbreviation so we don't bother
checking against words shorter than that
- $min = min( array_map( 'strlen', array_keys( $terms ) ) );
-
+// $min = min( array_map( 'strlen', array_keys( $terms ) ) );
//Parse HTML from page
// FIXME: this works in PHP 5.3.3. What about 5.1?
+ wfProfileIn( __METHOD__ . " 1 loadHTML" );
wfSuppressWarnings();
- $doc = DOMDocument::loadHTML( '<html><meta
http-equiv="content-type" content="charset=utf-8"/>' . $text . '</html>' );
+
+ $doc = DOMDocument::loadHTML(
+ '<html><meta http-equiv="content-type"
content="charset=utf-8"/>' . $text . '</html>'
+ );
+
wfRestoreWarnings();
+ wfProfileOut( __METHOD__ . " 1 loadHTML" );
+ wfProfileIn( __METHOD__ . " 2 xpath" );
//Find all text in HTML.
$xpath = new DOMXpath( $doc );
- $elements = $xpath -> query(
"//*[not(ancestor-or-self::*[@class='noglossary'] or
ancestor-or-self::a)][text()!=' ']/text()" );
+ $elements = $xpath -> query(
+
"//*[not(ancestor-or-self::*[@class='noglossary'] or
ancestor-or-self::a)][text()!=' ']/text()"
+ );
+ wfProfileOut( __METHOD__ . " 2 xpath" );
//Iterate all HTML text matches
$nb = $elements -> length;
- $changed = false;
+ $changedDoc = false;
for ( $pos = 0; $pos < $nb; $pos++ ) {
$el = $elements -> item( $pos );
- if ( strlen( $el -> nodeValue ) < $min )
+ if ( strlen( $el -> nodeValue ) < $glossary ->
getMinTermLength() ) {
continue;
+ }
- //Split node text into words, putting offset and text
into $offsets[0] array
-// preg_match_all( "/\b[^\b\s\.,;:]+/", $el -> nodeValue,
$offsets, PREG_OFFSET_CAPTURE );
- preg_match_all( "/[^\s{$sggSettings ->
punctuationCharacters}]+/", $el -> nodeValue, $offsets, PREG_OFFSET_CAPTURE );
+ wfProfileIn( __METHOD__ . " 3 lexer" );
+ $matches = array( );
+ preg_match_all( '/[[:alpha:]]+|[^[:alpha:]]/', $el ->
nodeValue, $matches, PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER );
+ wfProfileOut( __METHOD__ . " 3 lexer" );
- //Search and replace words in reverse order (from end
of string backwards),
- //This way we don't mess up the offsets of the words as
we iterate
- $len = count( $offsets[ 0 ] );
+ if ( count( $matches ) == 0 || count( $matches[ 0 ] )
== 0 ) {
+ continue;
+ }
- for ( $i = $len - 1; $i >= 0; $i-- ) {
+ $lexemes = &$matches[ 0 ];
+ $countLexemes = count( $lexemes );
+ $parent = &$el -> parentNode;
+ $index = 0;
+ $changedElem = false;
- $offset = $offsets[ 0 ][ $i ];
+// echo("\nrealParse: nodeValue: {$el->nodeValue}\n");
+ while ( $index < $countLexemes ) {
- //Check if word is an abbreviation from the
terminologies
- if ( !is_numeric( $offset[ 0 ] ) && isset(
$terms[ $offset[ 0 ] ] ) ) { //Word matches, replace with appropriate span tag
- $changed = true;
+ wfProfileIn( __METHOD__ . " 4 findNextTerm" );
+ list( $skipped, $used, $definition ) =
$glossary -> findNextTerm( $lexemes, $index, $countLexemes );
+ wfProfileOut( __METHOD__ . " 4 findNextTerm" );
- $beforeMatchNode = $doc ->
createTextNode( substr( $el -> nodeValue, 0, $offset[ 1 ] ) );
- $afterMatchNode = $doc ->
createTextNode( substr( $el -> nodeValue, $offset[ 1 ] + strlen( $offset[ 0 ]
), strlen( $el -> nodeValue ) - 1 ) );
+// echo("realParse: skipped: $skipped used:
$used\n");
+// var_export($definition);
+ wfProfileIn( __METHOD__ . " 5 insert" );
+ if ( $used > 0 ) { // found a term
+ if ( $skipped > 0 ) { // skipped some
text, insert it as is
+
+ $parent -> insertBefore(
+ $doc -> createTextNode(
+ substr( $el ->
nodeValue,
+
$currLexIndex = $lexemes[ $index ][ 1 ],
+
$lexemes[ $index + $skipped ][ 1 ] - $currLexIndex )
+ ),
+ $el
+ );
+ }
+
+ $index += $skipped;
+
//Wrap abbreviation in <span> tags
$span = $doc -> createElement( 'span' );
$span -> setAttribute( 'class',
"tooltip" );
//Wrap abbreviation in <span> tags,
hidden
- $spanAbr = $doc -> createElement(
'span', $offset[ 0 ] );
- $spanAbr -> setAttribute( 'class',
"tooltip_abbr" );
+ $lastLex = $lexemes[ $index + $used - 1
];
+ $spanTerm = $doc -> createElement(
'span',
+ substr( $el ->
nodeValue,
+ $currLexIndex =
$lexemes[ $index ][ 1 ],
+ $lastLex[ 1 ] -
$currLexIndex + strlen( $lastLex[ 0 ] ) )
+ );
+ $spanTerm -> setAttribute( 'class',
"tooltip_abbr" );
//Wrap definition in <span> tags, hidden
- $spanTip = $terms[ $offset[ 0 ] ] ->
getFullDefinition( $doc );
- $spanTip -> setAttribute( 'class',
"tooltip_tip" );
+ $spanDefinition = $definition ->
getFullDefinition( $doc );
+ $spanDefinition -> setAttribute(
'class', "tooltip_tip" );
- $el -> parentNode -> insertBefore(
$beforeMatchNode, $el );
- $el -> parentNode -> insertBefore(
$span, $el );
- $span -> appendChild( $spanAbr );
- $span -> appendChild( $spanTip );
- $el -> parentNode -> insertBefore(
$afterMatchNode, $el );
- $el -> parentNode -> removeChild( $el );
- $el = $beforeMatchNode; //Set new
element to the text before the match for next iteration
+ // insert term and definition
+ $span -> appendChild( $spanTerm );
+ $span -> appendChild( $spanDefinition );
+ $parent -> insertBefore( $span, $el );
+
+ $changedElem = true;
+ } else { // did not find term, just use the
rest of the text
+ // If we found no term now and no term
before, there was no
+ // term in the whole element. Might as
well not change the
+ // element at all.
+ // Only change element if found term
before
+ if ( $changedElem ) {
+ $parent -> insertBefore(
+ $doc -> createTextNode(
+ substr( $el ->
nodeValue, $lexemes[ $index ][ 1 ] )
+ ),
+ $el
+ );
+ } else {
+
+ wfProfileOut( __METHOD__ . " 5
insert" );
+ // In principle superfluous,
the loop would run out
+ // anyway. Might save a bit of
time.
+ break;
+ }
+
+ $index += $skipped;
}
+ wfProfileOut( __METHOD__ . " 5 insert" );
+
+
+ $index += $used;
}
+
+ if ( $changedElem ) {
+ $parent -> removeChild( $el );
+ $changedDoc = true;
+ }
+
+ //Split node text into words, putting offset and text
into $offsets[0] array
+// preg_match_all(
+// "/[^\s{$sggSettings ->
punctuationCharacters}]+/",
+// $el -> nodeValue,
+// $offsets,
+// PREG_OFFSET_CAPTURE
+// );
+//
+// var_export($offsets);
+//
+// //Search and replace words in reverse order (from end
of string backwards),
+// //This way we don't mess up the offsets of the words as
we iterate
+// $len = count( $offsets[ 0 ] );
+//
+// for ( $i = $len - 1; $i >= 0; $i-- ) {
+//
+// $offset = $offsets[ 0 ][ $i ];
+//
+// //Check if word is an abbreviation from the
terminologies
+// if ( !is_numeric( $offset[ 0 ] ) && isset(
$terms[ $offset[ 0 ] ] ) ) {
+// //Word matches, replace with
appropriate span tag
+//
+// $changed = true;
+//
+// $beforeMatchNode = $doc ->
createTextNode(
+// substr( $el ->
nodeValue, 0, $offset[ 1 ] )
+// );
+//
+// $afterMatchNode = $doc ->
createTextNode(
+// substr( $el ->
nodeValue,
+// $offset[ 1 ] +
strlen( $offset[ 0 ] ),
+// strlen( $el ->
nodeValue ) - 1 )
+// );
+//
+// //Wrap abbreviation in <span> tags
+// $span = $doc -> createElement( 'span' );
+// $span -> setAttribute( 'class',
"tooltip" );
+//
+// //Wrap abbreviation in <span> tags,
hidden
+// $spanAbr = $doc -> createElement(
'span', $offset[ 0 ] );
+// $spanAbr -> setAttribute( 'class',
"tooltip_abbr" );
+//
+// //Wrap definition in <span> tags, hidden
+// $spanTip = $terms[ $offset[ 0 ] ] ->
getFullDefinition( $doc );
+// $spanTip -> setAttribute( 'class',
"tooltip_tip" );
+//
+// $el -> parentNode -> insertBefore(
$beforeMatchNode, $el );
+// $el -> parentNode -> insertBefore(
$span, $el );
+// $span -> appendChild( $spanAbr );
+// $span -> appendChild( $spanTip );
+// $el -> parentNode -> insertBefore(
$afterMatchNode, $el );
+// $el -> parentNode -> removeChild( $el );
+// $el = $beforeMatchNode; //Set new
element to the text before the match for next iteration
+// }
+// }
}
- if ( $changed ) {
+ if ( $changedDoc ) {
$body = $xpath -> query( '/html/body' );
// $text = $doc -> saveXML( $body -> item( 0 ) );
@@ -219,8 +404,10 @@
return true;
}
- protected function loadModules ( &$parser ) {
+ protected
+ function loadModules ( &$parser ) {
+
global $wgOut, $wgScriptPath;
if ( defined( 'MW_SUPPORTS_RESOURCE_MODULES' ) ) {
@@ -240,3 +427,4 @@
}
}
+
Added: trunk/extensions/SemanticGlossary/SemanticGlossaryTree.php
===================================================================
--- trunk/extensions/SemanticGlossary/SemanticGlossaryTree.php
(rev 0)
+++ trunk/extensions/SemanticGlossary/SemanticGlossaryTree.php 2011-05-29
23:03:25 UTC (rev 89140)
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * File holding the SemanticGlossaryTree class
+ *
+ * @author Stephan Gambke
+ * @file
+ * @ingroup SemanticGlossary
+ */
+if ( !defined( 'SG_VERSION' ) ) {
+ die( 'This file is part of the SemanticGlossary extension, it is not a
valid entry point.' );
+}
+
+/**
+ * The SemanticGlossaryTree class.
+ *
+ * Vocabulary:
+ * Term - The term as a normal string
+ * Definition - Its definition object
+ * Element - An element (leaf) in the glossary tree
+ * Path - The path in the tree to the leaf representing a term
+ *
+ * @ingroup SemanticGlossary
+ */
+class SemanticGlossaryTree {
+
+ private $mTree = array( );
+ private $mDefinition = null;
+ private $mMinLength = -1;
+
+ /**
+ * Adds a string to the Glossary Tree
+ * @param String $term
+ */
+ function addTerm ( &$term, $definition ) {
+
+ if ( !$term ) {
+ return;
+ }
+
+ $matches;
+ preg_match_all( '/[[:alpha:]]+|[^[:alpha:]]/', $term, $matches
);
+
+ $this -> addElement( $matches[ 0 ], $definition );
+
+ if ( $this -> mMinLength > -1 ) {
+ $this -> mMinLength = min( array( $this -> mMinLength,
strlen( $term ) ) );
+ } else {
+ $this -> mMinLength = strlen( $term );
+ }
+ }
+
+ /**
+ * Recursively adds an element to the Glossary Tree
+ *
+ * @param array $path
+ * @param <type> $index
+ */
+ protected function addElement ( Array &$path, &$definition ) {
+
+ // end of path, store description; end of recursion
+ if ( $path == null ) {
+
+ $this -> addDefinition( $definition );
+ } else {
+
+ $step = array_shift( $path );
+
+ if ( !array_key_exists( $step, $this -> mTree ) ) {
+
+ $this -> mTree[ $step ] = new
SemanticGlossaryTree();
+ }
+
+ $this -> mTree[ $step ] -> addElement( $path,
$definition );
+ }
+ }
+
+ /**
+ * Adds a defintion to the treenodes list of definitions
+ * @param <type> $definition
+ */
+ protected function addDefinition ( &$definition ) {
+
+ if ( $this -> mDefinition ) {
+ $this -> mDefinition -> addDefinition( $definition );
+ } else {
+ $this -> mDefinition = new SemanticGlossaryElement(
$definition );
+ }
+ }
+
+ function getMinTermLength () {
+ return $this -> mMinLength;
+ }
+
+ function findNextTerm ( &$lexemes, $index, $countLexemes ) {
+
+ wfProfileIn( __METHOD__ );
+
+ $start = $lastindex = $index;
+ $definition = null;
+
+ // skip until ther start of a term is found
+ while ( $index < $countLexemes && !$definition ) {
+
+ $currLex = &$lexemes[ $index ][ 0 ];
+
+ // Did we find the start of a term?
+ if ( array_key_exists( $currLex, $this -> mTree ) ) {
+
+ list( $lastindex, $definition) = $this ->
mTree[ $currLex ] -> findNextTermNoSkip( $lexemes, $index, $countLexemes );
+ }
+
+ // this will increase the index even if we found
something;
+ // will be corrected after the loop
+ $index++;
+ }
+
+ wfProfileOut( __METHOD__ );
+ if ( $definition ) {
+ return array( $index - $start - 1, $lastindex - $index
+ 2, $definition );
+ } else {
+ return array( $index - $start, 0, null );
+ }
+ }
+
+ function findNextTermNoSkip ( &$lexemes, $index, $countLexemes ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( $index + 1 < $countLexemes && array_key_exists( $currLex =
$lexemes[ $index + 1 ][ 0 ], $this -> mTree ) ) {
+
+ $ret = $this -> mTree[ $currLex ] ->
findNextTermNoSkip( $lexemes, $index + 1, $countLexemes );
+ } else {
+
+ $ret = array( $index, &$this -> mDefinition );
+ }
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+}
Property changes on: trunk/extensions/SemanticGlossary/SemanticGlossaryTree.php
___________________________________________________________________
Added: svn:eol-style
+ native
_______________________________________________
MediaWiki-CVS mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs