xhtml tweaks, incomplete. sometimes closing p where there should be none
[lhc/web/wiklou.git] / includes / Parser.php
1 <?php
2
3 include_once('Tokenizer.php');
4
5 # PHP Parser
6 #
7 # Processes wiki markup
8 #
9 # There are two main entry points into the Parser class: parse() and preSaveTransform().
10 # The parse() function produces HTML output, preSaveTransform() produces altered wiki markup.
11 #
12 # Globals used:
13 # objects: $wgLang, $wgDateFormatter, $wgLinkCache, $wgCurParser
14 #
15 # NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
16 #
17 # settings: $wgUseTex*, $wgUseCategoryMagic*, $wgUseDynamicDates*, $wgInterwikiMagic*,
18 # $wgNamespacesWithSubpages, $wgLanguageCode, $wgAllowExternalImages*,
19 # $wgLocaltimezone
20 #
21 # * only within ParserOptions
22 #
23 #
24 #----------------------------------------
25 # Variable substitution O(N^2) attack
26 #-----------------------------------------
27 # Without countermeasures, it would be possible to attack the parser by saving a page
28 # filled with a large number of inclusions of large pages. The size of the generated
29 # page would be proportional to the square of the input size. Hence, we limit the number
30 # of inclusions of any given page, thus bringing any attack back to O(N).
31 #
32 define( "MAX_INCLUDE_REPEAT", 5 );
33
34 # Recursion depth of variable/inclusion evaluation
35 define( "MAX_INCLUDE_PASSES", 3 );
36
37 # Allowed values for $mOutputType
38 define( "OT_HTML", 1 );
39 define( "OT_WIKI", 2 );
40
41 class Parser
42 {
43 # Cleared with clearState():
44 var $mOutput, $mAutonumber, $mLastSection, $mDTopen, $mStripState;
45 var $mVariables, $mIncludeCount;
46
47 # Temporary:
48 var $mOptions, $mTitle, $mOutputType;
49
50 function Parser()
51 {
52 $this->clearState();
53 }
54
55 function clearState()
56 {
57 $this->mOutput = new ParserOutput;
58 $this->mAutonumber = 0;
59 $this->mLastSection = "";
60 $this->mDTopen = false;
61 $this->mStripState = false;
62 $this->mVariables = false;
63 $this->mIncludeCount = array();
64 }
65
66 # First pass--just handle <nowiki> sections, pass the rest off
67 # to doWikiPass2() which does all the real work.
68 #
69 # Returns a ParserOutput
70 #
71 function parse( $text, &$title, $options, $linestart = true, $clearState = true )
72 {
73 $fname = "Parser::parse";
74 wfProfileIn( $fname );
75
76 if ( $clearState ) {
77 $this->clearState();
78 }
79
80 $this->mOptions = $options;
81 $this->mTitle =& $title;
82 $this->mOutputType = OT_HTML;
83
84 $stripState = NULL;
85 $text = $this->strip( $text, $this->mStripState );
86 $text = $this->doWikiPass2( $text, $linestart );
87 $text = $this->unstrip( $text, $this->mStripState );
88
89 $this->mOutput->setText( $text );
90 wfProfileOut( $fname );
91 return $this->mOutput;
92 }
93
94 /* static */ function getRandomString()
95 {
96 return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
97 }
98
99 # Strips <nowiki>, <pre> and <math>
100 # Returns the text, and fills an array with data needed in unstrip()
101 #
102 function strip( $text, &$state )
103 {
104 $state = array(
105 'nwlist' => array(),
106 'nwsecs' => 0,
107 'nwunq' => Parser::getRandomString(),
108 'mathlist' => array(),
109 'mathsecs' => 0,
110 'mathunq' => Parser::getRandomString(),
111 'prelist' => array(),
112 'presecs' => 0,
113 'preunq' => Parser::getRandomString()
114 );
115 $render = ($this->mOutputType == OT_HTML);
116 $stripped = "";
117 $stripped2 = "";
118 $stripped3 = "";
119
120 # Replace any instances of the placeholders
121 $text = str_replace( $state['nwunq'], wfHtmlEscapeFirst( $state['nwunq'] ), $text );
122 $text = str_replace( $state['mathunq'], wfHtmlEscapeFirst( $state['mathunq'] ), $text );
123 $text = str_replace( $state['preunq'], wfHtmlEscapeFirst( $state['preunq'] ), $text );
124
125 while ( "" != $text ) {
126 $p = preg_split( "/<\\s*nowiki\\s*>/i", $text, 2 );
127 $stripped .= $p[0];
128 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) {
129 $text = "";
130 } else {
131 $q = preg_split( "/<\\/\\s*nowiki\\s*>/i", $p[1], 2 );
132 ++$state['nwsecs'];
133
134 if ( $render ) {
135 $state['nwlist'][$state['nwsecs']] = wfEscapeHTMLTagsOnly($q[0]);
136 } else {
137 $state['nwlist'][$state['nwsecs']] = "<nowiki>{$q[0]}</nowiki>";
138 }
139
140 $stripped .= $state['nwunq'] . sprintf("%08X", $state['nwsecs']);
141 $text = $q[1];
142 }
143 }
144
145 if( $this->mOptions->getUseTeX() ) {
146 while ( "" != $stripped ) {
147 $p = preg_split( "/<\\s*math\\s*>/i", $stripped, 2 );
148 $stripped2 .= $p[0];
149 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) {
150 $stripped = "";
151 } else {
152 $q = preg_split( "/<\\/\\s*math\\s*>/i", $p[1], 2 );
153 ++$state['mathsecs'];
154
155 if ( $render ) {
156 $state['mathlist'][$state['mathsecs']] = renderMath($q[0]);
157 } else {
158 $state['mathlist'][$state['mathsecs']] = "<math>{$q[0]}</math>";
159 }
160
161 $stripped2 .= $state['mathunq'] . sprintf("%08X", $state['mathsecs']);
162 $stripped = $q[1];
163 }
164 }
165 } else {
166 $stripped2 = $stripped;
167 }
168
169 while ( "" != $stripped2 ) {
170 $p = preg_split( "/<\\s*pre\\s*>/i", $stripped2, 2 );
171 $stripped3 .= $p[0];
172 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) {
173 $stripped2 = "";
174 } else {
175 $q = preg_split( "/<\\/\\s*pre\\s*>/i", $p[1], 2 );
176 ++$state['presecs'];
177
178 if ( $render ) {
179 $state['prelist'][$state['presecs']] = "<pre>". wfEscapeHTMLTagsOnly($q[0]). "</pre>\n";
180 } else {
181 $state['prelist'][$state['presecs']] = "<pre>{$q[0]}</pre>";
182 }
183
184 $stripped3 .= $state['preunq'] . sprintf("%08X", $state['presecs']);
185 $stripped2 = $q[1];
186 }
187 }
188 return $stripped3;
189 }
190
191 function unstrip( $text, &$state )
192 {
193 for ( $i = 1; $i <= $state['presecs']; ++$i ) {
194 $text = str_replace( $state['preunq'] . sprintf("%08X", $i), $state['prelist'][$i], $text );
195 }
196
197 for ( $i = 1; $i <= $state['mathsecs']; ++$i ) {
198 $text = str_replace( $state['mathunq'] . sprintf("%08X", $i), $state['mathlist'][$i], $text );
199 }
200
201 for ( $i = 1; $i <= $state['nwsecs']; ++$i ) {
202 $text = str_replace( $state['nwunq'] . sprintf("%08X", $i), $state['nwlist'][$i], $text );
203 }
204 return $text;
205 }
206
207 function categoryMagic ()
208 {
209 global $wgLang , $wgUser ;
210 if ( !$this->mOptions->getUseCategoryMagic() ) return ;
211 $id = $this->mTitle->getArticleID() ;
212 $cat = $wgLang->ucfirst ( wfMsg ( "category" ) ) ;
213 $ti = $this->mTitle->getText() ;
214 $ti = explode ( ":" , $ti , 2 ) ;
215 if ( $cat != $ti[0] ) return "" ;
216 $r = "<br break='all'/>\n" ;
217
218 $articles = array() ;
219 $parents = array () ;
220 $children = array() ;
221
222
223 # $sk =& $this->mGetSkin();
224 $sk =& $wgUser->getSkin() ;
225
226 $doesexist = false ;
227 if ( $doesexist ) {
228 $sql = "SELECT cur_title,cur_namespace FROM cur,links WHERE l_to={$id} AND l_from=cur_id";
229 } else {
230 $sql = "SELECT cur_title,cur_namespace FROM cur,brokenlinks WHERE bl_to={$id} AND bl_from=cur_id" ;
231 }
232
233 $res = wfQuery ( $sql, DB_READ ) ;
234 while ( $x = wfFetchObject ( $res ) )
235 {
236 # $t = new Title ;
237 # $t->newFromDBkey ( $x->l_from ) ;
238 # $t = $t->getText() ;
239 $t = $wgLang->getNsText ( $x->cur_namespace ) ;
240 if ( $t != "" ) $t .= ":" ;
241 $t .= $x->cur_title ;
242
243 $y = explode ( ":" , $t , 2 ) ;
244 if ( count ( $y ) == 2 && $y[0] == $cat ) {
245 array_push ( $children , $sk->makeLink ( $t , $y[1] ) ) ;
246 } else {
247 array_push ( $articles , $sk->makeLink ( $t ) ) ;
248 }
249 }
250 wfFreeResult ( $res ) ;
251
252 # Children
253 if ( count ( $children ) > 0 )
254 {
255 asort ( $children ) ;
256 $r .= "<h2>".wfMsg("subcategories")."</h2>\n" ;
257 $r .= implode ( ", " , $children ) ;
258 }
259
260 # Articles
261 if ( count ( $articles ) > 0 )
262 {
263 asort ( $articles ) ;
264 $h = wfMsg( "category_header", $ti[1] );
265 $r .= "<h2>{$h}</h2>\n" ;
266 $r .= implode ( ", " , $articles ) ;
267 }
268
269
270 return $r ;
271 }
272
273 function getHTMLattrs ()
274 {
275 $htmlattrs = array( # Allowed attributes--no scripting, etc.
276 "title", "align", "lang", "dir", "width", "height",
277 "bgcolor", "clear", /* BR */ "noshade", /* HR */
278 "cite", /* BLOCKQUOTE, Q */ "size", "face", "color",
279 /* FONT */ "type", "start", "value", "compact",
280 /* For various lists, mostly deprecated but safe */
281 "summary", "width", "border", "frame", "rules",
282 "cellspacing", "cellpadding", "valign", "char",
283 "charoff", "colgroup", "col", "span", "abbr", "axis",
284 "headers", "scope", "rowspan", "colspan", /* Tables */
285 "id", "class", "name", "style" /* For CSS */
286 );
287 return $htmlattrs ;
288 }
289
290 function fixTagAttributes ( $t )
291 {
292 if ( trim ( $t ) == "" ) return "" ; # Saves runtime ;-)
293 $htmlattrs = $this->getHTMLattrs() ;
294
295 # Strip non-approved attributes from the tag
296 $t = preg_replace(
297 "/(\\w+)(\\s*=\\s*([^\\s\">]+|\"[^\">]*\"))?/e",
298 "(in_array(strtolower(\"\$1\"),\$htmlattrs)?(\"\$1\".((\"x\$3\" != \"x\")?\"=\$3\":'')):'')",
299 $t);
300 # Strip javascript "expression" from stylesheets. Brute force approach:
301 # If anythin offensive is found, all attributes of the HTML tag are dropped
302
303 if( preg_match(
304 "/style\\s*=.*(expression|tps*:\/\/|url\\s*\().*/is",
305 wfMungeToUtf8( $t ) ) )
306 {
307 $t="";
308 }
309
310 return trim ( $t ) ;
311 }
312
313 function doTableStuff ( $t )
314 {
315 $t = explode ( "\n" , $t ) ;
316 $td = array () ; # Is currently a td tag open?
317 $ltd = array () ; # Was it TD or TH?
318 $tr = array () ; # Is currently a tr tag open?
319 $ltr = array () ; # tr attributes
320 foreach ( $t AS $k => $x )
321 {
322 $x = rtrim ( $x ) ;
323 $fc = substr ( $x , 0 , 1 ) ;
324 if ( "{|" == substr ( $x , 0 , 2 ) )
325 {
326 $t[$k] = "<table " . $this->fixTagAttributes ( substr ( $x , 3 ) ) . ">" ;
327 array_push ( $td , false ) ;
328 array_push ( $ltd , "" ) ;
329 array_push ( $tr , false ) ;
330 array_push ( $ltr , "" ) ;
331 }
332 else if ( count ( $td ) == 0 ) { } # Don't do any of the following
333 else if ( "|}" == substr ( $x , 0 , 2 ) )
334 {
335 $z = "</table>\n" ;
336 $l = array_pop ( $ltd ) ;
337 if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
338 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
339 array_pop ( $ltr ) ;
340 $t[$k] = $z ;
341 }
342 /* else if ( "|_" == substr ( $x , 0 , 2 ) ) # Caption
343 {
344 $z = trim ( substr ( $x , 2 ) ) ;
345 $t[$k] = "<caption>{$z}</caption>\n" ;
346 }*/
347 else if ( "|-" == substr ( $x , 0 , 2 ) ) # Allows for |---------------
348 {
349 $x = substr ( $x , 1 ) ;
350 while ( $x != "" && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ;
351 $z = "" ;
352 $l = array_pop ( $ltd ) ;
353 if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
354 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
355 array_pop ( $ltr ) ;
356 $t[$k] = $z ;
357 array_push ( $tr , false ) ;
358 array_push ( $td , false ) ;
359 array_push ( $ltd , "" ) ;
360 array_push ( $ltr , $this->fixTagAttributes ( $x ) ) ;
361 }
362 else if ( "|" == $fc || "!" == $fc || "|+" == substr ( $x , 0 , 2 ) ) # Caption
363 {
364 if ( "|+" == substr ( $x , 0 , 2 ) )
365 {
366 $fc = "+" ;
367 $x = substr ( $x , 1 ) ;
368 }
369 $after = substr ( $x , 1 ) ;
370 if ( $fc == "!" ) $after = str_replace ( "!!" , "||" , $after ) ;
371 $after = explode ( "||" , $after ) ;
372 $t[$k] = "" ;
373 foreach ( $after AS $theline )
374 {
375 $z = "" ;
376 if ( $fc != "+" )
377 {
378 $tra = array_pop ( $ltr ) ;
379 if ( !array_pop ( $tr ) ) $z = "<tr {$tra}>\n" ;
380 array_push ( $tr , true ) ;
381 array_push ( $ltr , "" ) ;
382 }
383
384 $l = array_pop ( $ltd ) ;
385 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
386 if ( $fc == "|" ) $l = "TD" ;
387 else if ( $fc == "!" ) $l = "TH" ;
388 else if ( $fc == "+" ) $l = "CAPTION" ;
389 else $l = "" ;
390 array_push ( $ltd , $l ) ;
391 $y = explode ( "|" , $theline , 2 ) ;
392 if ( count ( $y ) == 1 ) $y = "{$z}<{$l}>{$y[0]}" ;
393 else $y = $y = "{$z}<{$l} ".$this->fixTagAttributes($y[0]).">{$y[1]}" ;
394 $t[$k] .= $y ;
395 array_push ( $td , true ) ;
396 }
397 }
398 }
399
400 # Closing open td, tr && table
401 while ( count ( $td ) > 0 )
402 {
403 if ( array_pop ( $td ) ) $t[] = "</td>" ;
404 if ( array_pop ( $tr ) ) $t[] = "</tr>" ;
405 $t[] = "</table>" ;
406 }
407
408 $t = implode ( "\n" , $t ) ;
409 # $t = $this->removeHTMLtags( $t );
410 return $t ;
411 }
412
413 # Well, OK, it's actually about 14 passes. But since all the
414 # hard lifting is done inside PHP's regex code, it probably
415 # wouldn't speed things up much to add a real parser.
416 #
417 function doWikiPass2( $text, $linestart )
418 {
419 $fname = "Parser::doWikiPass2";
420 wfProfileIn( $fname );
421
422 $text = $this->removeHTMLtags( $text );
423 $text = $this->replaceVariables( $text );
424
425 # $text = preg_replace( "/(^|\n)-----*/", "\\1<hr>", $text );
426 $text = str_replace ( "<HR>", "<hr/>", $text );
427
428 $text = $this->doHeadings( $text );
429 $text = $this->doBlockLevels( $text, $linestart );
430
431 if($this->mOptions->getUseDynamicDates()) {
432 global $wgDateFormatter;
433 $text = $wgDateFormatter->reformat( $this->mOptions->getDateFormat(), $text );
434 }
435
436 $text = $this->replaceExternalLinks( $text );
437 $text = $this->replaceInternalLinks ( $text );
438 $text = $this->doTableStuff ( $text ) ;
439
440 $text = $this->formatHeadings( $text );
441
442 $sk =& $this->mOptions->getSkin();
443 $text = $sk->transformContent( $text );
444 $text .= $this->categoryMagic () ;
445
446 wfProfileOut( $fname );
447 return $text;
448 }
449
450
451 /* private */ function doHeadings( $text )
452 {
453 for ( $i = 6; $i >= 1; --$i ) {
454 $h = substr( "======", 0, $i );
455 $text = preg_replace( "/^{$h}(.+){$h}(\\s|$)/m",
456 "<h{$i}>\\1</h{$i}>\\2", $text );
457 }
458 return $text;
459 }
460
461 # Note: we have to do external links before the internal ones,
462 # and otherwise take great care in the order of things here, so
463 # that we don't end up interpreting some URLs twice.
464
465 /* private */ function replaceExternalLinks( $text )
466 {
467 $fname = "Parser::replaceExternalLinks";
468 wfProfileIn( $fname );
469 $text = $this->subReplaceExternalLinks( $text, "http", true );
470 $text = $this->subReplaceExternalLinks( $text, "https", true );
471 $text = $this->subReplaceExternalLinks( $text, "ftp", false );
472 $text = $this->subReplaceExternalLinks( $text, "irc", false );
473 $text = $this->subReplaceExternalLinks( $text, "gopher", false );
474 $text = $this->subReplaceExternalLinks( $text, "news", false );
475 $text = $this->subReplaceExternalLinks( $text, "mailto", false );
476 wfProfileOut( $fname );
477 return $text;
478 }
479
480 /* private */ function subReplaceExternalLinks( $s, $protocol, $autonumber )
481 {
482 $unique = "4jzAfzB8hNvf4sqyO9Edd8pSmk9rE2in0Tgw3";
483 $uc = "A-Za-z0-9_\\/~%\\-+&*#?!=()@\\x80-\\xFF";
484
485 # this is the list of separators that should be ignored if they
486 # are the last character of an URL but that should be included
487 # if they occur within the URL, e.g. "go to www.foo.com, where .."
488 # in this case, the last comma should not become part of the URL,
489 # but in "www.foo.com/123,2342,32.htm" it should.
490 $sep = ",;\.:";
491 $fnc = "A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF";
492 $images = "gif|png|jpg|jpeg";
493
494 # PLEASE NOTE: The curly braces { } are not part of the regex,
495 # they are interpreted as part of the string (used to tell PHP
496 # that the content of the string should be inserted there).
497 $e1 = "/(^|[^\\[])({$protocol}:)([{$uc}{$sep}]+)\\/([{$fnc}]+)\\." .
498 "((?i){$images})([^{$uc}]|$)/";
499
500 $e2 = "/(^|[^\\[])({$protocol}:)(([".$uc."]|[".$sep."][".$uc."])+)([^". $uc . $sep. "]|[".$sep."]|$)/";
501 $sk =& $this->mOptions->getSkin();
502
503 if ( $autonumber and $this->mOptions->getAllowExternalImages() ) { # Use img tags only for HTTP urls
504 $s = preg_replace( $e1, "\\1" . $sk->makeImage( "{$unique}:\\3" .
505 "/\\4.\\5", "\\4.\\5" ) . "\\6", $s );
506 }
507 $s = preg_replace( $e2, "\\1" . "<a href=\"{$unique}:\\3\"" .
508 $sk->getExternalLinkAttributes( "{$unique}:\\3", wfEscapeHTML(
509 "{$unique}:\\3" ) ) . ">" . wfEscapeHTML( "{$unique}:\\3" ) .
510 "</a>\\5", $s );
511 $s = str_replace( $unique, $protocol, $s );
512
513 $a = explode( "[{$protocol}:", " " . $s );
514 $s = array_shift( $a );
515 $s = substr( $s, 1 );
516
517 $e1 = "/^([{$uc}"."{$sep}]+)](.*)\$/sD";
518 $e2 = "/^([{$uc}"."{$sep}]+)\\s+([^\\]]+)](.*)\$/sD";
519
520 foreach ( $a as $line ) {
521 if ( preg_match( $e1, $line, $m ) ) {
522 $link = "{$protocol}:{$m[1]}";
523 $trail = $m[2];
524 if ( $autonumber ) { $text = "[" . ++$this->mAutonumber . "]"; }
525 else { $text = wfEscapeHTML( $link ); }
526 } else if ( preg_match( $e2, $line, $m ) ) {
527 $link = "{$protocol}:{$m[1]}";
528 $text = $m[2];
529 $trail = $m[3];
530 } else {
531 $s .= "[{$protocol}:" . $line;
532 continue;
533 }
534 if( $link == $text || preg_match( "!$protocol://" . preg_quote( $text, "/" ) . "/?$!", $link ) ) {
535 $paren = "";
536 } else {
537 # Expand the URL for printable version
538 $paren = "<span class='urlexpansion'> (<i>" . htmlspecialchars ( $link ) . "</i>)</span>";
539 }
540 $la = $sk->getExternalLinkAttributes( $link, $text );
541 $s .= "<a href='{$link}'{$la}>{$text}</a>{$paren}{$trail}";
542
543 }
544 return $s;
545 }
546
547 /* private */ function handle3Quotes( &$state, $token )
548 {
549 if ( $state["strong"] ) {
550 if ( $state["em"] && $state["em"] > $state["strong"] )
551 {
552 # ''' lala ''lala '''
553 $s = "</em></strong><em>";
554 } else {
555 $s = "</strong>";
556 }
557 $state["strong"] = FALSE;
558 } else {
559 $s = "<strong>";
560 $state["strong"] = $token["pos"];
561 }
562 return $s;
563 }
564
565 /* private */ function handle2Quotes( &$state, $token )
566 {
567 if ( $state["em"] ) {
568 if ( $state["strong"] && $state["strong"] > $state["em"] )
569 {
570 # ''lala'''lala'' ....'''
571 $s = "</strong></em><strong>";
572 } else {
573 $s = "</em>";
574 }
575 $state["em"] = FALSE;
576 } else {
577 $s = "<em>";
578 $state["em"] = $token["pos"];
579 }
580 return $s;
581 }
582
583 /* private */ function handle5Quotes( &$state, $token )
584 {
585 if ( $state["em"] && $state["strong"] ) {
586 if ( $state["em"] < $state["strong"] ) {
587 $s .= "</strong></em>";
588 } else {
589 $s .= "</em></strong>";
590 }
591 $state["strong"] = $state["em"] = FALSE;
592 } elseif ( $state["em"] ) {
593 $s .= "</em><strong>";
594 $state["em"] = FALSE;
595 $state["strong"] = $token["pos"];
596 } elseif ( $state["strong"] ) {
597 $s .= "</strong><em>";
598 $state["strong"] = FALSE;
599 $state["em"] = $token["pos"];
600 } else { # not $em and not $strong
601 $s .= "<strong><em>";
602 $state["strong"] = $state["em"] = $token["pos"];
603 }
604 return $s;
605 }
606
607 /* private */ function replaceInternalLinks( $str )
608 {
609 global $wgLang; # for language specific parser hook
610
611 $tokenizer=Tokenizer::newFromString( $str );
612 $tokenStack = array();
613
614 $s="";
615 $state["em"] = FALSE;
616 $state["strong"] = FALSE;
617 $tagIsOpen = FALSE;
618 $threeopen = false;
619
620 # The tokenizer splits the text into tokens and returns them one by one.
621 # Every call to the tokenizer returns a new token.
622 while ( $token = $tokenizer->nextToken() )
623 {
624 $threeopen = false;
625 switch ( $token["type"] )
626 {
627 case "text":
628 # simple text with no further markup
629 $txt = $token["text"];
630 break;
631 case "[[[":
632 # remember the tag opened with 3 [
633 $threeopen = true;
634 case "[[":
635 # link opening tag.
636 # FIXME : Treat orphaned open tags (stack not empty when text is over)
637 $tagIsOpen = TRUE;
638 array_push( $tokenStack, $token );
639 $txt="";
640 break;
641
642 case "]]]":
643 case "]]":
644 # link close tag.
645 # get text from stack, glue it together, and call the code to handle a
646 # link
647
648 if ( count( $tokenStack ) == 0 )
649 {
650 # stack empty. Found a ]] without an opening [[
651 $txt = "]]";
652 } else {
653 $linkText = "";
654 $lastToken = array_pop( $tokenStack );
655 while ( !(($lastToken["type"] == "[[[") or ($lastToken["type"] == "[[")) )
656 {
657 if( !empty( $lastToken["text"] ) ) {
658 $linkText = $lastToken["text"] . $linkText;
659 }
660 $lastToken = array_pop( $tokenStack );
661 }
662
663 $txt = $linkText ."]]";
664
665 if( isset( $lastToken["text"] ) ) {
666 $prefix = $lastToken["text"];
667 } else {
668 $prefix = "";
669 }
670 $nextToken = $tokenizer->previewToken();
671 if ( $nextToken["type"] == "text" )
672 {
673 # Preview just looks at it. Now we have to fetch it.
674 $nextToken = $tokenizer->nextToken();
675 $txt .= $nextToken["text"];
676 }
677 $txt = $this->handleInternalLink( $txt, $prefix );
678
679 # did the tag start with 3 [ ?
680 if($threeopen) {
681 # show the first as text
682 $txt = "[".$txt;
683 $threeopen=false;
684 }
685
686 }
687 $tagIsOpen = (count( $tokenStack ) != 0);
688 break;
689 case "----":
690 $txt = "\n<hr/>\n";
691 break;
692 case "'''":
693 # This and the three next ones handle quotes
694 $txt = $this->handle3Quotes( $state, $token );
695 break;
696 case "''":
697 $txt = $this->handle2Quotes( $state, $token );
698 break;
699 case "'''''":
700 $txt = $this->handle5Quotes( $state, $token );
701 break;
702 case "":
703 # empty token
704 $txt="";
705 break;
706 case "RFC ":
707 if ( $tagIsOpen ) {
708 $txt = "RFC ";
709 } else {
710 $txt = $this->doMagicRFC( $tokenizer );
711 }
712 break;
713 case "ISBN ":
714 if ( $tagIsOpen ) {
715 $txt = "ISBN ";
716 } else {
717 $txt = $this->doMagicISBN( $tokenizer );
718 }
719 break;
720 default:
721 # Call language specific Hook.
722 $txt = $wgLang->processToken( $token, $tokenStack );
723 if ( NULL == $txt ) {
724 # An unkown token. Highlight.
725 $txt = "<font color=\"#FF0000\"><b>".$token["type"]."</b></font>";
726 $txt .= "<font color=\"#FFFF00\"><b>".$token["text"]."</b></font>";
727 }
728 break;
729 }
730 # If we're parsing the interior of a link, don't append the interior to $s,
731 # but push it to the stack so it can be processed when a ]] token is found.
732 if ( $tagIsOpen && $txt != "" ) {
733 $token["type"] = "text";
734 $token["text"] = $txt;
735 array_push( $tokenStack, $token );
736 } else {
737 $s .= $txt;
738 }
739 } #end while
740 if ( count( $tokenStack ) != 0 )
741 {
742 # still objects on stack. opened [[ tag without closing ]] tag.
743 $txt = "";
744 while ( $lastToken = array_pop( $tokenStack ) )
745 {
746 if ( $lastToken["type"] == "text" )
747 {
748 $txt = $lastToken["text"] . $txt;
749 } else {
750 $txt = $lastToken["type"] . $txt;
751 }
752 }
753 $s .= $txt;
754 }
755 return $s;
756 }
757
758 /* private */ function handleInternalLink( $line, $prefix )
759 {
760 global $wgLang, $wgLinkCache;
761 global $wgNamespacesWithSubpages, $wgLanguageCode;
762 static $fname = "Parser::replaceInternalLinks" ;
763 wfProfileIn( $fname );
764
765 wfProfileIn( "$fname-setup" );
766 static $tc = FALSE;
767 if ( !$tc ) { $tc = Title::legalChars() . "#"; }
768 $sk =& $this->mOptions->getSkin();
769
770 # Match a link having the form [[namespace:link|alternate]]trail
771 static $e1 = FALSE;
772 if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|([^]]+))?]](.*)\$/sD"; }
773 # Match the end of a line for a word that's not followed by whitespace,
774 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
775 #$e2 = "/^(.*)\\b(\\w+)\$/suD";
776 #$e2 = "/^(.*\\s)(\\S+)\$/suD";
777 static $e2 = '/^(.*\s)([a-zA-Z\x80-\xff]+)$/sD';
778
779
780 # Special and Media are pseudo-namespaces; no pages actually exist in them
781 static $image = FALSE;
782 static $special = FALSE;
783 static $media = FALSE;
784 static $category = FALSE;
785 if ( !$image ) { $image = Namespace::getImage(); }
786 if ( !$special ) { $special = Namespace::getSpecial(); }
787 if ( !$media ) { $media = Namespace::getMedia(); }
788 if ( !$category ) { $category = wfMsg ( "category" ) ; }
789
790 $nottalk = !Namespace::isTalk( $this->mTitle->getNamespace() );
791
792 wfProfileOut( "$fname-setup" );
793 $s = "";
794
795 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
796 $text = $m[2];
797 $trail = $m[3];
798 } else { # Invalid form; output directly
799 $s .= $prefix . "[[" . $line ;
800 return $s;
801 }
802
803 /* Valid link forms:
804 Foobar -- normal
805 :Foobar -- override special treatment of prefix (images, language links)
806 /Foobar -- convert to CurrentPage/Foobar
807 /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
808 */
809 $c = substr($m[1],0,1);
810 $noforce = ($c != ":");
811 if( $c == "/" ) { # subpage
812 if(substr($m[1],-1,1)=="/") { # / at end means we don't want the slash to be shown
813 $m[1]=substr($m[1],1,strlen($m[1])-2);
814 $noslash=$m[1];
815 } else {
816 $noslash=substr($m[1],1);
817 }
818 if($wgNamespacesWithSubpages[$this->mTitle->getNamespace()]) { # subpages allowed here
819 $link = $this->mTitle->getPrefixedText(). "/" . trim($noslash);
820 if( "" == $text ) {
821 $text= $m[1];
822 } # this might be changed for ugliness reasons
823 } else {
824 $link = $noslash; # no subpage allowed, use standard link
825 }
826 } elseif( $noforce ) { # no subpage
827 $link = $m[1];
828 } else {
829 $link = substr( $m[1], 1 );
830 }
831 if( "" == $text )
832 $text = $link;
833
834 $nt = Title::newFromText( $link );
835 if( !$nt ) {
836 $s .= $prefix . "[[" . $line;
837 return $s;
838 }
839 $ns = $nt->getNamespace();
840 $iw = $nt->getInterWiki();
841 if( $noforce ) {
842 if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgLang->getLanguageName( $iw ) ) {
843 array_push( $this->mOutput->mLanguageLinks, $nt->getPrefixedText() );
844 $s .= $prefix . $trail;
845 return $s;
846 }
847 if( $ns == $image ) {
848 $s .= $prefix . $sk->makeImageLinkObj( $nt, $text ) . $trail;
849 $wgLinkCache->addImageLinkObj( $nt );
850 return $s;
851 }
852 }
853 if( ( $nt->getPrefixedText() == $this->mTitle->getPrefixedText() ) &&
854 ( strpos( $link, "#" ) == FALSE ) ) {
855 $s .= $prefix . "<strong>" . $text . "</strong>" . $trail;
856 return $s;
857 }
858
859 # Category feature
860 $catns = strtoupper ( $nt->getDBkey () ) ;
861 $catns = explode ( ":" , $catns ) ;
862 if ( count ( $catns ) > 1 ) $catns = array_shift ( $catns ) ;
863 else $catns = "" ;
864 if ( $catns == strtoupper($category) && $this->mOptions->getUseCategoryMagic() ) {
865 $t = explode ( ":" , $nt->getText() ) ;
866 array_shift ( $t ) ;
867 $t = implode ( ":" , $t ) ;
868 $t = $wgLang->ucFirst ( $t ) ;
869 $nnt = Title::newFromText ( $category.":".$t ) ;
870 $t = $sk->makeLinkObj( $nnt, $t, "", $trail , $prefix );
871 $this->mOutput->mCategoryLinks[] = $t ;
872 $s .= $prefix . $trail ;
873 return $s ;
874 }
875 if( $ns == $media ) {
876 $s .= $prefix . $sk->makeMediaLinkObj( $nt, $text ) . $trail;
877 $wgLinkCache->addImageLinkObj( $nt );
878 return $s;
879 } elseif( $ns == $special ) {
880 $s .= $prefix . $sk->makeKnownLinkObj( $nt, $text, "", $trail );
881 return $s;
882 }
883 $s .= $sk->makeLinkObj( $nt, $text, "", $trail , $prefix );
884
885 wfProfileOut( $fname );
886 return $s;
887 }
888
889 # Some functions here used by doBlockLevels()
890 #
891 /* private */ function closeParagraph()
892 {
893 $result = "";
894 if ( 0 != strcmp( "", $this->mLastSection ) ) {
895 $result = "</" . $this->mLastSection . ">";
896 }
897 $this->mLastSection = "";
898 return $result."\n";
899 }
900 # getCommon() returns the length of the longest common substring
901 # of both arguments, starting at the beginning of both.
902 #
903 /* private */ function getCommon( $st1, $st2 )
904 {
905 $fl = strlen( $st1 );
906 $shorter = strlen( $st2 );
907 if ( $fl < $shorter ) { $shorter = $fl; }
908
909 for ( $i = 0; $i < $shorter; ++$i ) {
910 if ( $st1{$i} != $st2{$i} ) { break; }
911 }
912 return $i;
913 }
914 # These next three functions open, continue, and close the list
915 # element appropriate to the prefix character passed into them.
916 #
917 /* private */ function openList( $char )
918 {
919 $result = $this->closeParagraph();
920
921 if ( "*" == $char ) { $result .= "<ul><li>"; }
922 else if ( "#" == $char ) { $result .= "<ol><li>"; }
923 else if ( ":" == $char ) { $result .= "<dl><dd>"; }
924 else if ( ";" == $char ) {
925 $result .= "<dl><dt>";
926 $this->mDTopen = true;
927 }
928 else { $result = "<!-- ERR 1 -->"; }
929
930 return $result;
931 }
932
933 /* private */ function nextItem( $char )
934 {
935 if ( "*" == $char || "#" == $char ) { return "</li><li>"; }
936 else if ( ":" == $char || ";" == $char ) {
937 $close = "</dd>";
938 if ( $this->mDTopen ) { $close = "</dt>"; }
939 if ( ";" == $char ) {
940 $this->mDTopen = true;
941 return $close . "<dt>";
942 } else {
943 $this->mDTopen = false;
944 return $close . "<dd>";
945 }
946 }
947 return "<!-- ERR 2 -->";
948 }
949
950 /* private */function closeList( $char )
951 {
952 if ( "*" == $char ) { $text = "</li></ul>"; }
953 else if ( "#" == $char ) { $text = "</li></ol>"; }
954 else if ( ":" == $char ) {
955 if ( $this->mDTopen ) {
956 $this->mDTopen = false;
957 $text = "</dt></dl>";
958 } else {
959 $text = "</dd></dl>";
960 }
961 }
962 else { return "<!-- ERR 3 -->"; }
963 return $text."\n";
964 }
965
966 /* private */ function doBlockLevels( $text, $linestart )
967 {
968 $fname = "Parser::doBlockLevels";
969 wfProfileIn( $fname );
970 # Parsing through the text line by line. The main thing
971 # happening here is handling of block-level elements p, pre,
972 # and making lists from lines starting with * # : etc.
973 #
974 $a = explode( "\n", $text );
975 $text = $lastPref = "";
976 $this->mDTopen = $inBlockElem = false;
977
978 if ( ! $linestart ) { $text .= array_shift( $a ); }
979 foreach ( $a as $t ) {
980 if ( "" != $text ) { $text .= "\n"; }
981
982 $oLine = $t;
983 $opl = strlen( $lastPref );
984 $npl = strspn( $t, "*#:;" );
985 $pref = substr( $t, 0, $npl );
986 $pref2 = str_replace( ";", ":", $pref );
987 $t = substr( $t, $npl );
988
989 if ( 0 != $npl && 0 == strcmp( $lastPref, $pref2 ) ) {
990 $text .= $this->nextItem( substr( $pref, -1 ) );
991
992 if ( ";" == substr( $pref, -1 ) ) {
993 $cpos = strpos( $t, ":" );
994 if ( ! ( false === $cpos ) ) {
995 $term = substr( $t, 0, $cpos );
996 $text .= $term . $this->nextItem( ":" );
997 $t = substr( $t, $cpos + 1 );
998 }
999 }
1000 } else if (0 != $npl || 0 != $opl) {
1001 $cpl = $this->getCommon( $pref, $lastPref );
1002
1003 while ( $cpl < $opl ) {
1004 $text .= $this->closeList( $lastPref{$opl-1} );
1005 --$opl;
1006 }
1007 if ( $npl <= $cpl && $cpl > 0 ) {
1008 $text .= $this->nextItem( $pref{$cpl-1} );
1009 }
1010 while ( $npl > $cpl ) {
1011 $char = substr( $pref, $cpl, 1 );
1012 $text .= $this->openList( $char );
1013
1014 if ( ";" == $char ) {
1015 $cpos = strpos( $t, ":" );
1016 if ( ! ( false === $cpos ) ) {
1017 $term = substr( $t, 0, $cpos );
1018 $text .= $term . $this->nextItem( ":" );
1019 $t = substr( $t, $cpos + 1 );
1020 }
1021 }
1022 ++$cpl;
1023 }
1024 $lastPref = $pref2;
1025 }
1026 if ( 0 == $npl ) { # No prefix--go to paragraph mode
1027 if ( preg_match(
1028 "/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<p)/i", $t ) ) {
1029 $text .= $this->closeParagraph();
1030 $inBlockElem = true;
1031 }
1032 if ( ! $inBlockElem ) {
1033 if ( " " == $t{0} ) {
1034 $newSection = "pre";
1035 # $t = wfEscapeHTML( $t );
1036 }
1037 else { $newSection = "p"; }
1038
1039 if ( 0 == strcmp( "", trim( $oLine ) ) ) {
1040 $text .= $this->closeParagraph();
1041 $text .= "<" . $newSection . ">";
1042 } else if ( 0 != strcmp( $this->mLastSection,
1043 $newSection ) ) {
1044 $text .= $this->closeParagraph();
1045 if ( 0 != strcmp( "p", $newSection ) ) {
1046 $text .= "<" . $newSection . ">";
1047 }
1048 }
1049 $this->mLastSection = $newSection;
1050 }
1051 if ( $inBlockElem &&
1052 preg_match( "/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|<\\/p)/i", $t ) ) {
1053 $inBlockElem = false;
1054 }
1055 }
1056 $text .= $t;
1057 }
1058 while ( $npl ) {
1059 $text .= $this->closeList( $pref2{$npl-1} );
1060 --$npl;
1061 }
1062 if ( "" != $this->mLastSection ) {
1063 if ( "p" != $this->mLastSection ) {
1064 $text .= "</" . $this->mLastSection . ">";
1065 }
1066 $this->mLastSection = "";
1067 }
1068 wfProfileOut( $fname );
1069 return $text;
1070 }
1071
1072 function getVariableValue( $index ) {
1073 global $wgLang;
1074
1075 switch ( $index ) {
1076 case MAG_CURRENTMONTH:
1077 return date( "m" );
1078 case MAG_CURRENTMONTHNAME:
1079 return $wgLang->getMonthName( date("n") );
1080 case MAG_CURRENTMONTHNAMEGEN:
1081 return $wgLang->getMonthNameGen( date("n") );
1082 case MAG_CURRENTDAY:
1083 return date("j");
1084 case MAG_CURRENTDAYNAME:
1085 return $wgLang->getWeekdayName( date("w")+1 );
1086 case MAG_CURRENTYEAR:
1087 return date( "Y" );
1088 case MAG_CURRENTTIME:
1089 return $wgLang->time( wfTimestampNow(), false );
1090 case MAG_NUMBEROFARTICLES:
1091 return wfNumberOfArticles();
1092 default:
1093 return NULL;
1094 }
1095 }
1096
1097 function initialiseVariables()
1098 {
1099 global $wgVariableIDs;
1100 $this->mVariables = array();
1101
1102 foreach ( $wgVariableIDs as $id ) {
1103 $mw =& MagicWord::get( $id );
1104 $mw->addToArray( $this->mVariables, $this->getVariableValue( $id ) );
1105 }
1106 }
1107
1108 /* private */ function replaceVariables( $text )
1109 {
1110 global $wgLang, $wgCurParser;
1111 global $wgScript, $wgArticlePath;
1112
1113 $fname = "Parser::replaceVariables";
1114 wfProfileIn( $fname );
1115
1116 $bail = false;
1117 if ( !$this->mVariables ) {
1118 $this->initialiseVariables();
1119 }
1120 $titleChars = Title::legalChars();
1121 $regex = "/{{([$titleChars]*?)}}/s";
1122
1123 # "Recursive" variable expansion: run it through a couple of passes
1124 for ( $i=0; $i<MAX_INCLUDE_REPEAT && !$bail; $i++ ) {
1125 $oldText = $text;
1126
1127 # It's impossible to rebind a global in PHP
1128 # Instead, we run the substitution on a copy, then merge the changed fields back in
1129 $wgCurParser = $this->fork();
1130
1131 $text = preg_replace_callback( $regex, "wfBraceSubstitution", $text );
1132 if ( $oldText == $text ) {
1133 $bail = true;
1134 }
1135 $this->merge( $wgCurParser );
1136 }
1137
1138 return $text;
1139 }
1140
1141 # Returns a copy of this object except with various variables cleared
1142 # This copy can be re-merged with the parent after operations on the copy
1143 function fork()
1144 {
1145 $copy = $this;
1146 $copy->mOutput = new ParserOutput;
1147 return $copy;
1148 }
1149
1150 # Merges a copy split off with fork()
1151 function merge( &$copy )
1152 {
1153 $this->mOutput->merge( $copy->mOutput );
1154
1155 # Merge include throttling arrays
1156 foreach( $copy->mIncludeCount as $dbk => $count ) {
1157 if ( array_key_exists( $dbk, $this->mIncludeCount ) ) {
1158 $this->mIncludeCount[$dbk] += $count;
1159 } else {
1160 $this->mIncludeCount[$dbk] = $count;
1161 }
1162 }
1163 }
1164
1165 function braceSubstitution( $matches )
1166 {
1167 global $wgLinkCache;
1168 $fname = "Parser::braceSubstitution";
1169 $found = false;
1170 $nowiki = false;
1171
1172 $text = $matches[1];
1173
1174 # SUBST
1175 $mwSubst =& MagicWord::get( MAG_SUBST );
1176 if ( $mwSubst->matchStartAndRemove( $text ) ) {
1177 if ( $this->mOutputType == OT_HTML ) {
1178 # Invalid SUBST not replaced at PST time
1179 # Return without further processing
1180 $text = $matches[0];
1181 $found = true;
1182 }
1183 } elseif ( $this->mOutputType == OT_WIKI ) {
1184 # SUBST not found in PST pass, do nothing
1185 $text = $matches[0];
1186 $found = true;
1187 }
1188
1189 # Various prefixes
1190 if ( !$found ) {
1191 # Check for MSGNW:
1192 $mwMsgnw =& MagicWord::get( MAG_MSGNW );
1193 if ( $mwMsgnw->matchStartAndRemove( $text ) ) {
1194 $nowiki = true;
1195 } else {
1196 # Remove obsolete MSG:
1197 $mwMsg =& MagicWord::get( MAG_MSG );
1198 $mwMsg->matchStartAndRemove( $text );
1199 }
1200
1201 # Check if it is an internal message
1202 $mwInt =& MagicWord::get( MAG_INT );
1203 if ( $mwInt->matchStartAndRemove( $text ) ) {
1204 $text = wfMsg( $text );
1205 $found = true;
1206 }
1207 }
1208
1209 # Check for a match against internal variables
1210 if ( !$found && array_key_exists( $text, $this->mVariables ) ) {
1211 $text = $this->mVariables[$text];
1212 $found = true;
1213 $this->mOutput->mContainsOldMagic = true;
1214 }
1215
1216 # Load from database
1217 if ( !$found ) {
1218 $title = Title::newFromText( $text, NS_TEMPLATE );
1219 if ( !is_null( $text ) && !$title->isExternal() ) {
1220 # Check for excessive inclusion
1221 $dbk = $title->getPrefixedDBkey();
1222 if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) {
1223 $this->mIncludeCount[$dbk] = 0;
1224 }
1225 if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) {
1226 $row = wfGetArray( "cur", array("cur_text"), array(
1227 "cur_namespace" => $title->getNamespace(),
1228 "cur_title" => $title->getDBkey() ), $fname );
1229 if ( $row !== false ) {
1230 $found = true;
1231 $text = $row->cur_text;
1232
1233 # Escaping and link table handling
1234 # Not required for preSaveTransform()
1235 if ( $this->mOutputType == OT_HTML ) {
1236 if ( $nowiki ) {
1237 $text = wfEscapeWikiText( $text );
1238 } else {
1239 $text = $this->removeHTMLtags( $text );
1240 }
1241 $wgLinkCache->suspend();
1242 $text = $this->replaceInternalLinks( $text );
1243 $wgLinkCache->resume();
1244 $wgLinkCache->addLinkObj( $title );
1245
1246 }
1247 }
1248 }
1249
1250 # If the title is valid but undisplayable, make a link to it
1251 if ( $this->mOutputType == OT_HTML && !$found ) {
1252 $text = "[[" . $title->getPrefixedText() . "]]";
1253 $found = true;
1254 }
1255 }
1256 }
1257
1258 if ( !$found ) {
1259 return $matches[0];
1260 } else {
1261 return $text;
1262 }
1263 }
1264
1265 # Cleans up HTML, removes dangerous tags and attributes
1266 /* private */ function removeHTMLtags( $text )
1267 {
1268 $fname = "Parser::removeHTMLtags";
1269 wfProfileIn( $fname );
1270 $htmlpairs = array( # Tags that must be closed
1271 "b", "i", "u", "font", "big", "small", "sub", "sup", "h1",
1272 "h2", "h3", "h4", "h5", "h6", "cite", "code", "em", "s",
1273 "strike", "strong", "tt", "var", "div", "center",
1274 "blockquote", "ol", "ul", "dl", "table", "caption", "pre",
1275 "ruby", "rt" , "rb" , "rp", "p"
1276 );
1277 $htmlsingle = array(
1278 "br", "hr", "li", "dt", "dd", "hr/"
1279 );
1280 $htmlnest = array( # Tags that can be nested--??
1281 "table", "tr", "td", "th", "div", "blockquote", "ol", "ul",
1282 "dl", "font", "big", "small", "sub", "sup"
1283 );
1284 $tabletags = array( # Can only appear inside table
1285 "td", "th", "tr"
1286 );
1287
1288 $htmlsingle = array_merge( $tabletags, $htmlsingle );
1289 $htmlelements = array_merge( $htmlsingle, $htmlpairs );
1290
1291 $htmlattrs = $this->getHTMLattrs () ;
1292
1293 # Remove HTML comments
1294 $text = preg_replace( "/<!--.*-->/sU", "", $text );
1295
1296 $bits = explode( "<", $text );
1297 $text = array_shift( $bits );
1298 $tagstack = array(); $tablestack = array();
1299
1300 foreach ( $bits as $x ) {
1301 $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) );
1302 preg_match( "/^(\\/?)(\\w+)([^>]*)(\\/{0,1}>)([^<]*)$/",
1303 $x, $regs );
1304 list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
1305 error_reporting( $prev );
1306
1307 $badtag = 0 ;
1308 if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
1309 # Check our stack
1310 if ( $slash ) {
1311 # Closing a tag...
1312 if ( ! in_array( $t, $htmlsingle ) &&
1313 ( $ot = array_pop( $tagstack ) ) != $t ) {
1314 array_push( $tagstack, $ot );
1315 $badtag = 1;
1316 } else {
1317 if ( $t == "table" ) {
1318 $tagstack = array_pop( $tablestack );
1319 }
1320 $newparams = "";
1321 }
1322 } else {
1323 # Keep track for later
1324 if ( in_array( $t, $tabletags ) &&
1325 ! in_array( "table", $tagstack ) ) {
1326 $badtag = 1;
1327 } else if ( in_array( $t, $tagstack ) &&
1328 ! in_array ( $t , $htmlnest ) ) {
1329 $badtag = 1 ;
1330 } else if ( ! in_array( $t, $htmlsingle ) ) {
1331 if ( $t == "table" ) {
1332 array_push( $tablestack, $tagstack );
1333 $tagstack = array();
1334 }
1335 array_push( $tagstack, $t );
1336 }
1337 # Strip non-approved attributes from the tag
1338 $newparams = $this->fixTagAttributes($params);
1339
1340 }
1341 if ( ! $badtag ) {
1342 $rest = str_replace( ">", "&gt;", $rest );
1343 $text .= "<$slash$t $newparams$brace$rest";
1344 continue;
1345 }
1346 }
1347 $text .= "&lt;" . str_replace( ">", "&gt;", $x);
1348 }
1349 # Close off any remaining tags
1350 while ( $t = array_pop( $tagstack ) ) {
1351 $text .= "</$t>\n";
1352 if ( $t == "table" ) { $tagstack = array_pop( $tablestack ); }
1353 }
1354 wfProfileOut( $fname );
1355 return $text;
1356 }
1357
1358 /*
1359 *
1360 * This function accomplishes several tasks:
1361 * 1) Auto-number headings if that option is enabled
1362 * 2) Add an [edit] link to sections for logged in users who have enabled the option
1363 * 3) Add a Table of contents on the top for users who have enabled the option
1364 * 4) Auto-anchor headings
1365 *
1366 * It loops through all headlines, collects the necessary data, then splits up the
1367 * string and re-inserts the newly formatted headlines.
1368 *
1369 */
1370
1371 /* private */ function formatHeadings( $text )
1372 {
1373 $doNumberHeadings = $this->mOptions->getNumberHeadings();
1374 $doShowToc = $this->mOptions->getShowToc();
1375 if( !$this->mTitle->userCanEdit() ) {
1376 $showEditLink = 0;
1377 $rightClickHack = 0;
1378 } else {
1379 $showEditLink = $this->mOptions->getEditSection();
1380 $rightClickHack = $this->mOptions->getEditSectionOnRightClick();
1381 }
1382
1383 # Inhibit editsection links if requested in the page
1384 $esw =& MagicWord::get( MAG_NOEDITSECTION );
1385 if( $esw->matchAndRemove( $text ) ) {
1386 $showEditLink = 0;
1387 }
1388 # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
1389 # do not add TOC
1390 $mw =& MagicWord::get( MAG_NOTOC );
1391 if( $mw->matchAndRemove( $text ) ) {
1392 $doShowToc = 0;
1393 }
1394
1395 # never add the TOC to the Main Page. This is an entry page that should not
1396 # be more than 1-2 screens large anyway
1397 if( $this->mTitle->getPrefixedText() == wfMsg("mainpage") ) {
1398 $doShowToc = 0;
1399 }
1400
1401 # We need this to perform operations on the HTML
1402 $sk =& $this->mOptions->getSkin();
1403
1404 # Get all headlines for numbering them and adding funky stuff like [edit]
1405 # links
1406 preg_match_all( "/<H([1-6])(.*?" . ">)(.*?)<\/H[1-6]>/i", $text, $matches );
1407
1408 # headline counter
1409 $headlineCount = 0;
1410
1411 # Ugh .. the TOC should have neat indentation levels which can be
1412 # passed to the skin functions. These are determined here
1413 $toclevel = 0;
1414 $toc = "";
1415 $full = "";
1416 $head = array();
1417 $sublevelCount = array();
1418 foreach( $matches[3] as $headline ) {
1419 if( $level ) {
1420 $prevlevel = $level;
1421 }
1422 $level = $matches[1][$headlineCount];
1423 if( ( $doNumberHeadings || $doShowToc ) && $prevlevel && $level > $prevlevel ) {
1424 # reset when we enter a new level
1425 $sublevelCount[$level] = 0;
1426 $toc .= $sk->tocIndent( $level - $prevlevel );
1427 $toclevel += $level - $prevlevel;
1428 }
1429 if( ( $doNumberHeadings || $doShowToc ) && $level < $prevlevel ) {
1430 # reset when we step back a level
1431 $sublevelCount[$level+1]=0;
1432 $toc .= $sk->tocUnindent( $prevlevel - $level );
1433 $toclevel -= $prevlevel - $level;
1434 }
1435 # count number of headlines for each level
1436 $sublevelCount[$level]++;
1437
1438 if( $doNumberHeadings || $doShowToc ) {
1439 for( $i = 1; $i <= $level; $i++ ) {
1440 if( $sublevelCount[$i] ) {
1441 if( $dot ) {
1442 $numbering .= ".";
1443 }
1444 $numbering .= $sublevelCount[$i];
1445 $dot = 1;
1446 }
1447 }
1448 }
1449
1450 # The canonized header is a version of the header text safe to use for links
1451 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
1452 $canonized_headline = Parser::unstrip( $headline, $this->mStripState );
1453
1454 # strip out HTML
1455 $canonized_headline = preg_replace( "/<.*?" . ">/","",$canonized_headline );
1456
1457 $tocline = trim( $canonized_headline );
1458 $canonized_headline = str_replace( '"', "", $canonized_headline );
1459 $canonized_headline = str_replace( " ", "_", trim( $canonized_headline) );
1460 $refer[$headlineCount] = $canonized_headline;
1461
1462 # count how many in assoc. array so we can track dupes in anchors
1463 $refers[$canonized_headline]++;
1464 $refcount[$headlineCount]=$refers[$canonized_headline];
1465
1466 # Prepend the number to the heading text
1467
1468 if( $doNumberHeadings || $doShowToc ) {
1469 $tocline = $numbering . " " . $tocline;
1470
1471 # Don't number the heading if it is the only one (looks silly)
1472 if( $doNumberHeadings && count( $matches[3] ) > 1) {
1473 # the two are different if the line contains a link
1474 $headline=$numbering . " " . $headline;
1475 }
1476 }
1477
1478 # Create the anchor for linking from the TOC to the section
1479 $anchor = $canonized_headline;
1480 if($refcount[$headlineCount] > 1 ) {
1481 $anchor .= "_" . $refcount[$headlineCount];
1482 }
1483 if( $doShowToc ) {
1484 $toc .= $sk->tocLine($anchor,$tocline,$toclevel);
1485 }
1486 if( $showEditLink ) {
1487 $head[$headlineCount] .= $sk->editSectionLink($headlineCount+1);
1488 }
1489
1490
1491 # the headline might have a link
1492 if( preg_match( "/(.*)<a(.*)/", $headline, $headlinematches ) ) {
1493 # if so give an anchor name to the already existent link
1494 $headline = $headlinematches[1]
1495 . "<a name=\"$anchor\" " . $headlinematches[2];
1496 } else {
1497 # else create an anchor link for the headline
1498 $headline = "<a name=\"$anchor.\">$headline</a>";
1499 }
1500
1501 # give headline the correct <h#> tag
1502 $head[$headlineCount] .= "<h".$level.$matches[2][$headlineCount] .$headline."</h".$level.">";
1503
1504 # Add the edit section link
1505 if( $rightClickHack ) {
1506 $head[$headlineCount] = $sk->editSectionScript($headlineCount+1,$head[$headlineCount]);
1507 }
1508
1509 $numbering = "";
1510 $headlineCount++;
1511 $dot = 0;
1512 }
1513
1514 if( $doShowToc ) {
1515 $toclines = $headlineCount;
1516 $toc .= $sk->tocUnindent( $toclevel );
1517 $toc = $sk->tocTable( $toc );
1518 }
1519
1520 # split up and insert constructed headlines
1521
1522 $blocks = preg_split( "/<H[1-6].*?" . ">.*?<\/H[1-6]>/i", $text );
1523 $i = 0;
1524
1525 foreach( $blocks as $block ) {
1526 if( $showEditLink && $headlineCount > 0 && $i == 0 ) {
1527 # This is the [edit] link that appears for the top block of text when
1528 # section editing is enabled
1529 $full .= $sk->editSectionLink(0);
1530 }
1531 $full .= $block;
1532 if( $doShowToc && $toclines>3 && !$i) {
1533 # Let's add a top anchor just in case we want to link to the top of the page
1534 $full = "<a name=\"top\"></a>".$full.$toc;
1535 }
1536
1537 if( !empty( $head[$i] ) ) {
1538 $full .= $head[$i];
1539 }
1540 $i++;
1541 }
1542
1543 return $full;
1544 }
1545
1546 /* private */ function doMagicISBN( &$tokenizer )
1547 {
1548 global $wgLang;
1549
1550 # Check whether next token is a text token
1551 # If yes, fetch it and convert the text into a
1552 # Special::BookSources link
1553 $token = $tokenizer->previewToken();
1554 while ( $token["type"] == "" )
1555 {
1556 $tokenizer->nextToken();
1557 $token = $tokenizer->previewToken();
1558 }
1559 if ( $token["type"] == "text" )
1560 {
1561 $token = $tokenizer->nextToken();
1562 $x = $token["text"];
1563 $valid = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1564
1565 $isbn = $blank = "" ;
1566 while ( " " == $x{0} ) {
1567 $blank .= " ";
1568 $x = substr( $x, 1 );
1569 }
1570 while ( strstr( $valid, $x{0} ) != false ) {
1571 $isbn .= $x{0};
1572 $x = substr( $x, 1 );
1573 }
1574 $num = str_replace( "-", "", $isbn );
1575 $num = str_replace( " ", "", $num );
1576
1577 if ( "" == $num ) {
1578 $text = "ISBN $blank$x";
1579 } else {
1580 $titleObj = Title::makeTitle( NS_SPECIAL, "Booksources" );
1581 $text = "<a href=\"" .
1582 $titleObj->escapeLocalUrl( "isbn={$num}" ) .
1583 "\" class=\"internal\">ISBN $isbn</a>";
1584 $text .= $x;
1585 }
1586 } else {
1587 $text = "ISBN ";
1588 }
1589 return $text;
1590 }
1591 /* private */ function doMagicRFC( &$tokenizer )
1592 {
1593 global $wgLang;
1594
1595 # Check whether next token is a text token
1596 # If yes, fetch it and convert the text into a
1597 # link to an RFC source
1598 $token = $tokenizer->previewToken();
1599 while ( $token["type"] == "" )
1600 {
1601 $tokenizer->nextToken();
1602 $token = $tokenizer->previewToken();
1603 }
1604 if ( $token["type"] == "text" )
1605 {
1606 $token = $tokenizer->nextToken();
1607 $x = $token["text"];
1608 $valid = "0123456789";
1609
1610 $rfc = $blank = "" ;
1611 while ( " " == $x{0} ) {
1612 $blank .= " ";
1613 $x = substr( $x, 1 );
1614 }
1615 while ( strstr( $valid, $x{0} ) != false ) {
1616 $rfc .= $x{0};
1617 $x = substr( $x, 1 );
1618 }
1619
1620 if ( "" == $rfc ) {
1621 $text .= "RFC $blank$x";
1622 } else {
1623 $url = wfmsg( "rfcurl" );
1624 $url = str_replace( "$1", $rfc, $url);
1625 $sk =& $this->mOptions->getSkin();
1626 $la = $sk->getExternalLinkAttributes( $url, "RFC {$rfc}" );
1627 $text = "<a href='{$url}'{$la}>RFC {$rfc}</a>{$x}";
1628 }
1629 } else {
1630 $text = "RFC ";
1631 }
1632 return $text;
1633 }
1634
1635 function preSaveTransform( $text, &$title, &$user, $options, $clearState = true )
1636 {
1637 $this->mOptions = $options;
1638 $this->mTitle = $title;
1639 $this->mOutputType = OT_WIKI;
1640
1641 if ( $clearState ) {
1642 $this->clearState();
1643 }
1644
1645 $stripState = false;
1646 $text = str_replace("\r\n", "\n", $text);
1647 $text = $this->strip( $text, $stripState, false );
1648 $text = $this->pstPass2( $text, $user );
1649 $text = $this->unstrip( $text, $stripState );
1650 return $text;
1651 }
1652
1653 /* private */ function pstPass2( $text, &$user )
1654 {
1655 global $wgLang, $wgLocaltimezone, $wgCurParser;
1656
1657 # Variable replacement
1658 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
1659 $text = $this->replaceVariables( $text );
1660
1661 # Signatures
1662 #
1663 $n = $user->getName();
1664 $k = $user->getOption( "nickname" );
1665 if ( "" == $k ) { $k = $n; }
1666 if(isset($wgLocaltimezone)) {
1667 $oldtz = getenv("TZ"); putenv("TZ=$wgLocaltimezone");
1668 }
1669 /* Note: this is an ugly timezone hack for the European wikis */
1670 $d = $wgLang->timeanddate( date( "YmdHis" ), false ) .
1671 " (" . date( "T" ) . ")";
1672 if(isset($wgLocaltimezone)) putenv("TZ=$oldtz");
1673
1674 $text = preg_replace( "/~~~~~/", $d, $text );
1675 $text = preg_replace( "/~~~~/", "[[" . $wgLang->getNsText(
1676 Namespace::getUser() ) . ":$n|$k]] $d", $text );
1677 $text = preg_replace( "/~~~/", "[[" . $wgLang->getNsText(
1678 Namespace::getUser() ) . ":$n|$k]]", $text );
1679
1680 # Context links: [[|name]] and [[name (context)|]]
1681 #
1682 $tc = "[&;%\\-,.\\(\\)' _0-9A-Za-z\\/:\\x80-\\xff]";
1683 $np = "[&;%\\-,.' _0-9A-Za-z\\/:\\x80-\\xff]"; # No parens
1684 $namespacechar = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
1685 $conpat = "/^({$np}+) \\(({$tc}+)\\)$/";
1686
1687 $p1 = "/\[\[({$np}+) \\(({$np}+)\\)\\|]]/"; # [[page (context)|]]
1688 $p2 = "/\[\[\\|({$tc}+)]]/"; # [[|page]]
1689 $p3 = "/\[\[($namespacechar+):({$np}+)\\|]]/"; # [[namespace:page|]]
1690 $p4 = "/\[\[($namespacechar+):({$np}+) \\(({$np}+)\\)\\|]]/";
1691 # [[ns:page (cont)|]]
1692 $context = "";
1693 $t = $this->mTitle->getText();
1694 if ( preg_match( $conpat, $t, $m ) ) {
1695 $context = $m[2];
1696 }
1697 $text = preg_replace( $p4, "[[\\1:\\2 (\\3)|\\2]]", $text );
1698 $text = preg_replace( $p1, "[[\\1 (\\2)|\\1]]", $text );
1699 $text = preg_replace( $p3, "[[\\1:\\2|\\2]]", $text );
1700
1701 if ( "" == $context ) {
1702 $text = preg_replace( $p2, "[[\\1]]", $text );
1703 } else {
1704 $text = preg_replace( $p2, "[[\\1 ({$context})|\\1]]", $text );
1705 }
1706
1707 /*
1708 $mw =& MagicWord::get( MAG_SUBST );
1709 $wgCurParser = $this->fork();
1710 $text = $mw->substituteCallback( $text, "wfBraceSubstitution" );
1711 $this->merge( $wgCurParser );
1712 */
1713
1714 # Trim trailing whitespace
1715 # MAG_END (__END__) tag allows for trailing
1716 # whitespace to be deliberately included
1717 $text = rtrim( $text );
1718 $mw =& MagicWord::get( MAG_END );
1719 $mw->matchAndRemove( $text );
1720
1721 return $text;
1722 }
1723
1724
1725 }
1726
1727 class ParserOutput
1728 {
1729 var $mText, $mLanguageLinks, $mCategoryLinks, $mContainsOldMagic;
1730
1731 function ParserOutput( $text = "", $languageLinks = array(), $categoryLinks = array(),
1732 $containsOldMagic = false )
1733 {
1734 $this->mText = $text;
1735 $this->mLanguageLinks = $languageLinks;
1736 $this->mCategoryLinks = $categoryLinks;
1737 $this->mContainsOldMagic = $containsOldMagic;
1738 }
1739
1740 function getText() { return $this->mText; }
1741 function getLanguageLinks() { return $this->mLanguageLinks; }
1742 function getCategoryLinks() { return $this->mCategoryLinks; }
1743 function containsOldMagic() { return $this->mContainsOldMagic; }
1744 function setText( $text ) { return wfSetVar( $this->mText, $text ); }
1745 function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); }
1746 function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategoryLinks, $cl ); }
1747 function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
1748
1749 function merge( $other ) {
1750 $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $other->mLanguageLinks );
1751 $this->mCategoryLinks = array_merge( $this->mCategoryLinks, $this->mLanguageLinks );
1752 $this->mContainsOldMagic = $this->mContainsOldMagic || $other->mContainsOldMagic;
1753 }
1754
1755 }
1756
1757 class ParserOptions
1758 {
1759 # All variables are private
1760 var $mUseTeX; # Use texvc to expand <math> tags
1761 var $mUseCategoryMagic; # Treat [[Category:xxxx]] tags specially
1762 var $mUseDynamicDates; # Use $wgDateFormatter to format dates
1763 var $mInterwikiMagic; # Interlanguage links are removed and returned in an array
1764 var $mAllowExternalImages; # Allow external images inline
1765 var $mSkin; # Reference to the preferred skin
1766 var $mDateFormat; # Date format index
1767 var $mEditSection; # Create "edit section" links
1768 var $mEditSectionOnRightClick; # Generate JavaScript to edit section on right click
1769 var $mNumberHeadings; # Automatically number headings
1770 var $mShowToc; # Show table of contents
1771
1772 function getUseTeX() { return $this->mUseTeX; }
1773 function getUseCategoryMagic() { return $this->mUseCategoryMagic; }
1774 function getUseDynamicDates() { return $this->mUseDynamicDates; }
1775 function getInterwikiMagic() { return $this->mInterwikiMagic; }
1776 function getAllowExternalImages() { return $this->mAllowExternalImages; }
1777 function getSkin() { return $this->mSkin; }
1778 function getDateFormat() { return $this->mDateFormat; }
1779 function getEditSection() { return $this->mEditSection; }
1780 function getEditSectionOnRightClick() { return $this->mEditSectionOnRightClick; }
1781 function getNumberHeadings() { return $this->mNumberHeadings; }
1782 function getShowToc() { return $this->mShowToc; }
1783
1784 function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); }
1785 function setUseCategoryMagic( $x ) { return wfSetVar( $this->mUseCategoryMagic, $x ); }
1786 function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); }
1787 function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); }
1788 function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); }
1789 function setSkin( $x ) { return wfSetRef( $this->mSkin, $x ); }
1790 function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); }
1791 function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); }
1792 function setEditSectionOnRightClick( $x ) { return wfSetVar( $this->mEditSectionOnRightClick, $x ); }
1793 function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); }
1794 function setShowToc( $x ) { return wfSetVar( $this->mShowToc, $x ); }
1795
1796 /* static */ function newFromUser( &$user )
1797 {
1798 $popts = new ParserOptions;
1799 $popts->initialiseFromUser( &$user );
1800 return $popts;
1801 }
1802
1803 function initialiseFromUser( &$userInput )
1804 {
1805 global $wgUseTeX, $wgUseCategoryMagic, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
1806
1807 if ( !$userInput ) {
1808 $user = new User;
1809 } else {
1810 $user =& $userInput;
1811 }
1812
1813 $this->mUseTeX = $wgUseTeX;
1814 $this->mUseCategoryMagic = $wgUseCategoryMagic;
1815 $this->mUseDynamicDates = $wgUseDynamicDates;
1816 $this->mInterwikiMagic = $wgInterwikiMagic;
1817 $this->mAllowExternalImages = $wgAllowExternalImages;
1818 $this->mSkin =& $user->getSkin();
1819 $this->mDateFormat = $user->getOption( "date" );
1820 $this->mEditSection = $user->getOption( "editsection" );
1821 $this->mEditSectionOnRightClick = $user->getOption( "editsectiononrightclick" );
1822 $this->mNumberHeadings = $user->getOption( "numberheadings" );
1823 $this->mShowToc = $user->getOption( "showtoc" );
1824 }
1825
1826
1827 }
1828
1829 # Regex callbacks, used in Parser::replaceVariables
1830 function wfBraceSubstitution( $matches )
1831 {
1832 global $wgCurParser;
1833 return $wgCurParser->braceSubstitution( $matches );
1834 }
1835
1836 ?>