OSDN Git Service

BugTrack2/230 Do nothing on updating page with unchanged content
[pukiwiki/pukiwiki.git] / lib / convert_html.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: convert_html.php,v 1.21 2011/01/25 15:01:01 henoheno Exp $
4 // Copyright (C)
5 //   2002-2005 PukiWiki Developers Team
6 //   2001-2002 Originally written by yu-ji
7 // License: GPL v2 or (at your option) any later version
8 //
9 // function 'convert_html()', wiki text parser
10 // and related classes-and-functions
11
12 function convert_html($lines)
13 {
14         global $vars, $digest;
15         static $contents_id = 0;
16
17         // Set digest
18         $digest = md5(join('', get_source($vars['page'])));
19
20         if (! is_array($lines)) $lines = explode("\n", $lines);
21
22         $body = new Body(++$contents_id);
23         $body->parse($lines);
24
25         return $body->toString();
26 }
27
28 // Block elements
29 class Element
30 {
31         var $parent;
32         var $elements; // References of childs
33         var $last;     // Insert new one at the back of the $last
34
35         function Element()
36         {
37                 $this->elements = array();
38                 $this->last     = & $this;
39         }
40
41         function setParent(& $parent)
42         {
43                 $this->parent = & $parent;
44         }
45
46         function & add(& $obj)
47         {
48                 if ($this->canContain($obj)) {
49                         return $this->insert($obj);
50                 } else {
51                         return $this->parent->add($obj);
52                 }
53         }
54
55         function & insert(& $obj)
56         {
57                 $obj->setParent($this);
58                 $this->elements[] = & $obj;
59
60                 return $this->last = & $obj->last;
61         }
62
63         function canContain($obj)
64         {
65                 return TRUE;
66         }
67
68         function wrap($string, $tag, $param = '', $canomit = TRUE)
69         {
70                 return ($canomit && $string == '') ? '' :
71                         '<' . $tag . $param . '>' . $string . '</' . $tag . '>';
72         }
73
74         function toString()
75         {
76                 $ret = array();
77                 foreach (array_keys($this->elements) as $key)
78                         $ret[] = $this->elements[$key]->toString();
79                 return join("\n", $ret);
80         }
81
82         function dump($indent = 0)
83         {
84                 $ret = str_repeat(' ', $indent) . get_class($this) . "\n";
85                 $indent += 2;
86                 foreach (array_keys($this->elements) as $key) {
87                         $ret .= is_object($this->elements[$key]) ?
88                                 $this->elements[$key]->dump($indent) : '';
89                                 //str_repeat(' ', $indent) . $this->elements[$key];
90                 }
91                 return $ret;
92         }
93 }
94
95 // Returns inline-related object
96 function & Factory_Inline($text)
97 {
98         // Check the first letter of the line
99         if (substr($text, 0, 1) == '~') {
100                 return new Paragraph(' ' . substr($text, 1));
101         } else {
102                 return new Inline($text);
103         }
104 }
105
106 function & Factory_DList(& $root, $text)
107 {
108         $out = explode('|', ltrim($text), 2);
109         if (count($out) < 2) {
110                 return Factory_Inline($text);
111         } else {
112                 return new DList($out);
113         }
114 }
115
116 // '|'-separated table
117 function & Factory_Table(& $root, $text)
118 {
119         if (! preg_match('/^\|(.+)\|([hHfFcC]?)$/', $text, $out)) {
120                 return Factory_Inline($text);
121         } else {
122                 return new Table($out);
123         }
124 }
125
126 // Comma-separated table
127 function & Factory_YTable(& $root, $text)
128 {
129         if ($text == ',') {
130                 return Factory_Inline($text);
131         } else {
132                 return new YTable(csv_explode(',', substr($text, 1)));
133         }
134 }
135
136 function & Factory_Div(& $root, $text)
137 {
138         $matches = array();
139
140         // Seems block plugin?
141         if (PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK) {
142                 // Usual code
143                 if (preg_match('/^\#([^\(]+)(?:\((.*)\))?/', $text, $matches) &&
144                     exist_plugin_convert($matches[1])) {
145                         return new Div($matches);
146                 }
147         } else {
148                 // Hack code
149                 if(preg_match('/^#([^\(\{]+)(?:\(([^\r]*)\))?(\{*)/', $text, $matches) &&
150                    exist_plugin_convert($matches[1])) {
151                         $len  = strlen($matches[3]);
152                         $body = array();
153                         if ($len == 0) {
154                                 return new Div($matches); // Seems legacy block plugin
155                         } else if (preg_match('/\{{' . $len . '}\s*\r(.*)\r\}{' . $len . '}/', $text, $body)) { 
156                                 $matches[2] .= "\r" . $body[1] . "\r";
157                                 return new Div($matches); // Seems multiline-enabled block plugin
158                         }
159                 }
160         }
161
162         return new Paragraph($text);
163 }
164
165 // Inline elements
166 class Inline extends Element
167 {
168         function Inline($text)
169         {
170                 parent::Element();
171                 $this->elements[] = trim((substr($text, 0, 1) == "\n") ?
172                         $text : make_link($text));
173         }
174
175         function & insert(& $obj)
176         {
177                 $this->elements[] = $obj->elements[0];
178                 return $this;
179         }
180
181         function canContain($obj)
182         {
183                 return is_a($obj, 'Inline');
184         }
185
186         function toString()
187         {
188                 global $line_break;
189                 return join(($line_break ? '<br />' . "\n" : "\n"), $this->elements);
190         }
191
192         function & toPara($class = '')
193         {
194                 $obj = new Paragraph('', $class);
195                 $obj->insert($this);
196                 return $obj;
197         }
198 }
199
200 // Paragraph: blank-line-separated sentences
201 class Paragraph extends Element
202 {
203         var $param;
204
205         function Paragraph($text, $param = '')
206         {
207                 parent::Element();
208                 $this->param = $param;
209                 if ($text == '') return;
210
211                 if (substr($text, 0, 1) == '~')
212                         $text = ' ' . substr($text, 1);
213
214                 $this->insert(Factory_Inline($text));
215         }
216
217         function canContain($obj)
218         {
219                 return is_a($obj, 'Inline');
220         }
221
222         function toString()
223         {
224                 return $this->wrap(parent::toString(), 'p', $this->param);
225         }
226 }
227
228 // * Heading1
229 // ** Heading2
230 // *** Heading3
231 class Heading extends Element
232 {
233         var $level;
234         var $id;
235         var $msg_top;
236
237         function Heading(& $root, $text)
238         {
239                 parent::Element();
240
241                 $this->level = min(3, strspn($text, '*'));
242                 list($text, $this->msg_top, $this->id) = $root->getAnchor($text, $this->level);
243                 $this->insert(Factory_Inline($text));
244                 $this->level++; // h2,h3,h4
245         }
246
247         function & insert(& $obj)
248         {
249                 parent::insert($obj);
250                 return $this->last = & $this;
251         }
252
253         function canContain(& $obj)
254         {
255                 return FALSE;
256         }
257
258         function toString()
259         {
260                 return $this->msg_top .  $this->wrap(parent::toString(),
261                         'h' . $this->level, ' id="' . $this->id . '"');
262         }
263 }
264
265 // ----
266 // Horizontal Rule
267 class HRule extends Element
268 {
269         function HRule(& $root, $text)
270         {
271                 parent::Element();
272         }
273
274         function canContain(& $obj)
275         {
276                 return FALSE;
277         }
278
279         function toString()
280         {
281                 global $hr;
282                 return $hr;
283         }
284 }
285
286 // Lists (UL, OL, DL)
287 class ListContainer extends Element
288 {
289         var $tag;
290         var $tag2;
291         var $level;
292         var $style;
293         var $margin;
294         var $left_margin;
295
296         function ListContainer($tag, $tag2, $head, $text)
297         {
298                 parent::Element();
299
300                 $var_margin      = '_' . $tag . '_margin';
301                 $var_left_margin = '_' . $tag . '_left_margin';
302                 global $$var_margin, $$var_left_margin;
303
304                 $this->margin      = $$var_margin;
305                 $this->left_margin = $$var_left_margin;
306
307                 $this->tag   = $tag;
308                 $this->tag2  = $tag2;
309                 $this->level = min(3, strspn($text, $head));
310                 $text = ltrim(substr($text, $this->level));
311
312                 parent::insert(new ListElement($this->level, $tag2));
313                 if ($text != '')
314                         $this->last = & $this->last->insert(Factory_Inline($text));
315         }
316
317         function canContain(& $obj)
318         {
319                 return (! is_a($obj, 'ListContainer')
320                         || ($this->tag == $obj->tag && $this->level == $obj->level));
321         }
322
323         function setParent(& $parent)
324         {
325                 global $_list_pad_str;
326
327                 parent::setParent($parent);
328
329                 $step = $this->level;
330                 if (isset($parent->parent) && is_a($parent->parent, 'ListContainer'))
331                         $step -= $parent->parent->level;
332
333                 $margin = $this->margin * $step;
334                 if ($step == $this->level)
335                         $margin += $this->left_margin;
336
337                 $this->style = sprintf($_list_pad_str, $this->level, $margin, $margin);
338         }
339
340         function & insert(& $obj)
341         {
342                 if (! is_a($obj, get_class($this)))
343                         return $this->last = & $this->last->insert($obj);
344
345                 // Break if no elements found (BugTrack/524)
346                 if (count($obj->elements) == 1 && empty($obj->elements[0]->elements))
347                         return $this->last->parent; // up to ListElement
348
349                 // Move elements
350                 foreach(array_keys($obj->elements) as $key)
351                         parent::insert($obj->elements[$key]);
352
353                 return $this->last;
354         }
355
356         function toString()
357         {
358                 return $this->wrap(parent::toString(), $this->tag, $this->style);
359         }
360 }
361
362 class ListElement extends Element
363 {
364         function ListElement($level, $head)
365         {
366                 parent::Element();
367                 $this->level = $level;
368                 $this->head  = $head;
369         }
370
371         function canContain(& $obj)
372         {
373                 return (! is_a($obj, 'ListContainer') || ($obj->level > $this->level));
374         }
375
376         function toString()
377         {
378                 return $this->wrap(parent::toString(), $this->head);
379         }
380 }
381
382 // - One
383 // - Two
384 // - Three
385 class UList extends ListContainer
386 {
387         function UList(& $root, $text)
388         {
389                 parent::ListContainer('ul', 'li', '-', $text);
390         }
391 }
392
393 // + One
394 // + Two
395 // + Three
396 class OList extends ListContainer
397 {
398         function OList(& $root, $text)
399         {
400                 parent::ListContainer('ol', 'li', '+', $text);
401         }
402 }
403
404 // : definition1 | description1
405 // : definition2 | description2
406 // : definition3 | description3
407 class DList extends ListContainer
408 {
409         function DList($out)
410         {
411                 parent::ListContainer('dl', 'dt', ':', $out[0]);
412                 $this->last = & Element::insert(new ListElement($this->level, 'dd'));
413                 if ($out[1] != '')
414                         $this->last = & $this->last->insert(Factory_Inline($out[1]));
415         }
416 }
417
418 // > Someting cited
419 // > like E-mail text
420 class BQuote extends Element
421 {
422         var $level;
423
424         function BQuote(& $root, $text)
425         {
426                 parent::Element();
427
428                 $head = substr($text, 0, 1);
429                 $this->level = min(3, strspn($text, $head));
430                 $text = ltrim(substr($text, $this->level));
431
432                 if ($head == '<') { // Blockquote close
433                         $level       = $this->level;
434                         $this->level = 0;
435                         $this->last  = & $this->end($root, $level);
436                         if ($text != '')
437                                 $this->last = & $this->last->insert(Factory_Inline($text));
438                 } else {
439                         $this->insert(Factory_Inline($text));
440                 }
441         }
442
443         function canContain(& $obj)
444         {
445                 return (! is_a($obj, get_class($this)) || $obj->level >= $this->level);
446         }
447
448         function & insert(& $obj)
449         {
450                 // BugTrack/521, BugTrack/545
451                 if (is_a($obj, 'inline'))
452                         return parent::insert($obj->toPara(' class="quotation"'));
453
454                 if (is_a($obj, 'BQuote') && $obj->level == $this->level && count($obj->elements)) {
455                         $obj = & $obj->elements[0];
456                         if (is_a($this->last, 'Paragraph') && count($obj->elements))
457                                 $obj = & $obj->elements[0];
458                 }
459                 return parent::insert($obj);
460         }
461
462         function toString()
463         {
464                 return $this->wrap(parent::toString(), 'blockquote');
465         }
466
467         function & end(& $root, $level)
468         {
469                 $parent = & $root->last;
470
471                 while (is_object($parent)) {
472                         if (is_a($parent, 'BQuote') && $parent->level == $level)
473                                 return $parent->parent;
474                         $parent = & $parent->parent;
475                 }
476                 return $this;
477         }
478 }
479
480 class TableCell extends Element
481 {
482         var $tag = 'td'; // {td|th}
483         var $colspan = 1;
484         var $rowspan = 1;
485         var $style; // is array('width'=>, 'align'=>...);
486
487         function TableCell($text, $is_template = FALSE)
488         {
489                 parent::Element();
490                 $this->style = $matches = array();
491
492                 while (preg_match('/^(?:(LEFT|CENTER|RIGHT)|(BG)?COLOR\(([#\w]+)\)|SIZE\((\d+)\)):(.*)$/',
493                     $text, $matches)) {
494                         if ($matches[1]) {
495                                 $this->style['align'] = 'text-align:' . strtolower($matches[1]) . ';';
496                                 $text = $matches[5];
497                         } else if ($matches[3]) {
498                                 $name = $matches[2] ? 'background-color' : 'color';
499                                 $this->style[$name] = $name . ':' . htmlsc($matches[3]) . ';';
500                                 $text = $matches[5];
501                         } else if ($matches[4]) {
502                                 $this->style['size'] = 'font-size:' . htmlsc($matches[4]) . 'px;';
503                                 $text = $matches[5];
504                         }
505                 }
506                 if ($is_template && is_numeric($text))
507                         $this->style['width'] = 'width:' . $text . 'px;';
508
509                 if ($text == '>') {
510                         $this->colspan = 0;
511                 } else if ($text == '~') {
512                         $this->rowspan = 0;
513                 } else if (substr($text, 0, 1) == '~') {
514                         $this->tag = 'th';
515                         $text      = substr($text, 1);
516                 }
517
518                 if ($text != '' && $text{0} == '#') {
519                         // Try using Div class for this $text
520                         $obj = & Factory_Div($this, $text);
521                         if (is_a($obj, 'Paragraph'))
522                                 $obj = & $obj->elements[0];
523                 } else {
524                         $obj = & Factory_Inline($text);
525                 }
526
527                 $this->insert($obj);
528         }
529
530         function setStyle(& $style)
531         {
532                 foreach ($style as $key=>$value)
533                         if (! isset($this->style[$key]))
534                                 $this->style[$key] = $value;
535         }
536
537         function toString()
538         {
539                 if ($this->rowspan == 0 || $this->colspan == 0) return '';
540
541                 $param = ' class="style_' . $this->tag . '"';
542                 if ($this->rowspan > 1)
543                         $param .= ' rowspan="' . $this->rowspan . '"';
544                 if ($this->colspan > 1) {
545                         $param .= ' colspan="' . $this->colspan . '"';
546                         unset($this->style['width']);
547                 }
548                 if (! empty($this->style))
549                         $param .= ' style="' . join(' ', $this->style) . '"';
550
551                 return $this->wrap(parent::toString(), $this->tag, $param, FALSE);
552         }
553 }
554
555 // | title1 | title2 | title3 |
556 // | cell1  | cell2  | cell3  |
557 // | cell4  | cell5  | cell6  |
558 class Table extends Element
559 {
560         var $type;
561         var $types;
562         var $col; // number of column
563
564         function Table($out)
565         {
566                 parent::Element();
567
568                 $cells       = explode('|', $out[1]);
569                 $this->col   = count($cells);
570                 $this->type  = strtolower($out[2]);
571                 $this->types = array($this->type);
572                 $is_template = ($this->type == 'c');
573                 $row = array();
574                 foreach ($cells as $cell)
575                         $row[] = new TableCell($cell, $is_template);
576                 $this->elements[] = $row;
577         }
578
579         function canContain(& $obj)
580         {
581                 return is_a($obj, 'Table') && ($obj->col == $this->col);
582         }
583
584         function & insert(& $obj)
585         {
586                 $this->elements[] = $obj->elements[0];
587                 $this->types[]    = $obj->type;
588                 return $this;
589         }
590
591         function toString()
592         {
593                 static $parts = array('h'=>'thead', 'f'=>'tfoot', ''=>'tbody');
594
595                 // Set rowspan (from bottom, to top)
596                 for ($ncol = 0; $ncol < $this->col; $ncol++) {
597                         $rowspan = 1;
598                         foreach (array_reverse(array_keys($this->elements)) as $nrow) {
599                                 $row = & $this->elements[$nrow];
600                                 if ($row[$ncol]->rowspan == 0) {
601                                         ++$rowspan;
602                                         continue;
603                                 }
604                                 $row[$ncol]->rowspan = $rowspan;
605                                 // Inherits row type
606                                 while (--$rowspan)
607                                         $this->types[$nrow + $rowspan] = $this->types[$nrow];
608                                 $rowspan = 1;
609                         }
610                 }
611
612                 // Set colspan and style
613                 $stylerow = NULL;
614                 foreach (array_keys($this->elements) as $nrow) {
615                         $row = & $this->elements[$nrow];
616                         if ($this->types[$nrow] == 'c')
617                                 $stylerow = & $row;
618                         $colspan = 1;
619                         foreach (array_keys($row) as $ncol) {
620                                 if ($row[$ncol]->colspan == 0) {
621                                         ++$colspan;
622                                         continue;
623                                 }
624                                 $row[$ncol]->colspan = $colspan;
625                                 if ($stylerow !== NULL) {
626                                         $row[$ncol]->setStyle($stylerow[$ncol]->style);
627                                         // Inherits column style
628                                         while (--$colspan)
629                                                 $row[$ncol - $colspan]->setStyle($stylerow[$ncol]->style);
630                                 }
631                                 $colspan = 1;
632                         }
633                 }
634
635                 // toString
636                 $string = '';
637                 foreach ($parts as $type => $part)
638                 {
639                         $part_string = '';
640                         foreach (array_keys($this->elements) as $nrow) {
641                                 if ($this->types[$nrow] != $type)
642                                         continue;
643                                 $row        = & $this->elements[$nrow];
644                                 $row_string = '';
645                                 foreach (array_keys($row) as $ncol)
646                                         $row_string .= $row[$ncol]->toString();
647                                 $part_string .= $this->wrap($row_string, 'tr');
648                         }
649                         $string .= $this->wrap($part_string, $part);
650                 }
651                 $string = $this->wrap($string, 'table', ' class="style_table" cellspacing="1" border="0"');
652
653                 return $this->wrap($string, 'div', ' class="ie5"');
654         }
655 }
656
657 // , cell1  , cell2  ,  cell3 
658 // , cell4  , cell5  ,  cell6 
659 // , cell7  ,        right,==
660 // ,left          ,==,  cell8
661 class YTable extends Element
662 {
663         var $col;       // Number of columns
664
665         // TODO: Seems unable to show literal '==' without tricks.
666         //       But it will be imcompatible.
667         // TODO: Why toString() or toXHTML() here
668         function YTable($row = array('cell1 ', ' cell2 ', ' cell3'))
669         {
670                 parent::Element();
671
672                 $str = array();
673                 $col = count($row);
674
675                 $matches = $_value = $_align = array();
676                 foreach($row as $cell) {
677                         if (preg_match('/^(\s+)?(.+?)(\s+)?$/', $cell, $matches)) {
678                                 if ($matches[2] == '==') {
679                                         // Colspan
680                                         $_value[] = FALSE;
681                                         $_align[] = FALSE;
682                                 } else {
683                                         $_value[] = $matches[2];
684                                         if ($matches[1] == '') {
685                                                 $_align[] = ''; // left
686                                         } else if (isset($matches[3])) {
687                                                 $_align[] = 'center';
688                                         } else {
689                                                 $_align[] = 'right';
690                                         }
691                                 }
692                         } else {
693                                 $_value[] = $cell;
694                                 $_align[] = '';
695                         }
696                 }
697
698                 for ($i = 0; $i < $col; $i++) {
699                         if ($_value[$i] === FALSE) continue;
700                         $colspan = 1;
701                         while (isset($_value[$i + $colspan]) && $_value[$i + $colspan] === FALSE) ++$colspan;
702                         $colspan = ($colspan > 1) ? ' colspan="' . $colspan . '"' : '';
703                         $align = $_align[$i] ? ' style="text-align:' . $_align[$i] . '"' : '';
704                         $str[] = '<td class="style_td"' . $align . $colspan . '>';
705                         $str[] = make_link($_value[$i]);
706                         $str[] = '</td>';
707                         unset($_value[$i], $_align[$i]);
708                 }
709
710                 $this->col        = $col;
711                 $this->elements[] = implode('', $str);
712         }
713
714         function canContain(& $obj)
715         {
716                 return is_a($obj, 'YTable') && ($obj->col == $this->col);
717         }
718
719         function & insert(& $obj)
720         {
721                 $this->elements[] = $obj->elements[0];
722                 return $this;
723         }
724
725         function toString()
726         {
727                 $rows = '';
728                 foreach ($this->elements as $str) {
729                         $rows .= "\n" . '<tr class="style_tr">' . $str . '</tr>' . "\n";
730                 }
731                 $rows = $this->wrap($rows, 'table', ' class="style_table" cellspacing="1" border="0"');
732                 return $this->wrap($rows, 'div', ' class="ie5"');
733         }
734 }
735
736 // ' 'Space-beginning sentence
737 // ' 'Space-beginning sentence
738 // ' 'Space-beginning sentence
739 class Pre extends Element
740 {
741         function Pre(& $root, $text)
742         {
743                 global $preformat_ltrim;
744                 parent::Element();
745                 $this->elements[] = htmlsc(
746                         (! $preformat_ltrim || $text == '' || $text{0} != ' ') ? $text : substr($text, 1));
747         }
748
749         function canContain(& $obj)
750         {
751                 return is_a($obj, 'Pre');
752         }
753
754         function & insert(& $obj)
755         {
756                 $this->elements[] = $obj->elements[0];
757                 return $this;
758         }
759
760         function toString()
761         {
762                 return $this->wrap(join("\n", $this->elements), 'pre');
763         }
764 }
765
766 // Block plugin: #something (started with '#')
767 class Div extends Element
768 {
769         var $name;
770         var $param;
771
772         function Div($out)
773         {
774                 parent::Element();
775                 list(, $this->name, $this->param) = array_pad($out, 3, '');
776         }
777
778         function canContain(& $obj)
779         {
780                 return FALSE;
781         }
782
783         function toString()
784         {
785                 // Call #plugin
786                 return do_plugin_convert($this->name, $this->param);
787         }
788 }
789
790 // LEFT:/CENTER:/RIGHT:
791 class Align extends Element
792 {
793         var $align;
794
795         function Align($align)
796         {
797                 parent::Element();
798                 $this->align = $align;
799         }
800
801         function canContain(& $obj)
802         {
803                 return is_a($obj, 'Inline');
804         }
805
806         function toString()
807         {
808                 return $this->wrap(parent::toString(), 'div', ' style="text-align:' . $this->align . '"');
809         }
810 }
811
812 // Body
813 class Body extends Element
814 {
815         var $id;
816         var $count = 0;
817         var $contents;
818         var $contents_last;
819         var $classes = array(
820                 '-' => 'UList',
821                 '+' => 'OList',
822                 '>' => 'BQuote',
823                 '<' => 'BQuote');
824         var $factories = array(
825                 ':' => 'DList',
826                 '|' => 'Table',
827                 ',' => 'YTable',
828                 '#' => 'Div');
829
830         function Body($id)
831         {
832                 $this->id            = $id;
833                 $this->contents      = new Element();
834                 $this->contents_last = & $this->contents;
835                 parent::Element();
836         }
837
838         function parse(& $lines)
839         {
840                 $this->last = & $this;
841                 $matches = array();
842
843                 while (! empty($lines)) {
844                         $line = array_shift($lines);
845
846                         // Escape comments
847                         if (substr($line, 0, 2) == '//') continue;
848
849                         if (preg_match('/^(LEFT|CENTER|RIGHT):(.*)$/', $line, $matches)) {
850                                 // <div style="text-align:...">
851                                 $this->last = & $this->last->add(new Align(strtolower($matches[1])));
852                                 if ($matches[2] == '') continue;
853                                 $line = $matches[2];
854                         }
855
856                         $line = rtrim($line, "\r\n");
857
858                         // Empty
859                         if ($line == '') {
860                                 $this->last = & $this;
861                                 continue;
862                         }
863
864                         // Horizontal Rule
865                         if (substr($line, 0, 4) == '----') {
866                                 $this->insert(new HRule($this, $line));
867                                 continue;
868                         }
869
870                         // Multiline-enabled block plugin
871                         if (! PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK &&
872                             preg_match('/^#[^{]+(\{\{+)\s*$/', $line, $matches)) {
873                                 $len = strlen($matches[1]);
874                                 $line .= "\r"; // Delimiter
875                                 while (! empty($lines)) {
876                                         $next_line = preg_replace("/[\r\n]*$/", '', array_shift($lines));
877                                         if (preg_match('/\}{' . $len . '}/', $next_line)) {
878                                                 $line .= $next_line;
879                                                 break;
880                                         } else {
881                                                 $line .= $next_line .= "\r"; // Delimiter
882                                         }
883                                 }
884                         }
885
886                         // The first character
887                         $head = $line{0};
888
889                         // Heading
890                         if ($head == '*') {
891                                 $this->insert(new Heading($this, $line));
892                                 continue;
893                         }
894
895                         // Pre
896                         if ($head == ' ' || $head == "\t") {
897                                 $this->last = & $this->last->add(new Pre($this, $line));
898                                 continue;
899                         }
900
901                         // Line Break
902                         if (substr($line, -1) == '~')
903                                 $line = substr($line, 0, -1) . "\r";
904                         
905                         // Other Character
906                         if (isset($this->classes[$head])) {
907                                 $classname  = $this->classes[$head];
908                                 $this->last = & $this->last->add(new $classname($this, $line));
909                                 continue;
910                         }
911
912                         // Other Character
913                         if (isset($this->factories[$head])) {
914                                 $factoryname = 'Factory_' . $this->factories[$head];
915                                 $this->last  = & $this->last->add($factoryname($this, $line));
916                                 continue;
917                         }
918
919                         // Default
920                         $this->last = & $this->last->add(Factory_Inline($line));
921                 }
922         }
923
924         function getAnchor($text, $level)
925         {
926                 global $top, $_symbol_anchor;
927
928                 // Heading id (auto-generated)
929                 $autoid = 'content_' . $this->id . '_' . $this->count;
930                 $this->count++;
931
932                 // Heading id (specified by users)
933                 $id = make_heading($text, FALSE); // Cut fixed-anchor from $text
934                 if ($id == '') {
935                         // Not specified
936                         $id     = & $autoid;
937                         $anchor = '';
938                 } else {
939                         $anchor = ' &aname(' . $id . ',super,full){' . $_symbol_anchor . '};';
940                 }
941
942                 $text = ' ' . $text;
943
944                 // Add 'page contents' link to its heading
945                 $this->contents_last = & $this->contents_last->add(new Contents_UList($text, $level, $id));
946
947                 // Add heding
948                 return array($text . $anchor, $this->count > 1 ? "\n" . $top : '', $autoid);
949         }
950
951         function & insert(& $obj)
952         {
953                 if (is_a($obj, 'Inline')) $obj = & $obj->toPara();
954                 return parent::insert($obj);
955         }
956
957         function toString()
958         {
959                 global $vars;
960
961                 $text = parent::toString();
962
963                 // #contents
964                 $text = preg_replace_callback('/<#_contents_>/',
965                         array(& $this, 'replace_contents'), $text);
966
967                 return $text . "\n";
968         }
969
970         function replace_contents($arr)
971         {
972                 $contents  = '<div class="contents">' . "\n" .
973                                 '<a id="contents_' . $this->id . '"></a>' . "\n" .
974                                 $this->contents->toString() . "\n" .
975                                 '</div>' . "\n";
976                 return $contents;
977         }
978 }
979
980 class Contents_UList extends ListContainer
981 {
982         function Contents_UList($text, $level, $id)
983         {
984                 // Reformatting $text
985                 // A line started with "\n" means "preformatted" ... X(
986                 make_heading($text);
987                 $text = "\n" . '<a href="#' . $id . '">' . $text . '</a>' . "\n";
988                 parent::ListContainer('ul', 'li', '-', str_repeat('-', $level));
989                 $this->insert(Factory_Inline($text));
990         }
991
992         function setParent(& $parent)
993         {
994                 global $_list_pad_str;
995
996                 parent::setParent($parent);
997                 $step   = $this->level;
998                 $margin = $this->left_margin;
999                 if (isset($parent->parent) && is_a($parent->parent, 'ListContainer')) {
1000                         $step  -= $parent->parent->level;
1001                         $margin = 0;
1002                 }
1003                 $margin += $this->margin * ($step == $this->level ? 1 : $step);
1004                 $this->style = sprintf($_list_pad_str, $this->level, $margin, $margin);
1005         }
1006 }
1007