7 * LICENSE: This source file is licensed under the terms of the GNU General Public License.
9 * @package Magic3 Framework
10 * @author 平田直毅(Naoki Hirata) <naoki@aplo.co.jp>
11 * @copyright Copyright 2006-2012 Magic3 Project.
12 * @license http://www.gnu.org/copyleft/gpl.html GPL License
13 * @version SVN: $Id: convert_html.php 4951 2012-06-09 09:55:41Z fishbone $
14 * @link http://www.magic3.org
17 // 2002-2005 PukiWiki Developers Team
18 // 2001-2002 Originally written by yu-ji
19 // License: GPL v2 or (at your option) any later version
21 // function 'convert_html()', wiki text parser
22 // and related classes-and-functions
24 function convert_html($lines)
26 global $vars, $digest;
27 static $contents_id = 0;
29 // comment for Magic3 by naoki on 2008/9/30
30 // get_sourceが2度実行されている
32 //$digest = md5(join('', get_source($vars['page'])));
33 $digest = md5(get_source(WikiParam::getPage(), true));
34 WikiParam::setDigest($digest); // ダイジェストの設定はここで行う
36 if (! is_array($lines)) $lines = explode("\n", $lines);
37 //global $gErrorManager;
38 //$gErrorManager->writeDebug(__METHOD__, 'convert_html start'); // 時間計測用
39 $body = new Body(++$contents_id);
41 //$gErrorManager->writeDebug(__METHOD__, 'convert_html end'); // 時間計測用
42 return $body->toString();
49 var $elements; // References of childs
50 var $last; // Insert new one at the back of the $last
54 $this->elements = array();
58 function setParent($parent)
60 $this->parent = $parent;
65 if ($this->canContain($obj)) {
66 return $this->insert($obj);
68 return $this->parent->add($obj);
74 $obj->setParent($this);
75 $this->elements[] = $obj;
77 return $this->last = $obj->last;
80 function canContain($obj)
85 function wrap($string, $tag, $param = '', $canomit = TRUE)
87 return ($canomit && $string == '') ? '' :
88 '<' . $tag . $param . '>' . $string . '</' . $tag . '>';
94 foreach (array_keys($this->elements) as $key)
95 $ret[] = $this->elements[$key]->toString();
96 return join("\n", $ret);
99 function dump($indent = 0)
101 $ret = str_repeat(' ', $indent) . get_class($this) . "\n";
103 foreach (array_keys($this->elements) as $key) {
104 $ret .= is_object($this->elements[$key]) ?
105 $this->elements[$key]->dump($indent) : '';
106 //str_repeat(' ', $indent) . $this->elements[$key];
112 // Returns inline-related object
113 function Factory_Inline($text)
115 // Check the first letter of the line
116 if (substr($text, 0, 1) == '~') {
117 return new Paragraph(' ' . substr($text, 1));
119 return new Inline($text);
123 function Factory_DList($root, $text)
125 $out = explode('|', ltrim($text), 2);
126 if (count($out) < 2) {
127 return Factory_Inline($text);
129 return new DList($out);
133 // '|'-separated table
134 function Factory_Table($root, $text)
136 if (! preg_match('/^\|(.+)\|([hHfFcC]?)$/', $text, $out)) {
137 return Factory_Inline($text);
139 return new Table($out);
143 // Comma-separated table
144 function Factory_YTable($root, $text)
147 return Factory_Inline($text);
149 return new YTable(csv_explode(',', substr($text, 1)));
153 function Factory_Div($root, $text)
157 // Seems block plugin?
158 if (PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK) {
160 if (preg_match('/^\#([^\(]+)(?:\((.*)\))?/', $text, $matches) &&
161 exist_plugin_convert($matches[1])) {
162 return new Div($matches);
166 if(preg_match('/^#([^\(\{]+)(?:\(([^\r]*)\))?(\{*)/', $text, $matches) &&
167 exist_plugin_convert($matches[1])) {
168 $len = strlen($matches[3]);
171 return new Div($matches); // Seems legacy block plugin
172 } else if (preg_match('/\{{' . $len . '}\s*\r(.*)\r\}{' . $len . '}/', $text, $body)) {
173 $matches[2] .= "\r" . $body[1] . "\r";
174 return new Div($matches); // Seems multiline-enabled block plugin
179 return new Paragraph($text);
183 class Inline extends Element
185 function Inline($text)
188 $this->elements[] = trim((substr($text, 0, 1) == "\n") ?
189 $text : make_link($text));
192 function insert($obj)
194 $this->elements[] = $obj->elements[0];
198 function canContain($obj)
200 return is_a($obj, 'Inline');
206 return join(($line_break ? '<br />' . "\n" : "\n"), $this->elements);
209 function toPara($class = '')
211 $obj = new Paragraph('', $class);
217 // Paragraph: blank-line-separated sentences
218 class Paragraph extends Element
222 function Paragraph($text, $param = '')
225 $this->param = $param;
226 if ($text == '') return;
228 if (substr($text, 0, 1) == '~')
229 $text = ' ' . substr($text, 1);
231 $this->insert(Factory_Inline($text));
234 function canContain($obj)
236 return is_a($obj, 'Inline');
241 return $this->wrap(parent::toString(), 'p', $this->param);
248 class Heading extends Element
254 function Heading($root, $text)
258 $this->level = min(3, strspn($text, '*'));
259 list($text, $this->msg_top, $this->id) = $root->getAnchor($text, $this->level);
260 $this->insert(Factory_Inline($text));
261 $this->level++; // h2,h3,h4
264 function insert($obj)
266 parent::insert($obj);
267 return $this->last = $this;
270 function canContain($obj)
277 return $this->msg_top . $this->wrap(parent::toString(),
278 'h' . $this->level, ' id="' . $this->id . '"');
284 class HRule extends Element
286 function HRule($root, $text)
291 function canContain($obj)
303 // Lists (UL, OL, DL)
304 class ListContainer extends Element
313 function ListContainer($tag, $tag2, $head, $text)
317 $var_margin = '_' . $tag . '_margin';
318 $var_left_margin = '_' . $tag . '_left_margin';
319 global $$var_margin, $$var_left_margin;
321 $this->margin = $$var_margin;
322 $this->left_margin = $$var_left_margin;
326 $this->level = min(3, strspn($text, $head));
327 $text = ltrim(substr($text, $this->level));
329 parent::insert(new ListElement($this->level, $tag2));
331 $this->last = $this->last->insert(Factory_Inline($text));
334 function canContain($obj)
336 return (! is_a($obj, 'ListContainer')
337 || ($this->tag == $obj->tag && $this->level == $obj->level));
340 function setParent($parent)
342 global $_list_pad_str;
344 parent::setParent($parent);
346 $step = $this->level;
347 if (isset($parent->parent) && is_a($parent->parent, 'ListContainer'))
348 $step -= $parent->parent->level;
350 $margin = $this->margin * $step;
351 if ($step == $this->level)
352 $margin += $this->left_margin;
354 $this->style = sprintf($_list_pad_str, $this->level, $margin, $margin);
357 function insert($obj)
359 if (! is_a($obj, get_class($this)))
360 return $this->last = $this->last->insert($obj);
362 // Break if no elements found (BugTrack/524)
363 if (count($obj->elements) == 1 && empty($obj->elements[0]->elements))
364 return $this->last->parent; // up to ListElement
367 foreach(array_keys($obj->elements) as $key)
368 parent::insert($obj->elements[$key]);
375 return $this->wrap(parent::toString(), $this->tag, $this->style);
379 class ListElement extends Element
381 function ListElement($level, $head)
384 $this->level = $level;
388 function canContain($obj)
390 return (! is_a($obj, 'ListContainer') || ($obj->level > $this->level));
395 return $this->wrap(parent::toString(), $this->head);
402 class UList extends ListContainer
404 function UList($root, $text)
406 parent::ListContainer('ul', 'li', '-', $text);
413 class OList extends ListContainer
415 function OList($root, $text)
417 parent::ListContainer('ol', 'li', '+', $text);
421 // : definition1 | description1
422 // : definition2 | description2
423 // : definition3 | description3
424 class DList extends ListContainer
428 parent::ListContainer('dl', 'dt', ':', $out[0]);
429 $this->last = Element::insert(new ListElement($this->level, 'dd'));
431 $this->last = $this->last->insert(Factory_Inline($out[1]));
436 // > like E-mail text
437 class BQuote extends Element
441 function BQuote($root, $text)
445 $head = substr($text, 0, 1);
446 $this->level = min(3, strspn($text, $head));
447 $text = ltrim(substr($text, $this->level));
449 if ($head == '<') { // Blockquote close
450 $level = $this->level;
452 $this->last = $this->end($root, $level);
454 $this->last = $this->last->insert(Factory_Inline($text));
456 $this->insert(Factory_Inline($text));
460 function canContain($obj)
462 return (! is_a($obj, get_class($this)) || $obj->level >= $this->level);
465 function insert($obj)
467 // BugTrack/521, BugTrack/545
468 if (is_a($obj, 'inline'))
469 return parent::insert($obj->toPara(' class="quotation"'));
471 if (is_a($obj, 'BQuote') && $obj->level == $this->level && count($obj->elements)) {
472 $obj = $obj->elements[0];
473 if (is_a($this->last, 'Paragraph') && count($obj->elements))
474 $obj = $obj->elements[0];
476 return parent::insert($obj);
481 return $this->wrap(parent::toString(), 'blockquote');
484 function end($root, $level)
486 $parent = $root->last;
488 while (is_object($parent)) {
489 if (is_a($parent, 'BQuote') && $parent->level == $level)
490 return $parent->parent;
491 $parent = $parent->parent;
497 class TableCell extends Element
499 var $tag = 'td'; // {td|th}
502 var $style; // is array('width'=>, 'align'=>...);
504 function TableCell($text, $is_template = FALSE)
507 $this->style = $matches = array();
509 while (preg_match('/^(?:(LEFT|CENTER|RIGHT)|(BG)?COLOR\(([#\w]+)\)|SIZE\((\d+)\)):(.*)$/',
512 $this->style['align'] = 'text-align:' . strtolower($matches[1]) . ';';
514 } else if ($matches[3]) {
515 $name = $matches[2] ? 'background-color' : 'color';
516 $this->style[$name] = $name . ':' . htmlspecialchars($matches[3]) . ';';
518 } else if ($matches[4]) {
519 $this->style['size'] = 'font-size:' . htmlspecialchars($matches[4]) . 'px;';
523 if ($is_template && is_numeric($text))
524 $this->style['width'] = 'width:' . $text . 'px;';
528 } else if ($text == '~') {
530 } else if (substr($text, 0, 1) == '~') {
532 $text = substr($text, 1);
535 if ($text != '' && $text{0} == '#') {
536 // Try using Div class for this $text
537 $obj = Factory_Div($this, $text);
538 if (is_a($obj, 'Paragraph'))
539 $obj = $obj->elements[0];
541 $obj = Factory_Inline($text);
547 function setStyle($style)
549 foreach ($style as $key=>$value)
550 if (! isset($this->style[$key]))
551 $this->style[$key] = $value;
556 if ($this->rowspan == 0 || $this->colspan == 0) return '';
558 $param = ' class="style_' . $this->tag . '"';
559 if ($this->rowspan > 1)
560 $param .= ' rowspan="' . $this->rowspan . '"';
561 if ($this->colspan > 1) {
562 $param .= ' colspan="' . $this->colspan . '"';
563 unset($this->style['width']);
565 if (! empty($this->style))
566 $param .= ' style="' . join(' ', $this->style) . '"';
568 return $this->wrap(parent::toString(), $this->tag, $param, FALSE);
572 // | title1 | title2 | title3 |
573 // | cell1 | cell2 | cell3 |
574 // | cell4 | cell5 | cell6 |
575 class Table extends Element
579 var $col; // number of column
585 $cells = explode('|', $out[1]);
586 $this->col = count($cells);
587 $this->type = strtolower($out[2]);
588 $this->types = array($this->type);
589 $is_template = ($this->type == 'c');
591 foreach ($cells as $cell)
592 $row[] = new TableCell($cell, $is_template);
593 $this->elements[] = $row;
596 function canContain($obj)
598 return is_a($obj, 'Table') && ($obj->col == $this->col);
601 function insert($obj)
603 $this->elements[] = $obj->elements[0];
604 $this->types[] = $obj->type;
610 static $parts = array('h'=>'thead', 'f'=>'tfoot', ''=>'tbody');
612 // Set rowspan (from bottom, to top)
613 for ($ncol = 0; $ncol < $this->col; $ncol++) {
615 foreach (array_reverse(array_keys($this->elements)) as $nrow) {
616 $row = $this->elements[$nrow];
617 if ($row[$ncol]->rowspan == 0) {
621 $row[$ncol]->rowspan = $rowspan;
624 $this->types[$nrow + $rowspan] = $this->types[$nrow];
629 // Set colspan and style
631 foreach (array_keys($this->elements) as $nrow) {
632 $row = $this->elements[$nrow];
633 if ($this->types[$nrow] == 'c')
636 foreach (array_keys($row) as $ncol) {
637 if ($row[$ncol]->colspan == 0) {
641 $row[$ncol]->colspan = $colspan;
642 if ($stylerow !== NULL) {
643 $row[$ncol]->setStyle($stylerow[$ncol]->style);
644 // Inherits column style
646 $row[$ncol - $colspan]->setStyle($stylerow[$ncol]->style);
654 foreach ($parts as $type => $part)
657 foreach (array_keys($this->elements) as $nrow) {
658 if ($this->types[$nrow] != $type)
660 $row = $this->elements[$nrow];
662 foreach (array_keys($row) as $ncol)
663 $row_string .= $row[$ncol]->toString();
664 $part_string .= $this->wrap($row_string, 'tr');
666 $string .= $this->wrap($part_string, $part);
668 $string = $this->wrap($string, 'table', ' class="style_table" cellspacing="1" border="0"');
670 return $this->wrap($string, 'div', ' class="ie5"');
674 // , title1 , title2 , title3
675 // , cell1 , cell2 , cell3
676 // , cell4 , cell5 , cell6
677 class YTable extends Element
681 function YTable($_value)
685 $align = $value = $matches = array();
686 foreach($_value as $val) {
687 if (preg_match('/^(\s+)?(.+?)(\s+)?$/', $val, $matches)) {
688 $align[] =($matches[1] != '') ?
689 ((isset($matches[3]) && $matches[3] != '') ?
690 ' style="text-align:center"' :
691 ' style="text-align:right"'
693 $value[] = $matches[2];
699 $this->col = count($value);
701 foreach ($value as $val)
702 $colspan[] = ($val == '==') ? 0 : 1;
704 $count = count($value);
705 for ($i = 0; $i < $count; $i++) {
707 while ($i + $colspan[$i] < $count && $value[$i + $colspan[$i]] == '==')
709 $colspan[$i] = ($colspan[$i] > 1) ? ' colspan="' . $colspan[$i] . '"' : '';
710 $str .= '<td class="style_td"' . $align[$i] . $colspan[$i] . '>' . make_link($value[$i]) . '</td>';
713 $this->elements[] = $str;
716 function canContain($obj)
718 return is_a($obj, 'YTable') && ($obj->col == $this->col);
721 function insert($obj)
723 $this->elements[] = $obj->elements[0];
730 foreach ($this->elements as $str)
731 $rows .= "\n" . '<tr class="style_tr">' . $str . '</tr>' . "\n";
732 $rows = $this->wrap($rows, 'table', ' class="style_table" cellspacing="1" border="0"');
733 return $this->wrap($rows, 'div', ' class="ie5"');
737 // ' 'Space-beginning sentence
738 // ' 'Space-beginning sentence
739 // ' 'Space-beginning sentence
740 class Pre extends Element
742 function Pre($root, $text)
744 global $preformat_ltrim;
746 $this->elements[] = htmlspecialchars(
747 (! $preformat_ltrim || $text == '' || $text{0} != ' ') ? $text : substr($text, 1));
750 function canContain($obj)
752 return is_a($obj, 'Pre');
755 function insert($obj)
757 $this->elements[] = $obj->elements[0];
763 //return $this->wrap(join("\n", $this->elements), 'pre');
764 return $this->wrap(join("\n", $this->elements), 'pre', ' class="wiki_pre"'); // modified for magic3
768 // Block plugin: #something (started with '#')
769 class Div extends Element
777 list(, $this->name, $this->param) = array_pad($out, 3, '');
780 function canContain($obj)
788 return do_plugin_convert($this->name, $this->param);
792 // LEFT:/CENTER:/RIGHT:
793 class Align extends Element
797 function Align($align)
800 $this->align = $align;
803 function canContain($obj)
805 return is_a($obj, 'Inline');
810 return $this->wrap(parent::toString(), 'div', ' style="text-align:' . $this->align . '"');
815 class Body extends Element
821 var $classes = array(
826 var $factories = array(
835 $this->contents = new Element();
836 $this->contents_last = $this->contents;
840 function parse($lines)
845 while (! empty($lines)) {
846 $line = array_shift($lines);
849 if (substr($line, 0, 2) == '//') continue;
851 if (preg_match('/^(LEFT|CENTER|RIGHT):(.*)$/', $line, $matches)) {
852 // <div style="text-align:...">
853 $this->last = $this->last->add(new Align(strtolower($matches[1])));
854 if ($matches[2] == '') continue;
858 $line = rtrim($line, "\r\n");
867 if (substr($line, 0, 4) == '----') {
868 $this->insert(new HRule($this, $line));
872 // Multiline-enabled block plugin
873 if (! PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK &&
874 preg_match('/^#[^{]+(\{\{+)\s*$/', $line, $matches)) {
875 $len = strlen($matches[1]);
876 $line .= "\r"; // Delimiter
877 while (! empty($lines)) {
878 $next_line = preg_replace("/[\r\n]*$/", '', array_shift($lines));
879 if (preg_match('/\}{' . $len . '}/', $next_line)) {
883 $line .= $next_line .= "\r"; // Delimiter
888 // The first character
893 $this->insert(new Heading($this, $line));
898 if ($head == ' ' || $head == "\t") {
899 $this->last = $this->last->add(new Pre($this, $line));
904 if (substr($line, -1) == '~')
905 $line = substr($line, 0, -1) . "\r";
908 if (isset($this->classes[$head])) {
909 $classname = $this->classes[$head];
910 $this->last = $this->last->add(new $classname($this, $line));
915 if (isset($this->factories[$head])) {
916 $factoryname = 'Factory_' . $this->factories[$head];
917 $this->last = $this->last->add($factoryname($this, $line));
922 $this->last = $this->last->add(Factory_Inline($line));
926 function getAnchor($text, $level)
928 global $top, $_symbol_anchor;
930 // Heading id (auto-generated)
931 $autoid = 'content_' . $this->id . '_' . $this->count;
934 // Heading id (specified by users)
935 $id = make_heading($text, FALSE); // Cut fixed-anchor from $text
941 $anchor = ' &aname(' . $id . ',super,full){' . $_symbol_anchor . '};';
946 // Add 'page contents' link to its heading
947 $this->contents_last = $this->contents_last->add(new Contents_UList($text, $level, $id));
950 return array($text . $anchor, $this->count > 1 ? "\n" . $top : '', $autoid);
953 function insert($obj)
955 if (is_a($obj, 'Inline')) $obj = $obj->toPara();
956 return parent::insert($obj);
963 $text = parent::toString();
966 $text = preg_replace_callback('/<#_contents_>/',
967 array($this, 'replace_contents'), $text);
972 function replace_contents($arr)
974 //$contents = '<div class="contents">' . "\n" . '<a id="contents_' . $this->id . '"></a>' . "\n" . $this->contents->toString() . "\n" . '</div>' . "\n";
975 $contents = '<div class="wiki_content">' . "\n" . '<a id="wiki_content_' . $this->id . '"></a>' . "\n" . $this->contents->toString() . "\n" . '</div>' . "\n";
980 class Contents_UList extends ListContainer
982 function Contents_UList($text, $level, $id)
984 // Reformatting $text
985 // A line started with "\n" means "preformatted" ... X(
987 $text = "\n" . '<a href="#' . $id . '">' . $text . '</a>' . "\n";
988 parent::ListContainer('ul', 'li', '-', str_repeat('-', $level));
989 $this->insert(Factory_Inline($text));
992 function setParent($parent)
994 global $_list_pad_str;
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;
1003 $margin += $this->margin * ($step == $this->level ? 1 : $step);
1004 $this->style = sprintf($_list_pad_str, $this->level, $margin, $margin);