OSDN Git Service

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