2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: convert_html.php,v 1.7 2005/01/21 13:17:16 henoheno Exp $
5 // function 'convert_html()', wiki text parser
6 // and related classes-and-functions
8 function convert_html($lines)
10 global $vars, $digest;
11 static $contents_id = 0;
14 $digest = md5(join('', get_source($vars['page'])));
16 if (! is_array($lines)) $lines = explode("\n", $lines);
18 $body = & new Body(++$contents_id);
21 return $body->toString();
27 var $parent; // ¿ÆÍ×ÁÇ
28 var $last; // ¼¡¤ËÍ×ÁǤòÁÞÆþ¤¹¤ëÀè
29 var $elements; // Í×ÁǤÎÇÛÎó
33 $this->elements = array();
34 $this->last = & $this;
37 function setParent(& $parent)
39 $this->parent = & $parent;
42 function & add(& $obj)
44 if ($this->canContain($obj)) {
45 return $this->insert($obj);
47 return $this->parent->add($obj);
51 function & insert(& $obj)
53 $obj->setParent($this);
54 $this->elements[] = & $obj;
56 return $this->last = & $obj->last;
59 function canContain($obj)
64 function wrap($string, $tag, $param = '', $canomit = TRUE)
66 return ($canomit && $string == '') ? '' :
67 '<' . $tag . $param . '>' . $string . '</' . $tag . '>';
73 foreach (array_keys($this->elements) as $key)
74 $ret[] = $this->elements[$key]->toString();
75 return join("\n", $ret);
78 function dump($indent = 0)
80 $ret = str_repeat(' ', $indent) . get_class($this) . "\n";
82 foreach (array_keys($this->elements) as $key) {
83 $ret .= is_object($this->elements[$key]) ?
84 $this->elements[$key]->dump($indent) : '';
85 //str_repeat(' ', $indent) . $this->elements[$key];
91 function & Factory_Inline($text)
93 if (substr($text, 0, 1) == '~') {
94 // ¹ÔƬ '~' ¡£¥Ñ¥é¥°¥é¥Õ³«»Ï
95 return new Paragraph(' ' . substr($text, 1));
97 return new Inline($text);
101 function & Factory_DList(& $root, $text)
103 $out = explode('|', ltrim($text), 2);
104 if (count($out) < 2) {
105 return Factory_Inline($text);
107 return new DList($out);
111 // '|'-separated table
112 function & Factory_Table(& $root, $text)
114 if (! preg_match('/^\|(.+)\|([hHfFcC]?)$/', $text, $out)) {
115 return Factory_Inline($text);
117 return new Table($out);
121 // Comma-separated table
122 function & Factory_YTable(& $root, $text)
125 return Factory_Inline($text);
127 return new YTable(csv_explode(',', substr($text, 1)));
131 function & Factory_Div(& $root, $text)
133 if (! preg_match('/^\#([^\(]+)(?:\((.*)\))?/', $text, $out) ||
134 ! exist_plugin_convert($out[1])) {
135 return new Paragraph($text);
137 return new Div($out);
142 class Inline extends Element
144 function Inline($text)
147 $this->elements[] = trim((substr($text, 0, 1) == "\n") ?
148 $text : make_link($text));
151 function & insert(& $obj)
153 $this->elements[] = $obj->elements[0];
157 function canContain($obj)
159 return is_a($obj, 'Inline');
165 return join(($line_break ? '<br />' . "\n" : "\n"), $this->elements);
168 function & toPara($class = '')
170 $obj = & new Paragraph('', $class);
176 // Paragraph: blank-line-separated sentences
177 class Paragraph extends Element
181 function Paragraph($text, $param = '')
184 $this->param = $param;
185 if ($text == '') return;
187 if (substr($text, 0, 1) == '~')
188 $text = ' ' . substr($text, 1);
190 $this->insert(Factory_Inline($text));
193 function canContain($obj)
195 return is_a($obj, 'Inline');
200 return $this->wrap(parent::toString(), 'p', $this->param);
207 class Heading extends Element
213 function Heading(& $root, $text)
217 $this->level = min(3, strspn($text, '*'));
218 list($text, $this->msg_top, $this->id) = $root->getAnchor($text, $this->level);
219 $this->insert(Factory_Inline($text));
220 $this->level++; // h2,h3,h4
223 function & insert(& $obj)
225 parent::insert($obj);
226 return $this->last = & $this;
229 function canContain(& $obj)
236 return $this->msg_top . $this->wrap(parent::toString(),
237 'h' . $this->level, ' id="' . $this->id . '"');
243 class HRule extends Element
245 function HRule(& $root, $text)
250 function canContain(& $obj)
262 class ListContainer extends Element
271 function ListContainer($tag, $tag2, $head, $text)
276 $var_margin = '_' . $tag . '_margin';
277 $var_left_margin = '_' . $tag . '_left_margin';
279 global $$var_margin, $$var_left_margin;
281 $this->margin = $$var_margin;
282 $this->left_margin = $$var_left_margin;
287 $this->level = min(3, strspn($text, $head));
288 $text = ltrim(substr($text, $this->level));
290 parent::insert(new ListElement($this->level, $tag2));
292 $this->last = & $this->last->insert(Factory_Inline($text));
295 function canContain(& $obj)
297 return (! is_a($obj, 'ListContainer')
298 || ($this->tag == $obj->tag && $this->level == $obj->level));
301 function setParent(& $parent)
303 global $_list_pad_str;
305 parent::setParent($parent);
307 $step = $this->level;
308 if (isset($parent->parent) && is_a($parent->parent, 'ListContainer'))
309 $step -= $parent->parent->level;
311 $margin = $this->margin * $step;
312 if ($step == $this->level)
313 $margin += $this->left_margin;
315 $this->style = sprintf($_list_pad_str, $this->level, $margin, $margin);
318 function & insert(& $obj)
320 if (! is_a($obj, get_class($this)))
321 return $this->last = & $this->last->insert($obj);
323 // ¹ÔƬʸ»ú¤Î¤ß¤Î»ØÄê»þ¤ÏUL/OL¥Ö¥í¥Ã¥¯¤òæ½Ð
325 if (count($obj->elements) == 1 && empty($obj->elements[0]->elements))
326 return $this->last->parent; // up to ListElement.
329 foreach(array_keys($obj->elements) as $key)
330 parent::insert($obj->elements[$key]);
337 return $this->wrap(parent::toString(), $this->tag, $this->style);
341 class ListElement extends Element
343 function ListElement($level, $head)
346 $this->level = $level;
350 function canContain(& $obj)
352 return (! is_a($obj, 'ListContainer') || ($obj->level > $this->level));
357 return $this->wrap(parent::toString(), $this->head);
364 class UList extends ListContainer
366 function UList(& $root, $text)
368 parent::ListContainer('ul', 'li', '-', $text);
375 class OList extends ListContainer
377 function OList(& $root, $text)
379 parent::ListContainer('ol', 'li', '+', $text);
383 // : definition1 | description1
384 // : definition2 | description2
385 // : definition3 | description3
386 class DList extends ListContainer
390 parent::ListContainer('dl', 'dt', ':', $out[0]);
391 $this->last = & Element::insert(new ListElement($this->level, 'dd'));
393 $this->last = & $this->last->insert(Factory_Inline($out[1]));
398 // > like E-mail text
399 class BQuote extends Element
403 function BQuote(& $root, $text)
407 $head = substr($text, 0, 1);
408 $this->level = min(3, strspn($text, $head));
409 $text = ltrim(substr($text, $this->level));
411 if ($head == '<') { // Blockquote close
412 $level = $this->level;
414 $this->last = & $this->end($root, $level);
416 $this->last = & $this->last->insert(Factory_Inline($text));
418 $this->insert(Factory_Inline($text));
422 function canContain(& $obj)
424 return (! is_a($obj, get_class($this)) || $obj->level >= $this->level);
427 function & insert(& $obj)
429 // BugTrack/521, BugTrack/545
430 if (is_a($obj, 'inline'))
431 return parent::insert($obj->toPara(' class="quotation"'));
433 if (is_a($obj, 'BQuote') && $obj->level == $this->level && count($obj->elements)) {
434 $obj = & $obj->elements[0];
435 if (is_a($this->last, 'Paragraph') && count($obj->elements))
436 $obj = & $obj->elements[0];
438 return parent::insert($obj);
443 return $this->wrap(parent::toString(), 'blockquote');
446 function & end(& $root, $level)
448 $parent = & $root->last;
450 while (is_object($parent)) {
451 if (is_a($parent, 'BQuote') && $parent->level == $level)
452 return $parent->parent;
453 $parent = & $parent->parent;
459 class TableCell extends Element
461 var $tag = 'td'; // {td|th}
464 var $style; // is array('width'=>, 'align'=>...);
466 function TableCell($text, $is_template = FALSE)
469 $this->style = $matches = array();
471 while (preg_match('/^(?:(LEFT|CENTER|RIGHT)|(BG)?COLOR\(([#\w]+)\)|SIZE\((\d+)\)):(.*)$/',
474 $this->style['align'] = 'text-align:' . strtolower($matches[1]) . ';';
476 } else if ($matches[3]) {
477 $name = $matches[2] ? 'background-color' : 'color';
478 $this->style[$name] = $name . ':' . htmlspecialchars($matches[3]) . ';';
480 } else if ($matches[4]) {
481 $this->style['size'] = 'font-size:' . htmlspecialchars($matches[4]) . 'px;';
485 if ($is_template && is_numeric($text))
486 $this->style['width'] = 'width:' . $text . 'px;';
490 } else if ($text == '~') {
492 } else if (substr($text, 0, 1) == '~') {
494 $text = substr($text, 1);
497 if ($text != '' && $text{0} == '#') {
498 // ¥»¥ëÆâÍƤ¬'#'¤Ç»Ï¤Þ¤ë¤È¤¤ÏDiv¥¯¥é¥¹¤òÄ̤·¤Æ¤ß¤ë
499 $obj = & Factory_Div($this, $text);
500 if (is_a($obj, 'Paragraph'))
501 $obj = & $obj->elements[0];
503 $obj = & Factory_Inline($text);
509 function setStyle(& $style)
511 foreach ($style as $key=>$value)
512 if (! isset($this->style[$key]))
513 $this->style[$key] = $value;
518 if ($this->rowspan == 0 || $this->colspan == 0) return '';
520 $param = ' class="style_' . $this->tag . '"';
521 if ($this->rowspan > 1)
522 $param .= ' rowspan="' . $this->rowspan . '"';
523 if ($this->colspan > 1) {
524 $param .= ' colspan="' . $this->colspan . '"';
525 unset($this->style['width']);
527 if (! empty($this->style))
528 $param .= ' style="' . join(' ', $this->style) . '"';
530 return $this->wrap(parent::toString(), $this->tag, $param, FALSE);
534 // | title1 | title2 | title3 |
535 // | cell1 | cell2 | cell3 |
536 // | cell4 | cell5 | cell6 |
537 class Table extends Element
541 var $col; // number of column
547 $cells = explode('|', $out[1]);
548 $this->col = count($cells);
549 $this->type = strtolower($out[2]);
550 $this->types = array($this->type);
551 $is_template = ($this->type == 'c');
553 foreach ($cells as $cell)
554 $row[] = & new TableCell($cell, $is_template);
555 $this->elements[] = $row;
558 function canContain(& $obj)
560 return is_a($obj, 'Table') && ($obj->col == $this->col);
563 function & insert(& $obj)
565 $this->elements[] = $obj->elements[0];
566 $this->types[] = $obj->type;
572 static $parts = array('h'=>'thead', 'f'=>'tfoot', ''=>'tbody');
574 // rowspan¤òÀßÄê(²¼¤«¤é¾å¤Ø)
575 for ($ncol = 0; $ncol < $this->col; $ncol++) {
577 foreach (array_reverse(array_keys($this->elements)) as $nrow) {
578 $row = & $this->elements[$nrow];
579 if ($row[$ncol]->rowspan == 0) {
583 $row[$ncol]->rowspan = $rowspan;
584 while (--$rowspan) // ¹Ô¼ïÊ̤ò·Ñ¾µ¤¹¤ë
585 $this->types[$nrow + $rowspan] = $this->types[$nrow];
590 // colspan,style¤òÀßÄê
592 foreach (array_keys($this->elements) as $nrow) {
593 $row = & $this->elements[$nrow];
594 if ($this->types[$nrow] == 'c')
597 foreach (array_keys($row) as $ncol) {
598 if ($row[$ncol]->colspan == 0) {
602 $row[$ncol]->colspan = $colspan;
603 if ($stylerow !== NULL) {
604 $row[$ncol]->setStyle($stylerow[$ncol]->style);
605 while (--$colspan) // Îó¥¹¥¿¥¤¥ë¤ò·Ñ¾µ¤¹¤ë
606 $row[$ncol - $colspan]->setStyle($stylerow[$ncol]->style);
614 foreach ($parts as $type => $part)
617 foreach (array_keys($this->elements) as $nrow) {
618 if ($this->types[$nrow] != $type)
620 $row = & $this->elements[$nrow];
622 foreach (array_keys($row) as $ncol)
623 $row_string .= $row[$ncol]->toString();
624 $part_string .= $this->wrap($row_string, 'tr');
626 $string .= $this->wrap($part_string, $part);
628 $string = $this->wrap($string, 'table', ' class="style_table" cellspacing="1" border="0"');
630 return $this->wrap($string, 'div', ' class="ie5"');
634 // , title1 , title2 , title3
635 // , cell1 , cell2 , cell3
636 // , cell4 , cell5 , cell6
637 class YTable extends Element
641 function YTable($_value)
645 $align = $value = $matches = array();
646 foreach($_value as $val) {
647 if (preg_match('/^(\s+)?(.+?)(\s+)?$/', $val, $matches)) {
648 $align[] =($matches[1] != '') ?
649 ((isset($matches[3]) && $matches[3] != '') ?
650 ' style="text-align:center"' :
651 ' style="text-align:right"'
653 $value[] = $matches[2];
659 $this->col = count($value);
661 foreach ($value as $val)
662 $colspan[] = ($val == '==') ? 0 : 1;
664 $count = count($value);
665 for ($i = 0; $i < $count; $i++) {
667 while ($i + $colspan[$i] < $count && $value[$i + $colspan[$i]] == '==')
669 $colspan[$i] = ($colspan[$i] > 1) ? ' colspan="' . $colspan[$i] . '"' : '';
670 $str .= '<td class="style_td"' . $align[$i] . $colspan[$i] . '>' . make_link($value[$i]) . '</td>';
673 $this->elements[] = $str;
676 function canContain(& $obj)
678 return is_a($obj, 'YTable') && ($obj->col == $this->col);
681 function & insert(& $obj)
683 $this->elements[] = $obj->elements[0];
690 foreach ($this->elements as $str)
691 $rows .= "\n" . '<tr class="style_tr">' . $str . '</tr>' . "\n";
692 $rows = $this->wrap($rows, 'table', ' class="style_table" cellspacing="1" border="0"');
693 return $this->wrap($rows, 'div', ' class="ie5"');
697 // ' 'Space-beginning sentence
698 // ' 'Space-beginning sentence
699 // ' 'Space-beginning sentence
700 class Pre extends Element
702 function Pre(& $root, $text)
704 global $preformat_ltrim;
706 $this->elements[] = htmlspecialchars(
707 (! $preformat_ltrim || $text == '' || $text{0} != ' ') ? $text : substr($text, 1));
710 function canContain(& $obj)
712 return is_a($obj, 'Pre');
715 function & insert(& $obj)
717 $this->elements[] = $obj->elements[0];
723 return $this->wrap(join("\n", $this->elements), 'pre');
727 // #something (started with '#')
728 class Div extends Element
736 list(, $this->name, $this->param) = array_pad($out, 3, '');
739 function canContain(& $obj)
746 return do_plugin_convert($this->name, $this->param);
750 // LEFT:/CENTER:/RIGHT:
751 class Align extends Element
755 function Align($align)
758 $this->align = $align;
761 function canContain(& $obj)
763 return is_a($obj, 'Inline');
768 return $this->wrap(parent::toString(), 'div', ' style="text-align:' . $this->align . '"');
773 class Body extends Element
779 var $classes = array(
784 var $factories = array(
793 $this->contents = & new Element();
794 $this->contents_last = & $this->contents;
798 function parse(& $lines)
800 $this->last = & $this;
803 while (! empty($lines)) {
804 $line = array_shift($lines);
807 if (substr($line, 0, 2) == '//') continue;
809 if (preg_match('/^(LEFT|CENTER|RIGHT):(.*)$/', $line, $matches)) {
810 // <div style="text-align:...">
811 $this->last = & $this->last->add(new Align(strtolower($matches[1])));
812 if ($matches[2] == '') continue;
816 $line = preg_replace("/[\r\n]*$/", '', $line);
820 $this->last = & $this;
825 if (substr($line, 0, 4) == '----') {
826 $this->insert(new HRule($this, $line));
830 // The first character
835 $this->insert(new Heading($this, $line));
840 if ($head == ' ' || $head == "\t") {
841 $this->last = & $this->last->add(new Pre($this, $line));
846 if (substr($line, -1) == '~')
847 $line = substr($line, 0, -1) . "\r";
850 if (isset($this->classes[$head])) {
851 $classname = $this->classes[$head];
852 $this->last = & $this->last->add(new $classname($this, $line));
857 if (isset($this->factories[$head])) {
858 $factoryname = 'Factory_' . $this->factories[$head];
859 $this->last = & $this->last->add($factoryname($this, $line));
864 $this->last = & $this->last->add(Factory_Inline($line));
868 function getAnchor($text, $level)
870 global $top, $_symbol_anchor;
872 $id = make_heading($text, FALSE); // Cut fixed-anchor from $text
873 $anchor = ($id == '') ? '' : ' &aname(' . $id . ',super,full){' . $_symbol_anchor . '};';
876 $id = 'content_' . $this->id . '_' . $this->count;
878 $this->contents_last = & $this->contents_last->add(new Contents_UList($text, $level, $id));
880 return array($text . $anchor, $this->count > 1 ? "\n" . $top : '', $id);
883 function & insert(& $obj)
885 if (is_a($obj, 'Inline')) $obj = & $obj->toPara();
886 return parent::insert($obj);
893 $text = parent::toString();
896 $text = preg_replace_callback('/(<p[^>]*>)<del>#contents<\/del>(\s*)(<\/p>)/',
897 array(& $this, 'replace_contents'), $text);
900 // <p>¤Î¤È¤¤Ï¹ÔƬ¤«¤é¡¢<del>¤Î¤È¤¤Ï¾¤ÎÍ×ÁǤλÒÍ×ÁǤȤ·¤Æ¸ºß
901 $text = preg_replace_callback('/(<p[^>]*>)<del>#related<\/del>(\s*)(<\/p>)/',
902 array(& $this, 'replace_related'), $text);
903 $text = preg_replace('/<del>#related<\/del>/', make_related($vars['page'], 'del'), $text);
907 function replace_contents($arr)
909 $contents = '<div class="contents">' . "\n" .
910 '<a id="contents_' . $this->id . '"></a>' . "\n" .
911 $this->contents->toString() . "\n" .
914 return ($arr[1] != '') ? $contents . join('', $arr) : $contents;
917 function replace_related($arr)
920 static $related = NULL;
922 if (is_null($related)) $related = make_related($vars['page'], 'p');
924 return ($arr[1] != '') ? $related . join('', $arr) : $related;
928 class Contents_UList extends ListContainer
930 function Contents_UList($text, $level, $id)
932 // ¥Æ¥¥¹¥È¤Î¥ê¥Õ¥©¡¼¥à
933 // ¹ÔƬ\n¤ÇÀ°·ÁºÑ¤ß¤òɽ¤¹ ... X(
935 $text = "\n" . '<a href="#' . $id . '">' . $text . '</a>' . "\n";
936 parent::ListContainer('ul', 'li', '-', str_repeat('-', $level));
937 $this->insert(Factory_Inline($text));
940 function setParent(& $parent)
942 global $_list_pad_str;
944 parent::setParent($parent);
945 $step = $this->level;
946 $margin = $this->left_margin;
947 if (isset($parent->parent) && is_a($parent->parent, 'ListContainer')) {
948 $step -= $parent->parent->level;
951 $margin += $this->margin * ($step == $this->level ? 1 : $step);
952 $this->style = sprintf($_list_pad_str, $this->level, $margin, $margin);