Index: trunk/extensions/Lingo/LingoMessageLog.php |
— | — | @@ -0,0 +1,77 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * File holding the LingoMessageLog class. |
| 6 | + * |
| 7 | + * @author Stephan Gambke |
| 8 | + * |
| 9 | + * @file |
| 10 | + * @ingroup Lingo |
| 11 | + */ |
| 12 | +if ( !defined( 'LINGO_VERSION' ) ) { |
| 13 | + die( 'This file is part of the Lingo extension, it is not a valid entry point.' ); |
| 14 | +} |
| 15 | + |
| 16 | +/** |
| 17 | + * This class holds messages (errors, warnings, notices) for Lingo |
| 18 | + * |
| 19 | + * Contains a static function to initiate the parsing. |
| 20 | + * |
| 21 | + * @ingroup Lingo |
| 22 | + */ |
| 23 | +class LingoMessageLog { |
| 24 | + |
| 25 | + private $mMessages = array(); |
| 26 | + private $parser = null; |
| 27 | + |
| 28 | + const MESSAGE_ERROR = 1; |
| 29 | + const MESSAGE_WARNING = 2; |
| 30 | + const MESSAGE_NOTICE = 3; |
| 31 | + |
| 32 | + function addMessage( $message, $severity = self::MESSAGE_NOTICE ) { |
| 33 | + $this->mMessages[] = array( $message, $severity ); |
| 34 | + } |
| 35 | + |
| 36 | + function addError( $message ) { |
| 37 | + $this->mMessages[] = array( $message, self::MESSAGE_ERROR ); |
| 38 | + } |
| 39 | + |
| 40 | + function addWarning( $message ) { |
| 41 | + $this->mMessages[] = array( $message, self::MESSAGE_WARNING ); |
| 42 | + } |
| 43 | + |
| 44 | + function addNotice( $message ) { |
| 45 | + $this->mMessages[] = array( $message, self::MESSAGE_NOTICE ); |
| 46 | + } |
| 47 | + |
| 48 | + function getMessagesFormatted( $severity = self::MESSAGE_WARNING, $header = null ) { |
| 49 | + global $wgTitle, $wgUser; |
| 50 | + |
| 51 | + $ret = ''; |
| 52 | + |
| 53 | + if ( $header == null ) { |
| 54 | + $header = wfMsg( 'semanticglossary-messageheader' ); |
| 55 | + } |
| 56 | + |
| 57 | + foreach ( $this->mMessages as $message ) { |
| 58 | + if ( $message[1] <= $severity ) { |
| 59 | + $ret .= '* ' . $message[0] . "\n"; |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + if ( $ret != '' ) { |
| 64 | + if ( !$this->parser ) { |
| 65 | + $parser = new Parser(); |
| 66 | + } |
| 67 | + |
| 68 | + $ret = Html::rawElement( 'div', array( 'class' => 'messages' ), |
| 69 | + Html::rawElement( 'div', array( 'class' => 'heading' ), $header ) . |
| 70 | + $parser->parse( $ret, $wgTitle, ParserOptions::newFromUser( $wgUser ) )->getText() |
| 71 | + ); |
| 72 | + } |
| 73 | + |
| 74 | + return $ret; |
| 75 | + } |
| 76 | + |
| 77 | +} |
| 78 | + |
Property changes on: trunk/extensions/Lingo/LingoMessageLog.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 79 | + native |
Index: trunk/extensions/Lingo/LingoBackend.php |
— | — | @@ -0,0 +1,36 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * File holding the LingoBackend class |
| 6 | + * |
| 7 | + * @author Stephan Gambke |
| 8 | + * @file |
| 9 | + * @ingroup Lingo |
| 10 | + */ |
| 11 | +if ( !defined( 'LINGO_VERSION' ) ) { |
| 12 | + die( 'This file is part of the Lingo extension, it is not a valid entry point.' ); |
| 13 | +} |
| 14 | + |
| 15 | +/** |
| 16 | + * The LingoBackend class. |
| 17 | + * |
| 18 | + * @ingroup Lingo |
| 19 | + */ |
| 20 | +abstract class LingoBackend { |
| 21 | + |
| 22 | + protected $mMessageLog; |
| 23 | + |
| 24 | + public function __construct( LingoMessageLog &$messages = null ) { |
| 25 | + |
| 26 | + $this->mMessageLog = $messages; |
| 27 | + |
| 28 | + } |
| 29 | + |
| 30 | + /** |
| 31 | + * |
| 32 | + * @return Boolean true, if a next element is available |
| 33 | + */ |
| 34 | + abstract public function next(); |
| 35 | + |
| 36 | +} |
| 37 | + |
Property changes on: trunk/extensions/Lingo/LingoBackend.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 38 | + native |
Index: trunk/extensions/Lingo/skins/Lingo.css |
— | — | @@ -0,0 +1,64 @@ |
| 2 | +/* |
| 3 | +* Stylesheet for the markup of glossary terms in wiki pages. |
| 4 | +*/ |
| 5 | + |
| 6 | +.tooltip { |
| 7 | + display: inline; |
| 8 | + position: relative; |
| 9 | + cursor: help; |
| 10 | +} |
| 11 | + |
| 12 | +.tooltip_abbr { |
| 13 | + border-bottom: 1px dotted #bbf; |
| 14 | +} |
| 15 | + |
| 16 | +.tooltip_tipwrapper { |
| 17 | + display: none; |
| 18 | + |
| 19 | + position: absolute; |
| 20 | + top: 0; |
| 21 | + left: 0; |
| 22 | + |
| 23 | + height: 3em; |
| 24 | + width: 23em; |
| 25 | + |
| 26 | + font-weight: normal; |
| 27 | + font-size: medium; |
| 28 | + line-height: 1.2em; |
| 29 | + |
| 30 | + z-index: 2; |
| 31 | +} |
| 32 | + |
| 33 | +.tooltip_tip { |
| 34 | + display: block; |
| 35 | + |
| 36 | + position: absolute; |
| 37 | + top: 1.5em; |
| 38 | + left: 2em; |
| 39 | + |
| 40 | + width: 20em; |
| 41 | + |
| 42 | + padding: 0.5em; |
| 43 | + margin: 0; |
| 44 | + |
| 45 | + background-color: #F9F9F9; |
| 46 | + border: 1px solid #aaa; |
| 47 | + |
| 48 | + -moz-border-radius: 5px; |
| 49 | + border-radius: 5px; |
| 50 | + |
| 51 | + -webkit-box-shadow: 3px 3px 3px #888; |
| 52 | + box-shadow: 3px 3px 3px #888; |
| 53 | +} |
| 54 | + |
| 55 | +.tooltip_tip span { |
| 56 | + display: block; |
| 57 | +} |
| 58 | + |
| 59 | +.tooltip:hover .tooltip_abbr { |
| 60 | + background-color:rgba(0,0,0,0.1); |
| 61 | +} |
| 62 | + |
| 63 | +.tooltip:hover .tooltip_tipwrapper { |
| 64 | + display: block; |
| 65 | +} |
Property changes on: trunk/extensions/Lingo/skins/Lingo.css |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 66 | + native |
Index: trunk/extensions/Lingo/skins/linkicon.png |
Cannot display: file marked as a binary type. |
svn:mime-type = image/png |
Property changes on: trunk/extensions/Lingo/skins/linkicon.png |
___________________________________________________________________ |
Added: svn:mime-type |
2 | 67 | + image/png |
Index: trunk/extensions/Lingo/LingoBasicBackend.php |
— | — | @@ -0,0 +1,74 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * File holding the LingoBackend class |
| 6 | + * |
| 7 | + * @author Stephan Gambke |
| 8 | + * @file |
| 9 | + * @ingroup Lingo |
| 10 | + */ |
| 11 | +if ( !defined( 'LINGO_VERSION' ) ) { |
| 12 | + die( 'This file is part of the Lingo extension, it is not a valid entry point.' ); |
| 13 | +} |
| 14 | + |
| 15 | +/** |
| 16 | + * The LingoBasicBackend class. |
| 17 | + * |
| 18 | + * @ingroup Lingo |
| 19 | + */ |
| 20 | +class LingoBasicBackend extends LingoBackend { |
| 21 | + |
| 22 | + protected $mArticleLines = array(); |
| 23 | + |
| 24 | + public function __construct( LingoMessageLog &$messages ) { |
| 25 | + |
| 26 | + parent::__construct( $messages ); |
| 27 | + |
| 28 | + // Get Terminology page |
| 29 | + $rev = Revision::newFromTitle( Title::makeTitle( null, 'Terminology' ) ); |
| 30 | + |
| 31 | + if ( !$rev ) { |
| 32 | + $messages->addWarning( '[[Terminology]] does not exist.' ); |
| 33 | + return false; |
| 34 | + } |
| 35 | + |
| 36 | + $content = $rev->getText(); |
| 37 | + |
| 38 | + $term = array(); |
| 39 | + $this->mArticleLines = explode( "\n", $content ); |
| 40 | + } |
| 41 | + |
| 42 | + /** |
| 43 | + * |
| 44 | + * @return Boolean true, if a next element is available |
| 45 | + */ |
| 46 | + public function next() { |
| 47 | + |
| 48 | + $ret = null; |
| 49 | + |
| 50 | + // find next valid line (yes, the assignation is intended) |
| 51 | + while ( ( $ret == null ) && ( $entry = each( $this->mArticleLines ) ) ) { |
| 52 | + |
| 53 | + if ( empty( $entry[1] ) || $entry[1][0] !== ';' ) { |
| 54 | + continue; |
| 55 | + } |
| 56 | + |
| 57 | + $terms = explode( ':', $entry[1], 2 ); |
| 58 | + |
| 59 | + if ( count( $terms ) < 2 ) { |
| 60 | + continue; // Invalid syntax |
| 61 | + } |
| 62 | + |
| 63 | + $ret = array( |
| 64 | + LingoElement::ELEMENT_TERM => trim( substr( $terms[0], 1 ) ), |
| 65 | + LingoElement::ELEMENT_DEFINITION => trim( $terms[1] ), |
| 66 | + LingoElement::ELEMENT_LINK => null, |
| 67 | + LingoElement::ELEMENT_SOURCE => null |
| 68 | + ); |
| 69 | + } |
| 70 | + |
| 71 | + return $ret; |
| 72 | + } |
| 73 | + |
| 74 | +} |
| 75 | + |
Property changes on: trunk/extensions/Lingo/LingoBasicBackend.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 76 | + native |
Index: trunk/extensions/Lingo/LingoElement.php |
— | — | @@ -0,0 +1,128 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * File holding the LingoElement class. |
| 6 | + * |
| 7 | + * @author Stephan Gambke |
| 8 | + * |
| 9 | + * @file |
| 10 | + * @ingroup Lingo |
| 11 | + */ |
| 12 | +if ( !defined( 'LINGO_VERSION' ) ) { |
| 13 | + die( 'This file is part of the Lingo extension, it is not a valid entry point.' ); |
| 14 | +} |
| 15 | + |
| 16 | +/** |
| 17 | + * This class represents a term-definition pair. |
| 18 | + * One term may be related to several definitions. |
| 19 | + * |
| 20 | + * @ingroup Lingo |
| 21 | + */ |
| 22 | +class LingoElement { |
| 23 | + const ELEMENT_TERM = 0; |
| 24 | + const ELEMENT_DEFINITION = 1; |
| 25 | + const ELEMENT_SOURCE = 2; |
| 26 | + const ELEMENT_LINK = 3; |
| 27 | + |
| 28 | + private $mFullDefinition = null; |
| 29 | + private $mDefinitions = array(); |
| 30 | + private $mTerm = null; |
| 31 | + static private $mLinkTemplate = null; |
| 32 | + |
| 33 | + public function __construct( &$term, &$definition = null ) { |
| 34 | + |
| 35 | + $this->mTerm = $term; |
| 36 | + |
| 37 | + if ( $definition ) { |
| 38 | + $this->addDefinition( $definition ); |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + public function addDefinition( &$definition ) { |
| 43 | + $this->mDefinitions[] = $definition; |
| 44 | + } |
| 45 | + |
| 46 | + public function getFullDefinition( DOMDocument &$doc ) { |
| 47 | + // only create if not yet created |
| 48 | + if ( $this->mFullDefinition == null || $this->mFullDefinition->ownerDocument !== $doc ) { |
| 49 | + |
| 50 | + // Wrap term and definition in <span> tags |
| 51 | + $span = $doc->createElement( 'span' ); |
| 52 | + $span->setAttribute( 'class', 'tooltip' ); |
| 53 | + |
| 54 | + // Wrap term in <span> tag, hidden |
| 55 | + $spanTerm = $doc->createElement( 'span', $this->mTerm ); |
| 56 | + $spanTerm->setAttribute( 'class', 'tooltip_abbr' ); |
| 57 | + |
| 58 | + // Wrap definition in two <span> tags |
| 59 | + $spanDefinitionOuter = $doc->createElement( 'span' ); |
| 60 | + $spanDefinitionOuter->setAttribute( 'class', 'tooltip_tipwrapper' ); |
| 61 | + |
| 62 | + $spanDefinitionInner = $doc->createElement( 'span' ); |
| 63 | + $spanDefinitionInner->setAttribute( 'class', 'tooltip_tip' ); |
| 64 | + |
| 65 | + foreach ( $this->mDefinitions as $definition ) { |
| 66 | + $element = $doc->createElement( 'span', htmlentities( $definition[self::ELEMENT_DEFINITION], ENT_COMPAT, 'UTF-8' ) . ' ' ); |
| 67 | + if ( $definition[self::ELEMENT_LINK] ) { |
| 68 | + $linkedTitle = Title::newFromText( $definition[self::ELEMENT_LINK] ); |
| 69 | + if ( $linkedTitle ) { |
| 70 | + $link = $this->getLinkTemplate( $doc ); |
| 71 | + $link->setAttribute( 'href', $linkedTitle->getFullURL() ); |
| 72 | + $element->appendChild( $link ); |
| 73 | + } |
| 74 | + } |
| 75 | + $spanDefinitionInner->appendChild( $element ); |
| 76 | + } |
| 77 | + |
| 78 | + // insert term and definition |
| 79 | + $span->appendChild( $spanTerm ); |
| 80 | + $span->appendChild( $spanDefinitionOuter ); |
| 81 | + $spanDefinitionOuter->appendChild( $spanDefinitionInner ); |
| 82 | + |
| 83 | + $this->mFullDefinition = $span; |
| 84 | + |
| 85 | + } |
| 86 | + |
| 87 | + return $this->mFullDefinition->cloneNode( true ); |
| 88 | + } |
| 89 | + |
| 90 | + public function getCurrentKey() { |
| 91 | + return key( $this->mDefinitions ); |
| 92 | + } |
| 93 | + |
| 94 | + public function getTerm( $key ) { |
| 95 | + return $this->mDefinitions[$key][self::ELEMENT_TERM]; |
| 96 | + } |
| 97 | + |
| 98 | + public function getSource( &$key ) { |
| 99 | + return $this->mDefinitions[$key][self::ELEMENT_SOURCE]; |
| 100 | + } |
| 101 | + |
| 102 | + public function getDefinition( &$key ) { |
| 103 | + return $this->mDefinitions[$key][self::ELEMENT_DEFINITION]; |
| 104 | + } |
| 105 | + |
| 106 | + public function getLink( &$key ) { |
| 107 | + return $this->mDefinitions[$key][self::ELEMENT_LINK]; |
| 108 | + } |
| 109 | + |
| 110 | + public function next() { |
| 111 | + next( $this->mDefinitions ); |
| 112 | + } |
| 113 | + |
| 114 | + private function getLinkTemplate( DOMDocument &$doc ) { |
| 115 | + // create template if it does not yet exist |
| 116 | + if ( !self::$mLinkTemplate || ( self::$mLinkTemplate->ownerDocument !== $doc ) ) { |
| 117 | + global $wgScriptPath; |
| 118 | + |
| 119 | + $linkimage = $doc->createElement( 'img' ); |
| 120 | + $linkimage->setAttribute( 'src', $wgScriptPath . '/extensions/SemanticLingo/skins/linkicon.png' ); |
| 121 | + |
| 122 | + self::$mLinkTemplate = $doc->createElement( 'a' ); |
| 123 | + self::$mLinkTemplate->appendChild( $linkimage ); |
| 124 | + } |
| 125 | + |
| 126 | + return self::$mLinkTemplate->cloneNode( true ); |
| 127 | + } |
| 128 | + |
| 129 | +} |
Property changes on: trunk/extensions/Lingo/LingoElement.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 130 | + native |
Index: trunk/extensions/Lingo/LingoParser.php |
— | — | @@ -0,0 +1,286 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * File holding the LingoParser class. |
| 6 | + * |
| 7 | + * @author Stephan Gambke |
| 8 | + * |
| 9 | + * @file |
| 10 | + * @ingroup Lingo |
| 11 | + */ |
| 12 | +if ( !defined( 'LINGO_VERSION' ) ) { |
| 13 | + die( 'This file is part of the Lingo extension, it is not a valid entry point.' ); |
| 14 | +} |
| 15 | + |
| 16 | +/** |
| 17 | + * This class parses the given text and enriches it with definitions for defined |
| 18 | + * terms. |
| 19 | + * |
| 20 | + * Contains a static function to initiate the parsing. |
| 21 | + * |
| 22 | + * @ingroup Lingo |
| 23 | + */ |
| 24 | +class LingoParser { |
| 25 | + |
| 26 | + private $mLingoArray = null; |
| 27 | + private $mLingoTree = null; |
| 28 | + private $mLingoBackend = null; |
| 29 | + private static $parserSingleton = null; |
| 30 | + |
| 31 | + public function __construct() { |
| 32 | + global $wgexLingoBackend; |
| 33 | + $this->mLingoBackend = new $wgexLingoBackend( new LingoMessageLog() ); |
| 34 | + } |
| 35 | + |
| 36 | + /** |
| 37 | + * |
| 38 | + * @param $parser |
| 39 | + * @param $text |
| 40 | + * @return Boolean |
| 41 | + */ |
| 42 | + static function parse( &$parser, &$text ) { |
| 43 | + wfProfileIn( __METHOD__ ); |
| 44 | + if ( !self::$parserSingleton ) { |
| 45 | + self::$parserSingleton = new LingoParser(); |
| 46 | + } |
| 47 | + |
| 48 | + self::$parserSingleton->realParse( $parser, $text ); |
| 49 | + |
| 50 | + wfProfileOut( __METHOD__ ); |
| 51 | + |
| 52 | + return true; |
| 53 | + } |
| 54 | + |
| 55 | + /** |
| 56 | + * Returns the list of terms applicable in the current context |
| 57 | + * |
| 58 | + * @return Array an array mapping terms (keys) to descriptions (values) |
| 59 | + */ |
| 60 | + function getLingoArray( LingoMessageLog &$messages = null ) { |
| 61 | + wfProfileIn( __METHOD__ ); |
| 62 | + |
| 63 | + // build glossary array only once per request |
| 64 | + if ( !$this->mLingoArray ) { |
| 65 | + $this->buildLingo( $messages ); |
| 66 | + } |
| 67 | + |
| 68 | + wfProfileOut( __METHOD__ ); |
| 69 | + |
| 70 | + return $this->mLingoArray; |
| 71 | + } |
| 72 | + |
| 73 | + /** |
| 74 | + * Returns the list of terms applicable in the current context |
| 75 | + * |
| 76 | + * @return LingoTree a LingoTree mapping terms (keys) to descriptions (values) |
| 77 | + */ |
| 78 | + function getLingoTree( LingoMessageLog &$messages = null ) { |
| 79 | + wfProfileIn( __METHOD__ ); |
| 80 | + |
| 81 | + // build glossary array only once per request |
| 82 | + if ( !$this->mLingoTree ) { |
| 83 | + $this->buildLingo( $messages ); |
| 84 | + } |
| 85 | + |
| 86 | + wfProfileOut( __METHOD__ ); |
| 87 | + |
| 88 | + return $this->mLingoTree; |
| 89 | + } |
| 90 | + |
| 91 | + protected function buildLingo( LingoMessageLog &$messages = null ) { |
| 92 | + wfProfileIn( __METHOD__ ); |
| 93 | + |
| 94 | + $this->mLingoTree = new LingoTree(); |
| 95 | + |
| 96 | + $backend = &$this->mLingoBackend; |
| 97 | + |
| 98 | + // assemble the result array |
| 99 | + $this->mLingoArray = array(); |
| 100 | + while ( $elementData = $backend->next() ) { |
| 101 | + |
| 102 | + if ( array_key_exists( ( $term = $elementData[LingoElement::ELEMENT_TERM] ), $this->mLingoArray ) ) { |
| 103 | + $this->mLingoArray[$term]->addDefinition( $elementData ); |
| 104 | + } else { |
| 105 | + $this->mLingoArray[$term] = new LingoElement( $term, $elementData ); |
| 106 | + } |
| 107 | + |
| 108 | + $this->mLingoTree->addTerm( $term, $elementData ); |
| 109 | + } |
| 110 | + |
| 111 | + wfProfileOut( __METHOD__ ); |
| 112 | + } |
| 113 | + |
| 114 | + /** |
| 115 | + * Parses the given text and enriches applicable terms |
| 116 | + * |
| 117 | + * This method currently only recognizes terms consisting of max one word |
| 118 | + * |
| 119 | + * @param $parser |
| 120 | + * @param $text |
| 121 | + * @return Boolean |
| 122 | + */ |
| 123 | + protected function realParse( &$parser, &$text ) { |
| 124 | + global $wgRequest; |
| 125 | + |
| 126 | + wfProfileIn( __METHOD__ ); |
| 127 | + |
| 128 | + $action = $wgRequest->getVal( 'action', 'view' ); |
| 129 | + |
| 130 | + if ( $text == null || |
| 131 | + $text == '' || |
| 132 | + $action == 'edit' || |
| 133 | + $action == 'ajax' || |
| 134 | + isset( $_POST['wpPreview'] ) |
| 135 | + ) { |
| 136 | + |
| 137 | + wfProfileOut( __METHOD__ ); |
| 138 | + return true; |
| 139 | + } |
| 140 | + |
| 141 | + // Get array of terms |
| 142 | + $glossary = $this->getLingoTree(); |
| 143 | + |
| 144 | + if ( $glossary == null ) { |
| 145 | + wfProfileOut( __METHOD__ ); |
| 146 | + return true; |
| 147 | + } |
| 148 | + |
| 149 | + // Parse HTML from page |
| 150 | + // @todo FIXME: this works in PHP 5.3.3. What about 5.1? |
| 151 | + wfProfileIn( __METHOD__ . ' 1 loadHTML' ); |
| 152 | + wfSuppressWarnings(); |
| 153 | + |
| 154 | + $doc = DOMDocument::loadHTML( |
| 155 | + '<html><meta http-equiv="content-type" content="charset=utf-8"/>' . $text . '</html>' |
| 156 | + ); |
| 157 | + |
| 158 | + wfRestoreWarnings(); |
| 159 | + wfProfileOut( __METHOD__ . ' 1 loadHTML' ); |
| 160 | + |
| 161 | + wfProfileIn( __METHOD__ . ' 2 xpath' ); |
| 162 | + // Find all text in HTML. |
| 163 | + $xpath = new DOMXpath( $doc ); |
| 164 | + $elements = $xpath->query( |
| 165 | + "//*[not(ancestor-or-self::*[@class='noglossary'] or ancestor-or-self::a)][text()!=' ']/text()" |
| 166 | + ); |
| 167 | + wfProfileOut( __METHOD__ . ' 2 xpath' ); |
| 168 | + |
| 169 | + // Iterate all HTML text matches |
| 170 | + $nb = $elements->length; |
| 171 | + $changedDoc = false; |
| 172 | + |
| 173 | + for ( $pos = 0; $pos < $nb; $pos++ ) { |
| 174 | + $el = $elements->item( $pos ); |
| 175 | + |
| 176 | + if ( strlen( $el->nodeValue ) < $glossary->getMinTermLength() ) { |
| 177 | + continue; |
| 178 | + } |
| 179 | + |
| 180 | + wfProfileIn( __METHOD__ . ' 3 lexer' ); |
| 181 | + $matches = array(); |
| 182 | + preg_match_all( |
| 183 | + '/[[:alpha:]]+|[^[:alpha:]]/u', |
| 184 | + $el->nodeValue, |
| 185 | + $matches, |
| 186 | + PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER |
| 187 | + ); |
| 188 | + wfProfileOut( __METHOD__ . ' 3 lexer' ); |
| 189 | + |
| 190 | + if ( count( $matches ) == 0 || count( $matches[0] ) == 0 ) { |
| 191 | + continue; |
| 192 | + } |
| 193 | + |
| 194 | + $lexemes = &$matches[0]; |
| 195 | + $countLexemes = count( $lexemes ); |
| 196 | + $parent = &$el->parentNode; |
| 197 | + $index = 0; |
| 198 | + $changedElem = false; |
| 199 | + |
| 200 | + while ( $index < $countLexemes ) { |
| 201 | + wfProfileIn( __METHOD__ . ' 4 findNextTerm' ); |
| 202 | + list( $skipped, $used, $definition ) = |
| 203 | + $glossary->findNextTerm( $lexemes, $index, $countLexemes ); |
| 204 | + wfProfileOut( __METHOD__ . ' 4 findNextTerm' ); |
| 205 | + |
| 206 | + wfProfileIn( __METHOD__ . ' 5 insert' ); |
| 207 | + if ( $used > 0 ) { // found a term |
| 208 | + if ( $skipped > 0 ) { // skipped some text, insert it as is |
| 209 | + $parent->insertBefore( |
| 210 | + $doc->createTextNode( |
| 211 | + substr( $el->nodeValue, |
| 212 | + $currLexIndex = $lexemes[$index][1], |
| 213 | + $lexemes[$index + $skipped][1] - $currLexIndex ) |
| 214 | + ), |
| 215 | + $el |
| 216 | + ); |
| 217 | + } |
| 218 | + |
| 219 | + $parent->insertBefore( $definition->getFullDefinition( $doc ), $el ); |
| 220 | + |
| 221 | + $changedElem = true; |
| 222 | + } else { // did not find term, just use the rest of the text |
| 223 | + // If we found no term now and no term before, there was no |
| 224 | + // term in the whole element. Might as well not change the |
| 225 | + // element at all. |
| 226 | + // Only change element if found term before |
| 227 | + if ( $changedElem ) { |
| 228 | + $parent->insertBefore( |
| 229 | + $doc->createTextNode( |
| 230 | + substr( $el->nodeValue, $lexemes[$index][1] ) |
| 231 | + ), |
| 232 | + $el |
| 233 | + ); |
| 234 | + } else { |
| 235 | + wfProfileOut( __METHOD__ . ' 5 insert' ); |
| 236 | + // In principle superfluous, the loop would run out |
| 237 | + // anyway. Might save a bit of time. |
| 238 | + break; |
| 239 | + } |
| 240 | + } |
| 241 | + wfProfileOut( __METHOD__ . ' 5 insert' ); |
| 242 | + |
| 243 | + $index += $used + $skipped; |
| 244 | + } |
| 245 | + |
| 246 | + if ( $changedElem ) { |
| 247 | + $parent->removeChild( $el ); |
| 248 | + $changedDoc = true; |
| 249 | + } |
| 250 | + } |
| 251 | + |
| 252 | + if ( $changedDoc ) { |
| 253 | + $body = $xpath->query( '/html/body' ); |
| 254 | + |
| 255 | + $text = ''; |
| 256 | + foreach ( $body->item( 0 )->childNodes as $child ) { |
| 257 | + $text .= $doc->saveXML( $child ); |
| 258 | + } |
| 259 | + |
| 260 | + $this->loadModules( $parser ); |
| 261 | + } |
| 262 | + |
| 263 | + wfProfileOut( __METHOD__ ); |
| 264 | + |
| 265 | + return true; |
| 266 | + } |
| 267 | + |
| 268 | + protected function loadModules( &$parser ) { |
| 269 | + global $wgOut, $wgScriptPath; |
| 270 | + |
| 271 | + if ( defined( 'MW_SUPPORTS_RESOURCE_MODULES' ) ) { |
| 272 | + if ( !is_null( $parser ) ) { |
| 273 | + $parser->getOutput()->addModules( 'ext.Lingo' ); |
| 274 | + } else { |
| 275 | + $wgOut->addModules( 'ext.Lingo' ); |
| 276 | + } |
| 277 | + } else { |
| 278 | + if ( !is_null( $parser ) && ( $wgOut->isArticle() ) ) { |
| 279 | + $parser->getOutput()->addHeadItem( '<link rel="stylesheet" href="' . $wgScriptPath . '/extensions/Lingo/skins/Lingo.css" />', 'ext.Lingo.css' ); |
| 280 | + } else { |
| 281 | + $wgOut->addHeadItem( 'ext.Lingo.css', '<link rel="stylesheet" href="' . $wgScriptPath . '/extensions/Lingo/skins/Lingo.css" />' ); |
| 282 | + } |
| 283 | + } |
| 284 | + } |
| 285 | + |
| 286 | +} |
| 287 | + |
Property changes on: trunk/extensions/Lingo/LingoParser.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 288 | + native |
Index: trunk/extensions/Lingo/Lingo.php |
— | — | @@ -7,7 +7,8 @@ |
8 | 8 | * @defgroup Lingo |
9 | 9 | * @author Barry Coughlan |
10 | 10 | * @copyright 2010 Barry Coughlan |
11 | | - * @version 0.1 |
| 11 | + * @author Stephan Gambke |
| 12 | + * @version 0.2 alpha |
12 | 13 | * @licence GNU General Public Licence 2.0 or later |
13 | 14 | * @see http://www.mediawiki.org/wiki/Extension:Lingo Documentation |
14 | 15 | */ |
— | — | @@ -15,145 +16,54 @@ |
16 | 17 | die( 'This file is part of a MediaWiki extension, it is not a valid entry point.' ); |
17 | 18 | } |
18 | 19 | |
19 | | -// Extension credits that will show up on Special:Version |
20 | | -$wgExtensionCredits['parserhook'][] = array( |
| 20 | +define( 'LINGO_VERSION', '0.2 alpha' ); |
| 21 | + |
| 22 | +$wgExtensionCredits[ 'parserhook' ][ ] = array( |
21 | 23 | 'path' => __FILE__, |
22 | 24 | 'name' => 'Lingo', |
23 | | - 'version' => '0.1', |
24 | | - 'author' => 'Barry Coughlan', |
| 25 | + 'author' => array( 'Barry Coughlan', '[http://www.mediawiki.org/wiki/User:F.trott Stephan Gambke]' ), |
25 | 26 | 'url' => 'http://www.mediawiki.org/wiki/Extension:Lingo', |
26 | 27 | 'description' => 'Provides hover-over tool tips on articles from words defined on the [[Terminology]] page', |
| 28 | + 'version' => LINGO_VERSION, |
27 | 29 | ); |
28 | 30 | |
29 | | -$wgHooks['OutputPageBeforeHTML'][] = 'lingoHook'; |
| 31 | +// server-local path to this file |
| 32 | +$wgexLingoDir = dirname( __FILE__ ); |
| 33 | +$wgexLingoBackend = 'LingoBasicBackend'; |
30 | 34 | |
31 | | -function lingoHook( &$out, &$text ) { |
32 | | - global $wgOut, $wgScriptPath; |
33 | | - $out->includeJQuery(); |
34 | | - $out->addHeadItem( 'tooltip.css', '<link rel="stylesheet" type="text/css" href="' . $wgScriptPath . '/extensions/Lingo/tooltip.css"/>' ); |
35 | | - $out->addHeadItem( 'tooltip.js', '<script type="text/javascript" src="' . $wgScriptPath . '/extensions/Lingo/tooltip.min.js"></script>' ); |
36 | | - return $out; |
37 | | -} |
| 35 | +// register message file |
| 36 | +// $wgExtensionMessagesFiles['Lingo'] = $dir . '/Lingo.i18n.php'; |
| 37 | +// $wgExtensionMessagesFiles['LingoAlias'] = $dir . '/Lingo.alias.php'; |
38 | 38 | |
39 | | -function getLingoArray( &$content ) { |
40 | | - $term = array(); |
41 | | - $c = explode( "\n", $content ); |
| 39 | +// register class files with the Autoloader |
| 40 | +// $wgAutoloadClasses['LingoSettings'] = $dir . '/LingoSettings.php'; |
| 41 | +$wgAutoloadClasses['LingoParser'] = $wgexLingoDir . '/LingoParser.php'; |
| 42 | +$wgAutoloadClasses['LingoTree'] = $wgexLingoDir . '/LingoTree.php'; |
| 43 | +$wgAutoloadClasses['LingoElement'] = $wgexLingoDir . '/LingoElement.php'; |
| 44 | +$wgAutoloadClasses['LingoBackend'] = $wgexLingoDir . '/LingoBackend.php'; |
| 45 | +$wgAutoloadClasses['LingoBasicBackend'] = $wgexLingoDir . '/LingoBasicBackend.php'; |
| 46 | +$wgAutoloadClasses['LingoMessageLog'] = $wgexLingoDir . '/LingoMessageLog.php'; |
| 47 | +// $wgAutoloadClasses['SpecialLingoBrowser'] = $dir . '/SpecialLingoBrowser.php'; |
42 | 48 | |
43 | | - foreach ( $c as $entry ) { |
44 | | - if ( empty( $entry ) || $entry[ 0 ] !== ';' ) { |
45 | | - continue; |
46 | | - } |
| 49 | +$wgHooks['ParserAfterTidy'][] = 'LingoParser::parse'; |
47 | 50 | |
48 | | - $terms = explode( ':', $entry, 2 ); |
49 | | - if ( count( $terms ) < 2 ) { |
50 | | - continue; // Invalid syntax |
51 | | - } |
52 | | - // Add to array |
53 | | - $term[trim( substr( $terms[0], 1 ) )] = trim( $terms[1] ); |
54 | | - } |
55 | | - return $term; |
56 | | -} |
| 51 | +// register resource modules with the Resource Loader |
| 52 | +$wgResourceModules['ext.Lingo'] = array( |
| 53 | + // JavaScript and CSS styles. To combine multiple file, just list them as an array. |
| 54 | + // 'scripts' => 'js/ext.myExtension.js', |
| 55 | + 'styles' => 'css/Lingo.css', |
57 | 56 | |
58 | | -$wgHooks['ParserAfterTidy'][] = 'lingoParser'; |
| 57 | + // When your module is loaded, these messages will be available to mediaWiki.msg() |
| 58 | + // 'messages' => array( 'myextension-hello-world', 'myextension-goodbye-world' ), |
59 | 59 | |
60 | | -function lingoParser( &$parser, &$text ) { |
61 | | - global $wgRequest; |
| 60 | + // If your scripts need code from other modules, list their identifiers as dependencies |
| 61 | + // and ResourceLoader will make sure they're loaded before you. |
| 62 | + // You don't need to manually list 'mediawiki' or 'jquery', which are always loaded. |
| 63 | + // 'dependencies' => array( 'jquery.ui.datepicker' ), |
62 | 64 | |
63 | | - $action = $wgRequest->getVal( 'action', 'view' ); |
64 | | - if ( $action == 'edit' || $action == 'ajax' || isset( $_POST['wpPreview'] ) ) { |
65 | | - return false; |
66 | | - } |
| 65 | + // ResourceLoader needs to know where your files are; specify your |
| 66 | + // subdir relative to "extensions" or $wgExtensionAssetsPath |
| 67 | + 'localBasePath' => dirname( __FILE__ ), |
| 68 | + 'remoteExtPath' => 'Lingo' |
| 69 | +); |
67 | 70 | |
68 | | - // Get Terminology page |
69 | | - $rev = Revision::newFromTitle( Title::makeTitle( null, 'Terminology' ) ); |
70 | | - if ( !$rev ) { |
71 | | - return false; |
72 | | - } |
73 | | - $content = &$rev->getText(); |
74 | | - if ( empty( $content ) ) { |
75 | | - return false; |
76 | | - } |
77 | | - |
78 | | - // Get array of terms |
79 | | - $terms = getLingoArray( $content ); |
80 | | - // Get the minimum length abbreviation so we don't bother checking against words shorter than that |
81 | | - $min = min( array_map( 'strlen', array_keys( $terms ) ) ); |
82 | | - |
83 | | - // Parse HTML from page |
84 | | - // @todo FIXME: this works in PHP 5.3.3. What about 5.1? |
85 | | - wfSuppressWarnings(); |
86 | | - $doc = DOMDocument::loadHTML( '<html><meta http-equiv="content-type" content="charset=utf-8"/>' . $text . '</html>' ); |
87 | | - wfRestoreWarnings(); |
88 | | - |
89 | | - // Find all text in HTML. |
90 | | - $xpath = new DOMXpath( $doc ); |
91 | | - $elements = $xpath->query( "//*[text()!=' ']/text()" ); |
92 | | - |
93 | | - // Iterate all HTML text matches |
94 | | - $nb = $elements->length; |
95 | | - $changed = false; |
96 | | - for ( $pos = 0; $pos < $nb; $pos++ ) { |
97 | | - $el = &$elements->item( $pos ); |
98 | | - if ( strlen( $el->nodeValue ) < $min ) { |
99 | | - continue; |
100 | | - } |
101 | | - |
102 | | - // Split node text into words, putting offset and text into $offsets[0] array |
103 | | - preg_match_all( |
104 | | - '/[^\s\.,;:]+/', |
105 | | - $el->nodeValue, |
106 | | - $offsets, |
107 | | - PREG_OFFSET_CAPTURE |
108 | | - ); |
109 | | - |
110 | | - // Search and replace words in reverse order (from end of string backwards), |
111 | | - // This way we don't mess up the offsets of the words as we iterate |
112 | | - $len = count( $offsets[0] ); |
113 | | - for ( $i = $len - 1; $i >= 0; $i-- ) { |
114 | | - $offset = $offsets[0][$i]; |
115 | | - // Check if word is an abbreviation from the terminologies |
116 | | - // Word matches, replace with appropriate span tag |
117 | | - if ( !is_numeric( $offset[0] ) && isset( $terms[$offset[0]] ) ) { |
118 | | - $changed = true; |
119 | | - |
120 | | - $tip = htmlentities( $terms[$offset[0]], ENT_COMPAT, 'UTF-8' ); |
121 | | - |
122 | | - $beforeMatchNode = $doc->createTextNode( substr( $el->nodeValue, 0, $offset[1] ) ); |
123 | | - $afterMatchNode = $doc->createTextNode( |
124 | | - substr( |
125 | | - $el->nodeValue, |
126 | | - $offset[1] + strlen( $offset[0] ), |
127 | | - strlen( $el->nodeValue ) - 1 |
128 | | - ) |
129 | | - ); |
130 | | - |
131 | | - // Wrap abbreviation in <span> tags |
132 | | - $span = $doc->createElement( 'span', $offset[0] ); |
133 | | - $span->setAttribute( 'class', 'tooltip_abbr' ); |
134 | | - |
135 | | - // Wrap definition in <span> tags, hidden |
136 | | - $spanTip = $doc->createElement( 'span', $tip ); |
137 | | - $spanTip->setAttribute( 'class', 'tooltip_hide' ); |
138 | | - |
139 | | - $el->parentNode->insertBefore( $beforeMatchNode, $el ); |
140 | | - $el->parentNode->insertBefore( $span, $el ); |
141 | | - $span->appendChild( $spanTip ); |
142 | | - $el->parentNode->insertBefore( $afterMatchNode, $el ); |
143 | | - $el->parentNode->removeChild( $el ); |
144 | | - // Set new element to the text before the match for next iteration |
145 | | - $el = $beforeMatchNode; |
146 | | - } |
147 | | - } |
148 | | - } |
149 | | - |
150 | | - if ( $changed ) { |
151 | | - $body = $xpath->query( '/html/body' ); |
152 | | - |
153 | | - $text = ''; |
154 | | - foreach ( $body->item( 0 )->childNodes as $child ) { |
155 | | - $text .= $doc->saveXML( $child ); |
156 | | - } |
157 | | - } |
158 | | - |
159 | | - return true; |
160 | | -} |
Index: trunk/extensions/Lingo/LingoTree.php |
— | — | @@ -0,0 +1,130 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * File holding the LingoTree class |
| 6 | + * |
| 7 | + * @author Stephan Gambke |
| 8 | + * |
| 9 | + * @file |
| 10 | + * @ingroup Lingo |
| 11 | + */ |
| 12 | +if ( !defined( 'LINGO_VERSION' ) ) { |
| 13 | + die( 'This file is part of the Lingo extension, it is not a valid entry point.' ); |
| 14 | +} |
| 15 | + |
| 16 | +/** |
| 17 | + * The LingoTree class. |
| 18 | + * |
| 19 | + * Vocabulary: |
| 20 | + * Term - The term as a normal string |
| 21 | + * Definition - Its definition object |
| 22 | + * Element - An element (leaf) in the glossary tree |
| 23 | + * Path - The path in the tree to the leaf representing a term |
| 24 | + * |
| 25 | + * @ingroup Lingo |
| 26 | + */ |
| 27 | +class LingoTree { |
| 28 | + |
| 29 | + private $mTree = array(); |
| 30 | + private $mDefinition = null; |
| 31 | + private $mMinLength = -1; |
| 32 | + |
| 33 | + /** |
| 34 | + * Adds a string to the Lingo Tree |
| 35 | + * @param String $term |
| 36 | + */ |
| 37 | + function addTerm( &$term, $definition ) { |
| 38 | + if ( !$term ) { |
| 39 | + return; |
| 40 | + } |
| 41 | + |
| 42 | + $matches; |
| 43 | + preg_match_all( '/[[:alpha:]]+|[^[:alpha:]]/u', $term, $matches ); |
| 44 | + |
| 45 | + $this->addElement( $matches[0], $term, $definition ); |
| 46 | + |
| 47 | + if ( $this->mMinLength > -1 ) { |
| 48 | + $this->mMinLength = min( array( $this->mMinLength, strlen( $term ) ) ); |
| 49 | + } else { |
| 50 | + $this->mMinLength = strlen( $term ); |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + /** |
| 55 | + * Recursively adds an element to the Lingo Tree |
| 56 | + * |
| 57 | + * @param array $path |
| 58 | + * @param <type> $index |
| 59 | + */ |
| 60 | + protected function addElement( Array &$path, &$term, &$definition ) { |
| 61 | + // end of path, store description; end of recursion |
| 62 | + if ( $path == null ) { |
| 63 | + $this -> addDefinition( $term, $definition ); |
| 64 | + } else { |
| 65 | + $step = array_shift( $path ); |
| 66 | + |
| 67 | + if ( !array_key_exists( $step, $this->mTree ) ) { |
| 68 | + $this->mTree[$step] = new LingoTree(); |
| 69 | + } |
| 70 | + |
| 71 | + $this->mTree[$step]->addElement( $path, $term, $definition ); |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Adds a defintion to the treenodes list of definitions |
| 77 | + * @param <type> $definition |
| 78 | + */ |
| 79 | + protected function addDefinition( &$term, &$definition ) { |
| 80 | + if ( $this->mDefinition ) { |
| 81 | + $this->mDefinition->addDefinition( $definition ); |
| 82 | + } else { |
| 83 | + $this->mDefinition = new LingoElement( $term, $definition ); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + function getMinTermLength() { |
| 88 | + return $this->mMinLength; |
| 89 | + } |
| 90 | + |
| 91 | + function findNextTerm( &$lexemes, $index, $countLexemes ) { |
| 92 | + wfProfileIn( __METHOD__ ); |
| 93 | + |
| 94 | + $start = $lastindex = $index; |
| 95 | + $definition = null; |
| 96 | + |
| 97 | + // skip until ther start of a term is found |
| 98 | + while ( $index < $countLexemes && !$definition ) { |
| 99 | + $currLex = &$lexemes[$index][0]; |
| 100 | + |
| 101 | + // Did we find the start of a term? |
| 102 | + if ( array_key_exists( $currLex, $this->mTree ) ) { |
| 103 | + list( $lastindex, $definition ) = $this->mTree[$currLex]->findNextTermNoSkip( $lexemes, $index, $countLexemes ); |
| 104 | + } |
| 105 | + |
| 106 | + // this will increase the index even if we found something; |
| 107 | + // will be corrected after the loop |
| 108 | + $index++; |
| 109 | + } |
| 110 | + |
| 111 | + wfProfileOut( __METHOD__ ); |
| 112 | + if ( $definition ) { |
| 113 | + return array( $index - $start - 1, $lastindex - $index + 2, $definition ); |
| 114 | + } else { |
| 115 | + return array( $index - $start, 0, null ); |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + function findNextTermNoSkip( &$lexemes, $index, $countLexemes ) { |
| 120 | + wfProfileIn( __METHOD__ ); |
| 121 | + |
| 122 | + if ( $index + 1 < $countLexemes && array_key_exists( $currLex = $lexemes[$index + 1][0], $this->mTree ) ) { |
| 123 | + $ret = $this->mTree[$currLex]->findNextTermNoSkip( $lexemes, $index + 1, $countLexemes ); |
| 124 | + } else { |
| 125 | + $ret = array( $index, &$this->mDefinition ); |
| 126 | + } |
| 127 | + wfProfileOut( __METHOD__ ); |
| 128 | + return $ret; |
| 129 | + } |
| 130 | + |
| 131 | +} |
Property changes on: trunk/extensions/Lingo/LingoTree.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 132 | + native |