3
class FCKeditorParser extends FCKeditorParserWrapper
5
public static $fkc_mw_makeImage_options;
6
protected $fck_mw_strtr_span;
7
protected $fck_mw_strtr_span_counter=1;
8
protected $fck_mw_taghook;
9
protected $fck_internal_parse_text;
10
protected $fck_matches = array();
12
private $FCKeditorMagicWords = array(
18
'__NOCONTENTCONVERT__',
29
* Add special string (that would be changed by Parser) to array and return simple unique string
30
* that will remain unchanged during whole parsing operation.
31
* At the end we'll replace all this unique strings with original content
36
private function fck_addToStrtr($text, $replaceLineBreaks = true) {
37
$key = 'Fckmw'.$this->fck_mw_strtr_span_counter.'fckmw';
38
$this->fck_mw_strtr_span_counter++;
39
if ($replaceLineBreaks) {
40
$this->fck_mw_strtr_span[$key] = str_replace(array("\r\n", "\n", "\r"),"fckLR",$text);
43
$this->fck_mw_strtr_span[$key] = $text;
49
* Handle link to subpage if necessary
50
* @param string $target the source of the link
51
* @param string &$text the link text, modified as necessary
52
* @return string the full name of the link
55
function maybeDoSubpageLink($target, &$text) {
60
* DO NOT Replace special strings like "ISBN xxx" and "RFC xxx" with
61
* magic external links.
66
function doMagicLinks( $text ) {
71
* Callback function for custom tags: feed, ref, references etc.
73
* @param string $str Input
74
* @param array $argv Arguments
77
function fck_genericTagHook( $str, $argv, $parser ) {
78
if (in_array($this->fck_mw_taghook, array("ref", "math", "references", "source"))) {
79
$class = $this->fck_mw_taghook;
86
$ret = "<span class=\"fck_mw_".$class."\" _fck_mw_customtag=\"true\" _fck_mw_tagname=\"".$this->fck_mw_taghook."\">";
89
$ret = "<span class=\"fck_mw_".$class."\" _fck_mw_customtag=\"true\" _fck_mw_tagname=\"".$this->fck_mw_taghook."\"";
90
foreach ($argv as $key=>$value) {
91
$ret .= " ".$key."=\"".$value."\"";
96
$ret = substr($ret, 0, -1) . " />";
99
$ret .= htmlspecialchars($str);
103
$replacement = $this->fck_addToStrtr($ret);
108
* Callback function for wiki tags: nowiki, includeonly, noinclude
110
* @param string $tagName tag name, eg. nowiki, math
111
* @param string $str Input
112
* @param array $argv Arguments
115
function fck_wikiTag( $tagName, $str, $argv = array()) {
117
$ret = "<span class=\"fck_mw_".$tagName."\" _fck_mw_customtag=\"true\" _fck_mw_tagname=\"".$tagName."\">";
120
$ret = "<span class=\"fck_mw_".$tagName."\" _fck_mw_customtag=\"true\" _fck_mw_tagname=\"".$tagName."\"";
121
foreach ($argv as $key=>$value) {
122
$ret .= " ".$key."=\"".$value."\"";
127
$ret = substr($ret, 0, -1) . " />";
130
$ret .= htmlspecialchars($str);
134
$replacement = $this->fck_addToStrtr($ret);
140
* Strips and renders nowiki, pre, math, hiero
141
* If $render is set, performs necessary rendering operations on plugins
142
* Returns the text, and fills an array with data needed in unstrip()
144
* @param StripState $state
146
* @param bool $stripcomments when set, HTML comments <!-- like this -->
147
* will be stripped in addition to other tags. This is important
148
* for section editing, where these comments cause confusion when
149
* counting the sections in the wikisource
151
* @param array dontstrip contains tags which should not be stripped;
152
* used to prevent stipping of <gallery> when saving (fixes bug 2700)
156
function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
157
global $wgContLang, $wgUseTeX, $wgScriptPath, $wgVersion, $wgHooks, $wgExtensionFunctions;
159
wfProfileIn( __METHOD__ );
160
$render = ($this->mOutputType == OT_HTML);
162
$uniq_prefix = $this->mUniqPrefix;
163
$commentState = new ReplacementArray;
164
$nowikiItems = array();
165
$generalItems = array();
167
$elements = array_merge( array( 'nowiki', 'gallery', 'math' ), array_keys( $this->mTagHooks ) );
168
if ( (isset($wgHooks['ParserFirstCallInit']) && in_array('efSyntaxHighlight_GeSHiSetup', $wgHooks['ParserFirstCallInit']))
169
|| (isset($wgExtensionFunctions) && in_array('efSyntaxHighlight_GeSHiSetup', $wgExtensionFunctions)) ) {
170
$elements = array_merge( $elements, array( 'source' ) );
172
if ( (isset($wgHooks['ParserFirstCallInit']) && in_array('wfCite', $wgHooks['ParserFirstCallInit']))
173
|| (isset($wgExtensionFunctions) && in_array('wfCite', $wgExtensionFunctions)) ) {
174
$elements = array_merge( $elements, array( 'ref', 'references' ) );
178
$elements[] = 'html';
181
# Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700)
182
foreach ( $elements AS $k => $v ) {
183
if ( !in_array ( $v , $dontstrip ) ) continue;
184
unset ( $elements[$k] );
187
$elements = array_unique($elements);
189
if (version_compare("1.12", $wgVersion, ">")) {
190
$text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
193
$text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
196
foreach( $matches as $marker => $data ) {
197
list( $element, $content, $params, $tag ) = $data;
199
$tagName = strtolower( $element );
200
wfProfileIn( __METHOD__."-render-$tagName" );
204
if( substr( $tag, -3 ) == '-->' ) {
207
// Unclosed comment in input.
208
// Close it so later stripping can remove it
213
$output = $this->fck_wikiTag('references', $content, $params);
216
$output = $this->fck_wikiTag('ref', $content, $params);
219
$output = $this->fck_wikiTag('source', $content, $params);
223
$output = $this->fck_wikiTag('html', $content, $params);
227
$output = $this->fck_wikiTag('nowiki', $content, $params); //required by FCKeditor
230
if($wgUseTeX){ //normal render
231
$output = $wgContLang->armourMath( MathRenderer::renderMath( $content ) );
232
}else //show fakeimage
233
$output = '<img _fckfakelement="true" class="FCK__MWMath" _fck_mw_math="'.$content.'" src="'.$wgScriptPath.'/skins/common/images/button_math.png" />';
236
$output = $this->fck_wikiTag('gallery', $content, $params); //required by FCKeditor
237
//$output = $this->renderImageGallery( $content, $params );
240
if( isset( $this->mTagHooks[$tagName] ) ) {
241
$this->fck_mw_taghook = $tagName; //required by FCKeditor
242
$output = call_user_func_array( $this->mTagHooks[$tagName],
243
array( $content, $params, $this ) );
245
throw new MWException( "Invalid call hook $element" );
248
wfProfileOut( __METHOD__."-render-$tagName" );
250
// Just stripping tags; keep the source
254
// Unstrip the output, to support recursive strip() calls
255
$output = $state->unstripBoth( $output );
257
if( !$stripcomments && $element == '!--' ) {
258
$commentState->setPair( $marker, $output );
259
} elseif ( $element == 'html' || $element == 'nowiki' ) {
260
$nowikiItems[$marker] = $output;
262
$generalItems[$marker] = $output;
265
# Add the new items to the state
266
# We do this after the loop instead of during it to avoid slowing
267
# down the recursive unstrip
268
$state->nowiki->mergeArray( $nowikiItems );
269
$state->general->mergeArray( $generalItems );
271
# Unstrip comments unless explicitly told otherwise.
272
# (The comments are always stripped prior to this point, so as to
273
# not invoke any extension tags / parser hooks contained within
275
if ( !$stripcomments ) {
276
// Put them all back and forget them
277
$text = $commentState->replace( $text );
280
$this->fck_matches = $matches;
281
wfProfileOut( __METHOD__ );
285
/** Replace HTML comments with unique text using fck_addToStrtr function
288
* @param string $text
291
private function fck_replaceHTMLcomments( $text ) {
292
wfProfileIn( __METHOD__ );
293
while (($start = strpos($text, '<!--')) !== false) {
294
$end = strpos($text, '-->', $start + 4);
295
if ($end === false) {
296
# Unterminated comment; bail out
302
# Trim space and newline if the comment is both
303
# preceded and followed by a newline
304
$spaceStart = max($start - 1, 0);
305
$spaceLen = $end - $spaceStart;
306
while (substr($text, $spaceStart, 1) === ' ' && $spaceStart > 0) {
310
while (substr($text, $spaceStart + $spaceLen, 1) === ' ')
312
if (substr($text, $spaceStart, 1) === "\n" and substr($text, $spaceStart + $spaceLen, 1) === "\n") {
313
# Remove the comment, leading and trailing
314
# spaces, and leave only one newline.
315
$replacement = $this->fck_addToStrtr(substr($text, $spaceStart, $spaceLen+1), false);
316
$text = substr_replace($text, $replacement."\n", $spaceStart, $spaceLen + 1);
319
# Remove just the comment.
320
$replacement = $this->fck_addToStrtr(substr($text, $start, $end - $start), false);
321
$text = substr_replace($text, $replacement, $start, $end - $start);
324
wfProfileOut( __METHOD__ );
329
function replaceInternalLinks( $text ) {
330
$text = preg_replace("/\[\[([^|\[\]]*?)\]\]/", "[[$1|RTENOTITLE]]", $text); //#2223: [[()]] => [[%1|RTENOTITLE]]
331
$text = preg_replace("/\[\[:(.*?)\]\]/", "[[RTECOLON$1]]", $text); //change ':' => 'RTECOLON' in links
332
$text = parent::replaceInternalLinks($text);
333
$text = preg_replace("/\|RTENOTITLE\]\]/", "]]", $text); // remove unused RTENOTITLE
338
function makeImage( $nt, $options ) {
339
FCKeditorParser::$fkc_mw_makeImage_options = $options;
340
return parent::makeImage( $nt, $options );
344
* Replace templates with unique text to preserve them from parsing
346
* @todo if {{template}} is inside string that also must be returned unparsed,
347
* e.g. <noinclude>{{template}}</noinclude>
348
* {{template}} replaced with Fckmw[n]fckmw which is wrong...
350
* @param string $text
353
private function fck_replaceTemplates( $text ) {
355
$callback = array('{' =>
359
2=>array('FCKeditorParser', 'fck_leaveTemplatesAlone'),
360
3=>array('FCKeditorParser', 'fck_leaveTemplatesAlone'),
367
$text = $this->replace_callback($text, $callback);
372
while (false !== ($pos = strpos($textTmp, "<!--FCK_SKIP_START-->")))
374
$tags[abs($pos + $offset)] = 1;
375
$textTmp = substr($textTmp, $pos+21);
376
$offset += $pos + 21;
381
while (false !== ($pos = strpos($textTmp, "<!--FCK_SKIP_END-->")))
383
$tags[abs($pos + $offset)] = -1;
384
$textTmp = substr($textTmp, $pos+19);
385
$offset += $pos + 19;
391
$strtr = array("<!--FCK_SKIP_START-->" => "", "<!--FCK_SKIP_END-->" => "");
399
$strtr_span = array();
400
foreach ($tags as $pos=>$type) {
407
$opened = substr_count($text, '[', 0, $pos); //count [
408
$closed = substr_count($text, ']', 0, $pos); //count ]
410
if ($sum == 1 && $lastSum == 0) {
411
$stringToParse .= strtr(substr($text, $startingPos, $pos - $startingPos), $strtr);
414
else if ($sum == 0) {
415
$stringToParse .= 'Fckmw'.$this->fck_mw_strtr_span_counter.'fckmw';
416
$inner = htmlspecialchars(strtr(substr($text, $startingPos, $pos - $startingPos + 19), $strtr));
417
$this->fck_mw_strtr_span['href="Fckmw'.$this->fck_mw_strtr_span_counter.'fckmw"'] = 'href="'.$inner.'"';
418
if($opened <= $closed) { // {{template}} is NOT in [] or [[]]
419
$this->fck_mw_strtr_span['Fckmw'.$this->fck_mw_strtr_span_counter.'fckmw'] = '<span class="fck_mw_template">'.str_replace(array("\r\n", "\n", "\r"),"fckLR",$inner).'</span>';
421
$this->fck_mw_strtr_span['Fckmw'.$this->fck_mw_strtr_span_counter.'fckmw'] = str_replace(array("\r\n", "\n", "\r"),"fckLR",$inner);
423
$startingPos = $pos + 19;
424
$this->fck_mw_strtr_span_counter++;
428
$stringToParse .= substr($text, $startingPos);
429
$text = &$stringToParse;
435
function internalParse ( $text ) {
437
$this->fck_internal_parse_text =& $text;
439
//these three tags should remain unchanged
440
$text = StringUtils::delimiterReplaceCallback( '<includeonly>', '</includeonly>', array($this, 'fck_includeonly'), $text );
441
$text = StringUtils::delimiterReplaceCallback( '<noinclude>', '</noinclude>', array($this, 'fck_noinclude'), $text );
442
$text = StringUtils::delimiterReplaceCallback( '<onlyinclude>', '</onlyinclude>', array($this, 'fck_onlyinclude'), $text );
444
//html comments shouldn't be stripped
445
$text = $this->fck_replaceHTMLcomments( $text );
446
//as well as templates
447
$text = $this->fck_replaceTemplates( $text );
449
$finalString = parent::internalParse($text);
453
function fck_includeonly( $matches ) {
454
return $this->fck_wikiTag('includeonly', $matches[1]);
456
function fck_noinclude( $matches ) {
457
return $this->fck_wikiTag('noinclude', $matches[1]);
459
function fck_onlyinclude( $matches ) {
460
return $this->fck_wikiTag('onlyinclude', $matches[1]);
462
function fck_leaveTemplatesAlone( $matches ) {
463
return "<!--FCK_SKIP_START-->".$matches['text']."<!--FCK_SKIP_END-->";
465
function formatHeadings( $text, $isMain=true ) {
468
function replaceFreeExternalLinks( $text ) { return $text; }
469
function stripNoGallery(&$text) {}
470
function stripToc( $text ) {
471
//$prefix = '<span class="fck_mw_magic">';
472
//$suffix = '</span>';
477
foreach ($this->FCKeditorMagicWords as $word) {
478
$strtr[$word] = $prefix . $word . $suffix;
481
return strtr( $text, $strtr );
484
function doDoubleUnderscore( $text ) {
488
function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
489
$text = preg_replace("/^#REDIRECT/", "<!--FCK_REDIRECT-->", $text);
490
$parserOutput = parent::parse($text, $title, $options, $linestart , $clearState , $revid );
492
$categories = $parserOutput->getCategories();
495
foreach ($categories as $cat=>$val) {
497
if( $val == 'RTENOTITLE' ){
498
$args .= '_fcknotitle="true" ';
501
if ($val != $title->mTextform) {
502
$appendString .= "<a ".$args."href=\"Category:" . $cat ."\">" . $val ."</a> ";
505
$appendString .= "<a ".$args."href=\"Category:" . $cat ."\">Category:" . $cat ."</a> ";
508
$parserOutput->setText($parserOutput->getText() . $appendString);
511
if (!empty($this->fck_mw_strtr_span)) {
512
global $leaveRawTemplates;
513
if (!empty($leaveRawTemplates)) {
514
foreach ($leaveRawTemplates as $l) {
515
$this->fck_mw_strtr_span[$l] = substr($this->fck_mw_strtr_span[$l], 30, -7);
518
$text = strtr($parserOutput->getText(), $this->fck_mw_strtr_span);
519
$parserOutput->setText(strtr($text, $this->fck_mw_strtr_span));
521
if (!empty($this->fck_matches)) {
522
$text = $parserOutput->getText() ;
523
foreach ($this->fck_matches as $key => $m) {
524
$text = str_replace( $key, $m[3], $text);
526
$parserOutput->setText($text);
529
if (!empty($parserOutput->mLanguageLinks)) {
530
foreach ($parserOutput->mLanguageLinks as $l) {
531
$parserOutput->setText($parserOutput->getText() . "\n" . "<a href=\"".$l."\">".$l."</a>") ;
535
$parserOutput->setText(str_replace("<!--FCK_REDIRECT-->", "#REDIRECT", $parserOutput->getText()));
537
return $parserOutput;
541
* Make lists from lines starting with ':', '*', '#', etc.
544
* @return string the lists rendered as HTML
546
function doBlockLevels( $text, $linestart ) {
547
$fname = 'Parser::doBlockLevels';
548
wfProfileIn( $fname );
550
# Parsing through the text line by line. The main thing
551
# happening here is handling of block-level elements p, pre,
552
# and making lists from lines starting with * # : etc.
554
$textLines = explode( "\n", $text );
556
$lastPrefix = $output = '';
557
$this->mDTopen = $inBlockElem = false;
559
$paragraphStack = false;
562
$output .= array_shift( $textLines );
564
foreach ( $textLines as $oLine ) {
565
$lastPrefixLength = strlen( $lastPrefix );
566
$preCloseMatch = preg_match('/<\\/pre/i', $oLine );
567
$preOpenMatch = preg_match('/<pre/i', $oLine );
568
if ( !$this->mInPre ) {
569
# Multiple prefixes may abut each other for nested lists.
570
$prefixLength = strspn( $oLine, '*#:;' );
571
$pref = substr( $oLine, 0, $prefixLength );
574
$pref2 = str_replace( ';', ':', $pref );
575
$t = substr( $oLine, $prefixLength );
576
$this->mInPre = !empty($preOpenMatch);
578
# Don't interpret any other prefixes in preformatted text
585
if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
586
# Same as the last item, so no need to deal with nesting or opening stuff
587
$output .= $this->nextItem( substr( $pref, -1 ) );
588
$paragraphStack = false;
590
if ( substr( $pref, -1 ) == ';') {
591
# The one nasty exception: definition lists work like this:
592
# ; title : definition text
593
# So we check for : in the remainder text to split up the
594
# title and definition, without b0rking links.
596
if ($this->findColonNoLinks($t, $term, $t2) !== false) {
598
$output .= $term . $this->nextItem( ':' );
601
} elseif( $prefixLength || $lastPrefixLength ) {
602
# Either open or close a level...
603
$commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
604
$paragraphStack = false;
606
while( $commonPrefixLength < $lastPrefixLength ) {
607
$output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
610
if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
611
$output .= $this->nextItem( $pref{$commonPrefixLength-1} );
613
while ( $prefixLength > $commonPrefixLength ) {
614
$char = substr( $pref, $commonPrefixLength, 1 );
615
$output .= $this->openList( $char );
617
if ( ';' == $char ) {
618
# FIXME: This is dupe of code above
619
if ($this->findColonNoLinks($t, $term, $t2) !== false) {
621
$output .= $term . $this->nextItem( ':' );
624
++$commonPrefixLength;
626
$lastPrefix = $pref2;
628
if( 0 == $prefixLength ) {
629
wfProfileIn( "$fname-paragraph" );
630
# No prefix (not in list)--go to paragraph mode
631
// XXX: use a stack for nestable elements like span, table and div
632
$openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
633
$closematch = preg_match(
634
'/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
635
'<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
636
if ( $openmatch or $closematch ) {
637
$paragraphStack = false;
638
# TODO bug 5718: paragraph closed
639
$output .= $this->closeParagraph();
640
if ( $preOpenMatch and !$preCloseMatch ) {
641
$this->mInPre = true;
644
$inBlockElem = false;
648
} else if ( !$inBlockElem && !$this->mInPre ) {
649
if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
651
if ($this->mLastSection != 'pre') {
652
$paragraphStack = false;
653
$output .= $this->closeParagraph().'<pre class="_fck_mw_lspace">';
654
$this->mLastSection = 'pre';
656
$t = substr( $t, 1 );
659
if ( '' == trim($t) ) {
660
if ( $paragraphStack ) {
661
$output .= $paragraphStack.'<br />';
662
$paragraphStack = false;
663
$this->mLastSection = 'p';
665
if ($this->mLastSection != 'p' ) {
666
$output .= $this->closeParagraph();
667
$this->mLastSection = '';
668
$paragraphStack = '<p>';
670
$paragraphStack = '</p><p>';
674
if ( $paragraphStack ) {
675
$output .= $paragraphStack;
676
$paragraphStack = false;
677
$this->mLastSection = 'p';
678
} else if ($this->mLastSection != 'p') {
679
$output .= $this->closeParagraph().'<p>';
680
$this->mLastSection = 'p';
685
wfProfileOut( "$fname-paragraph" );
687
// somewhere above we forget to get out of pre block (bug 785)
688
if($preCloseMatch && $this->mInPre) {
689
$this->mInPre = false;
691
if ($paragraphStack === false) {
695
while ( $prefixLength ) {
696
$output .= $this->closeList( $pref2{$prefixLength-1} );
699
if ( '' != $this->mLastSection ) {
700
$output .= '</' . $this->mLastSection . '>';
701
$this->mLastSection = '';
704
wfProfileOut( $fname );