diff options
Diffstat (limited to 'platform/www/inc/Parsing/Handler')
-rw-r--r-- | platform/www/inc/Parsing/Handler/AbstractRewriter.php | 39 | ||||
-rw-r--r-- | platform/www/inc/Parsing/Handler/Block.php | 211 | ||||
-rw-r--r-- | platform/www/inc/Parsing/Handler/CallWriter.php | 40 | ||||
-rw-r--r-- | platform/www/inc/Parsing/Handler/CallWriterInterface.php | 30 | ||||
-rw-r--r-- | platform/www/inc/Parsing/Handler/Lists.php | 186 | ||||
-rw-r--r-- | platform/www/inc/Parsing/Handler/Nest.php | 82 | ||||
-rw-r--r-- | platform/www/inc/Parsing/Handler/Preformatted.php | 49 | ||||
-rw-r--r-- | platform/www/inc/Parsing/Handler/Quote.php | 86 | ||||
-rw-r--r-- | platform/www/inc/Parsing/Handler/ReWriterInterface.php | 37 | ||||
-rw-r--r-- | platform/www/inc/Parsing/Handler/Table.php | 320 |
10 files changed, 1080 insertions, 0 deletions
diff --git a/platform/www/inc/Parsing/Handler/AbstractRewriter.php b/platform/www/inc/Parsing/Handler/AbstractRewriter.php new file mode 100644 index 0000000..d9becbf --- /dev/null +++ b/platform/www/inc/Parsing/Handler/AbstractRewriter.php @@ -0,0 +1,39 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +/** + * Basic implementation of the rewriter interface to be specialized by children + */ +abstract class AbstractRewriter implements ReWriterInterface +{ + /** @var CallWriterInterface original CallWriter */ + protected $callWriter; + + /** @var array[] list of calls */ + public $calls = array(); + + /** @inheritdoc */ + public function __construct(CallWriterInterface $callWriter) + { + $this->callWriter = $callWriter; + } + + /** @inheritdoc */ + public function writeCall($call) + { + $this->calls[] = $call; + } + + /** * @inheritdoc */ + public function writeCalls($calls) + { + $this->calls = array_merge($this->calls, $calls); + } + + /** @inheritDoc */ + public function getCallWriter() + { + return $this->callWriter; + } +} diff --git a/platform/www/inc/Parsing/Handler/Block.php b/platform/www/inc/Parsing/Handler/Block.php new file mode 100644 index 0000000..4cfa686 --- /dev/null +++ b/platform/www/inc/Parsing/Handler/Block.php @@ -0,0 +1,211 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +/** + * Handler for paragraphs + * + * @author Harry Fuecks <hfuecks@gmail.com> + */ +class Block +{ + protected $calls = array(); + protected $skipEol = false; + protected $inParagraph = false; + + // Blocks these should not be inside paragraphs + protected $blockOpen = array( + 'header', + 'listu_open','listo_open','listitem_open','listcontent_open', + 'table_open','tablerow_open','tablecell_open','tableheader_open','tablethead_open', + 'quote_open', + 'code','file','hr','preformatted','rss', + 'htmlblock','phpblock', + 'footnote_open', + ); + + protected $blockClose = array( + 'header', + 'listu_close','listo_close','listitem_close','listcontent_close', + 'table_close','tablerow_close','tablecell_close','tableheader_close','tablethead_close', + 'quote_close', + 'code','file','hr','preformatted','rss', + 'htmlblock','phpblock', + 'footnote_close', + ); + + // Stacks can contain paragraphs + protected $stackOpen = array( + 'section_open', + ); + + protected $stackClose = array( + 'section_close', + ); + + + /** + * Constructor. Adds loaded syntax plugins to the block and stack + * arrays + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + public function __construct() + { + global $DOKU_PLUGINS; + //check if syntax plugins were loaded + if (empty($DOKU_PLUGINS['syntax'])) return; + foreach ($DOKU_PLUGINS['syntax'] as $n => $p) { + $ptype = $p->getPType(); + if ($ptype == 'block') { + $this->blockOpen[] = 'plugin_'.$n; + $this->blockClose[] = 'plugin_'.$n; + } elseif ($ptype == 'stack') { + $this->stackOpen[] = 'plugin_'.$n; + $this->stackClose[] = 'plugin_'.$n; + } + } + } + + protected function openParagraph($pos) + { + if ($this->inParagraph) return; + $this->calls[] = array('p_open',array(), $pos); + $this->inParagraph = true; + $this->skipEol = true; + } + + /** + * Close a paragraph if needed + * + * This function makes sure there are no empty paragraphs on the stack + * + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string|integer $pos + */ + protected function closeParagraph($pos) + { + if (!$this->inParagraph) return; + // look back if there was any content - we don't want empty paragraphs + $content = ''; + $ccount = count($this->calls); + for ($i=$ccount-1; $i>=0; $i--) { + if ($this->calls[$i][0] == 'p_open') { + break; + } elseif ($this->calls[$i][0] == 'cdata') { + $content .= $this->calls[$i][1][0]; + } else { + $content = 'found markup'; + break; + } + } + + if (trim($content)=='') { + //remove the whole paragraph + //array_splice($this->calls,$i); // <- this is much slower than the loop below + for ($x=$ccount; $x>$i; + $x--) array_pop($this->calls); + } else { + // remove ending linebreaks in the paragraph + $i=count($this->calls)-1; + if ($this->calls[$i][0] == 'cdata') $this->calls[$i][1][0] = rtrim($this->calls[$i][1][0], "\n"); + $this->calls[] = array('p_close',array(), $pos); + } + + $this->inParagraph = false; + $this->skipEol = true; + } + + protected function addCall($call) + { + $key = count($this->calls); + if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) { + $this->calls[$key-1][1][0] .= $call[1][0]; + } else { + $this->calls[] = $call; + } + } + + // simple version of addCall, without checking cdata + protected function storeCall($call) + { + $this->calls[] = $call; + } + + /** + * Processes the whole instruction stack to open and close paragraphs + * + * @author Harry Fuecks <hfuecks@gmail.com> + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param array $calls + * + * @return array + */ + public function process($calls) + { + // open first paragraph + $this->openParagraph(0); + foreach ($calls as $key => $call) { + $cname = $call[0]; + if ($cname == 'plugin') { + $cname='plugin_'.$call[1][0]; + $plugin = true; + $plugin_open = (($call[1][2] == DOKU_LEXER_ENTER) || ($call[1][2] == DOKU_LEXER_SPECIAL)); + $plugin_close = (($call[1][2] == DOKU_LEXER_EXIT) || ($call[1][2] == DOKU_LEXER_SPECIAL)); + } else { + $plugin = false; + } + /* stack */ + if (in_array($cname, $this->stackClose) && (!$plugin || $plugin_close)) { + $this->closeParagraph($call[2]); + $this->storeCall($call); + $this->openParagraph($call[2]); + continue; + } + if (in_array($cname, $this->stackOpen) && (!$plugin || $plugin_open)) { + $this->closeParagraph($call[2]); + $this->storeCall($call); + $this->openParagraph($call[2]); + continue; + } + /* block */ + // If it's a substition it opens and closes at the same call. + // To make sure next paragraph is correctly started, let close go first. + if (in_array($cname, $this->blockClose) && (!$plugin || $plugin_close)) { + $this->closeParagraph($call[2]); + $this->storeCall($call); + $this->openParagraph($call[2]); + continue; + } + if (in_array($cname, $this->blockOpen) && (!$plugin || $plugin_open)) { + $this->closeParagraph($call[2]); + $this->storeCall($call); + continue; + } + /* eol */ + if ($cname == 'eol') { + // Check this isn't an eol instruction to skip... + if (!$this->skipEol) { + // Next is EOL => double eol => mark as paragraph + if (isset($calls[$key+1]) && $calls[$key+1][0] == 'eol') { + $this->closeParagraph($call[2]); + $this->openParagraph($call[2]); + } else { + //if this is just a single eol make a space from it + $this->addCall(array('cdata',array("\n"), $call[2])); + } + } + continue; + } + /* normal */ + $this->addCall($call); + $this->skipEol = false; + } + // close last paragraph + $call = end($this->calls); + $this->closeParagraph($call[2]); + return $this->calls; + } +} diff --git a/platform/www/inc/Parsing/Handler/CallWriter.php b/platform/www/inc/Parsing/Handler/CallWriter.php new file mode 100644 index 0000000..2457143 --- /dev/null +++ b/platform/www/inc/Parsing/Handler/CallWriter.php @@ -0,0 +1,40 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +class CallWriter implements CallWriterInterface +{ + + /** @var \Doku_Handler $Handler */ + protected $Handler; + + /** + * @param \Doku_Handler $Handler + */ + public function __construct(\Doku_Handler $Handler) + { + $this->Handler = $Handler; + } + + /** @inheritdoc */ + public function writeCall($call) + { + $this->Handler->calls[] = $call; + } + + /** @inheritdoc */ + public function writeCalls($calls) + { + $this->Handler->calls = array_merge($this->Handler->calls, $calls); + } + + /** + * @inheritdoc + * function is required, but since this call writer is first/highest in + * the chain it is not required to do anything + */ + public function finalise() + { + unset($this->Handler); + } +} diff --git a/platform/www/inc/Parsing/Handler/CallWriterInterface.php b/platform/www/inc/Parsing/Handler/CallWriterInterface.php new file mode 100644 index 0000000..ffc2468 --- /dev/null +++ b/platform/www/inc/Parsing/Handler/CallWriterInterface.php @@ -0,0 +1,30 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +interface CallWriterInterface +{ + /** + * Add a call to our call list + * + * @param array $call the call to be added + */ + public function writeCall($call); + + /** + * Append a list of calls to our call list + * + * @param array[] $calls list of calls to be appended + */ + public function writeCalls($calls); + + /** + * Explicit request to finish up and clean up NOW! + * (probably because document end has been reached) + * + * If part of a CallWriter chain, call finalise on + * the original call writer + * + */ + public function finalise(); +} diff --git a/platform/www/inc/Parsing/Handler/Lists.php b/platform/www/inc/Parsing/Handler/Lists.php new file mode 100644 index 0000000..282ddfb --- /dev/null +++ b/platform/www/inc/Parsing/Handler/Lists.php @@ -0,0 +1,186 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +class Lists extends AbstractRewriter +{ + protected $listCalls = array(); + protected $listStack = array(); + + protected $initialDepth = 0; + + const NODE = 1; + + /** @inheritdoc */ + public function finalise() + { + $last_call = end($this->calls); + $this->writeCall(array('list_close',array(), $last_call[2])); + + $this->process(); + $this->callWriter->finalise(); + unset($this->callWriter); + } + + /** @inheritdoc */ + public function process() + { + + foreach ($this->calls as $call) { + switch ($call[0]) { + case 'list_item': + $this->listOpen($call); + break; + case 'list_open': + $this->listStart($call); + break; + case 'list_close': + $this->listEnd($call); + break; + default: + $this->listContent($call); + break; + } + } + + $this->callWriter->writeCalls($this->listCalls); + return $this->callWriter; + } + + protected function listStart($call) + { + $depth = $this->interpretSyntax($call[1][0], $listType); + + $this->initialDepth = $depth; + // array(list type, current depth, index of current listitem_open) + $this->listStack[] = array($listType, $depth, 1); + + $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]); + $this->listCalls[] = array('listitem_open',array(1),$call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + } + + + protected function listEnd($call) + { + $closeContent = true; + + while ($list = array_pop($this->listStack)) { + if ($closeContent) { + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $closeContent = false; + } + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]); + } + } + + protected function listOpen($call) + { + $depth = $this->interpretSyntax($call[1][0], $listType); + $end = end($this->listStack); + $key = key($this->listStack); + + // Not allowed to be shallower than initialDepth + if ($depth < $this->initialDepth) { + $depth = $this->initialDepth; + } + + if ($depth == $end[1]) { + // Just another item in the list... + if ($listType == $end[0]) { + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + // new list item, update list stack's index into current listitem_open + $this->listStack[$key][2] = count($this->listCalls) - 2; + + // Switched list type... + } else { + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); + $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); + $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + array_pop($this->listStack); + $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2); + } + } elseif ($depth > $end[1]) { // Getting deeper... + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); + $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + // set the node/leaf state of this item's parent listitem_open to NODE + $this->listCalls[$this->listStack[$key][2]][1][1] = self::NODE; + + $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2); + } else { // Getting shallower ( $depth < $end[1] ) + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); + + // Throw away the end - done + array_pop($this->listStack); + + while (1) { + $end = end($this->listStack); + $key = key($this->listStack); + + if ($end[1] <= $depth) { + // Normalize depths + $depth = $end[1]; + + $this->listCalls[] = array('listitem_close',array(),$call[2]); + + if ($end[0] == $listType) { + $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + // new list item, update list stack's index into current listitem_open + $this->listStack[$key][2] = count($this->listCalls) - 2; + } else { + // Switching list type... + $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); + $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); + $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + array_pop($this->listStack); + $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2); + } + + break; + + // Haven't dropped down far enough yet.... ( $end[1] > $depth ) + } else { + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); + + array_pop($this->listStack); + } + } + } + } + + protected function listContent($call) + { + $this->listCalls[] = $call; + } + + protected function interpretSyntax($match, & $type) + { + if (substr($match, -1) == '*') { + $type = 'u'; + } else { + $type = 'o'; + } + // Is the +1 needed? It used to be count(explode(...)) + // but I don't think the number is seen outside this handler + return substr_count(str_replace("\t", ' ', $match), ' ') + 1; + } +} diff --git a/platform/www/inc/Parsing/Handler/Nest.php b/platform/www/inc/Parsing/Handler/Nest.php new file mode 100644 index 0000000..98d2134 --- /dev/null +++ b/platform/www/inc/Parsing/Handler/Nest.php @@ -0,0 +1,82 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +/** + * Generic call writer class to handle nesting of rendering instructions + * within a render instruction. Also see nest() method of renderer base class + * + * @author Chris Smith <chris@jalakai.co.uk> + */ +class Nest extends AbstractRewriter +{ + protected $closingInstruction; + + /** + * @inheritdoc + * + * @param CallWriterInterface $CallWriter the parser's current call writer, i.e. the one above us in the chain + * @param string $close closing instruction name, this is required to properly terminate the + * syntax mode if the document ends without a closing pattern + */ + public function __construct(CallWriterInterface $CallWriter, $close = "nest_close") + { + parent::__construct($CallWriter); + $this->closingInstruction = $close; + } + + /** @inheritdoc */ + public function writeCall($call) + { + $this->calls[] = $call; + } + + /** @inheritdoc */ + public function writeCalls($calls) + { + $this->calls = array_merge($this->calls, $calls); + } + + /** @inheritdoc */ + public function finalise() + { + $last_call = end($this->calls); + $this->writeCall(array($this->closingInstruction,array(), $last_call[2])); + + $this->process(); + $this->callWriter->finalise(); + unset($this->callWriter); + } + + /** @inheritdoc */ + public function process() + { + // merge consecutive cdata + $unmerged_calls = $this->calls; + $this->calls = array(); + + foreach ($unmerged_calls as $call) $this->addCall($call); + + $first_call = reset($this->calls); + $this->callWriter->writeCall(array("nest", array($this->calls), $first_call[2])); + + return $this->callWriter; + } + + /** + * @param array $call + */ + protected function addCall($call) + { + $key = count($this->calls); + if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) { + $this->calls[$key-1][1][0] .= $call[1][0]; + } elseif ($call[0] == 'eol') { + // do nothing (eol shouldn't be allowed, to counter preformatted fix in #1652 & #1699) + } else { + $this->calls[] = $call; + } + } + + +} diff --git a/platform/www/inc/Parsing/Handler/Preformatted.php b/platform/www/inc/Parsing/Handler/Preformatted.php new file mode 100644 index 0000000..41beb66 --- /dev/null +++ b/platform/www/inc/Parsing/Handler/Preformatted.php @@ -0,0 +1,49 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +class Preformatted extends AbstractRewriter +{ + + protected $pos; + protected $text =''; + + /** @inheritdoc */ + public function finalise() + { + $last_call = end($this->calls); + $this->writeCall(array('preformatted_end',array(), $last_call[2])); + + $this->process(); + $this->callWriter->finalise(); + unset($this->callWriter); + } + + /** @inheritdoc */ + public function process() + { + foreach ($this->calls as $call) { + switch ($call[0]) { + case 'preformatted_start': + $this->pos = $call[2]; + break; + case 'preformatted_newline': + $this->text .= "\n"; + break; + case 'preformatted_content': + $this->text .= $call[1][0]; + break; + case 'preformatted_end': + if (trim($this->text)) { + $this->callWriter->writeCall(array('preformatted', array($this->text), $this->pos)); + } + // see FS#1699 & FS#1652, add 'eol' instructions to ensure proper triggering of following p_open + $this->callWriter->writeCall(array('eol', array(), $this->pos)); + $this->callWriter->writeCall(array('eol', array(), $this->pos)); + break; + } + } + + return $this->callWriter; + } +} diff --git a/platform/www/inc/Parsing/Handler/Quote.php b/platform/www/inc/Parsing/Handler/Quote.php new file mode 100644 index 0000000..74861b1 --- /dev/null +++ b/platform/www/inc/Parsing/Handler/Quote.php @@ -0,0 +1,86 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +class Quote extends AbstractRewriter +{ + protected $quoteCalls = array(); + + /** @inheritdoc */ + public function finalise() + { + $last_call = end($this->calls); + $this->writeCall(array('quote_end',array(), $last_call[2])); + + $this->process(); + $this->callWriter->finalise(); + unset($this->callWriter); + } + + /** @inheritdoc */ + public function process() + { + + $quoteDepth = 1; + + foreach ($this->calls as $call) { + switch ($call[0]) { + + /** @noinspection PhpMissingBreakStatementInspection */ + case 'quote_start': + $this->quoteCalls[] = array('quote_open',array(),$call[2]); + // fallthrough + case 'quote_newline': + $quoteLength = $this->getDepth($call[1][0]); + + if ($quoteLength > $quoteDepth) { + $quoteDiff = $quoteLength - $quoteDepth; + for ($i = 1; $i <= $quoteDiff; $i++) { + $this->quoteCalls[] = array('quote_open',array(),$call[2]); + } + } elseif ($quoteLength < $quoteDepth) { + $quoteDiff = $quoteDepth - $quoteLength; + for ($i = 1; $i <= $quoteDiff; $i++) { + $this->quoteCalls[] = array('quote_close',array(),$call[2]); + } + } else { + if ($call[0] != 'quote_start') $this->quoteCalls[] = array('linebreak',array(),$call[2]); + } + + $quoteDepth = $quoteLength; + + break; + + case 'quote_end': + if ($quoteDepth > 1) { + $quoteDiff = $quoteDepth - 1; + for ($i = 1; $i <= $quoteDiff; $i++) { + $this->quoteCalls[] = array('quote_close',array(),$call[2]); + } + } + + $this->quoteCalls[] = array('quote_close',array(),$call[2]); + + $this->callWriter->writeCalls($this->quoteCalls); + break; + + default: + $this->quoteCalls[] = $call; + break; + } + } + + return $this->callWriter; + } + + /** + * @param string $marker + * @return int + */ + protected function getDepth($marker) + { + preg_match('/>{1,}/', $marker, $matches); + $quoteLength = strlen($matches[0]); + return $quoteLength; + } +} diff --git a/platform/www/inc/Parsing/Handler/ReWriterInterface.php b/platform/www/inc/Parsing/Handler/ReWriterInterface.php new file mode 100644 index 0000000..2fa7b25 --- /dev/null +++ b/platform/www/inc/Parsing/Handler/ReWriterInterface.php @@ -0,0 +1,37 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +/** + * A ReWriter takes over from the orignal call writer and handles all new calls itself until + * the process method is called and control is given back to the original writer. + * + * @property array[] $calls The list of current calls + */ +interface ReWriterInterface extends CallWriterInterface +{ + /** + * ReWriterInterface constructor. + * + * This rewriter will be registered as the new call writer in the Handler. + * The original is passed as parameter + * + * @param CallWriterInterface $callWriter the original callwriter + */ + public function __construct(CallWriterInterface $callWriter); + + /** + * Process any calls that have been added and add them to the + * original call writer + * + * @return CallWriterInterface the orignal call writer + */ + public function process(); + + /** + * Accessor for this rewriter's original CallWriter + * + * @return CallWriterInterface + */ + public function getCallWriter(); +} diff --git a/platform/www/inc/Parsing/Handler/Table.php b/platform/www/inc/Parsing/Handler/Table.php new file mode 100644 index 0000000..35ff5a8 --- /dev/null +++ b/platform/www/inc/Parsing/Handler/Table.php @@ -0,0 +1,320 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +class Table extends AbstractRewriter +{ + + protected $tableCalls = array(); + protected $maxCols = 0; + protected $maxRows = 1; + protected $currentCols = 0; + protected $firstCell = false; + protected $lastCellType = 'tablecell'; + protected $inTableHead = true; + protected $currentRow = array('tableheader' => 0, 'tablecell' => 0); + protected $countTableHeadRows = 0; + + /** @inheritdoc */ + public function finalise() + { + $last_call = end($this->calls); + $this->writeCall(array('table_end',array(), $last_call[2])); + + $this->process(); + $this->callWriter->finalise(); + unset($this->callWriter); + } + + /** @inheritdoc */ + public function process() + { + foreach ($this->calls as $call) { + switch ($call[0]) { + case 'table_start': + $this->tableStart($call); + break; + case 'table_row': + $this->tableRowClose($call); + $this->tableRowOpen(array('tablerow_open',$call[1],$call[2])); + break; + case 'tableheader': + case 'tablecell': + $this->tableCell($call); + break; + case 'table_end': + $this->tableRowClose($call); + $this->tableEnd($call); + break; + default: + $this->tableDefault($call); + break; + } + } + $this->callWriter->writeCalls($this->tableCalls); + + return $this->callWriter; + } + + protected function tableStart($call) + { + $this->tableCalls[] = array('table_open',$call[1],$call[2]); + $this->tableCalls[] = array('tablerow_open',array(),$call[2]); + $this->firstCell = true; + } + + protected function tableEnd($call) + { + $this->tableCalls[] = array('table_close',$call[1],$call[2]); + $this->finalizeTable(); + } + + protected function tableRowOpen($call) + { + $this->tableCalls[] = $call; + $this->currentCols = 0; + $this->firstCell = true; + $this->lastCellType = 'tablecell'; + $this->maxRows++; + if ($this->inTableHead) { + $this->currentRow = array('tablecell' => 0, 'tableheader' => 0); + } + } + + protected function tableRowClose($call) + { + if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) { + $this->countTableHeadRows++; + } + // Strip off final cell opening and anything after it + while ($discard = array_pop($this->tableCalls)) { + if ($discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') { + break; + } + if (!empty($this->currentRow[$discard[0]])) { + $this->currentRow[$discard[0]]--; + } + } + $this->tableCalls[] = array('tablerow_close', array(), $call[2]); + + if ($this->currentCols > $this->maxCols) { + $this->maxCols = $this->currentCols; + } + } + + protected function isTableHeadRow() + { + $td = $this->currentRow['tablecell']; + $th = $this->currentRow['tableheader']; + + if (!$th || $td > 2) return false; + if (2*$td > $th) return false; + + return true; + } + + protected function tableCell($call) + { + if ($this->inTableHead) { + $this->currentRow[$call[0]]++; + } + if (!$this->firstCell) { + // Increase the span + $lastCall = end($this->tableCalls); + + // A cell call which follows an open cell means an empty cell so span + if ($lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open') { + $this->tableCalls[] = array('colspan',array(),$call[2]); + } + + $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]); + $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]); + $this->lastCellType = $call[0]; + } else { + $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]); + $this->lastCellType = $call[0]; + $this->firstCell = false; + } + + $this->currentCols++; + } + + protected function tableDefault($call) + { + $this->tableCalls[] = $call; + } + + protected function finalizeTable() + { + + // Add the max cols and rows to the table opening + if ($this->tableCalls[0][0] == 'table_open') { + // Adjust to num cols not num col delimeters + $this->tableCalls[0][1][] = $this->maxCols - 1; + $this->tableCalls[0][1][] = $this->maxRows; + $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]); + } else { + trigger_error('First element in table call list is not table_open'); + } + + $lastRow = 0; + $lastCell = 0; + $cellKey = array(); + $toDelete = array(); + + // if still in tableheader, then there can be no table header + // as all rows can't be within <THEAD> + if ($this->inTableHead) { + $this->inTableHead = false; + $this->countTableHeadRows = 0; + } + + // Look for the colspan elements and increment the colspan on the + // previous non-empty opening cell. Once done, delete all the cells + // that contain colspans + for ($key = 0; $key < count($this->tableCalls); ++$key) { + $call = $this->tableCalls[$key]; + + switch ($call[0]) { + case 'table_open': + if ($this->countTableHeadRows) { + array_splice($this->tableCalls, $key+1, 0, array( + array('tablethead_open', array(), $call[2]))); + } + break; + + case 'tablerow_open': + $lastRow++; + $lastCell = 0; + break; + + case 'tablecell_open': + case 'tableheader_open': + $lastCell++; + $cellKey[$lastRow][$lastCell] = $key; + break; + + case 'table_align': + $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open')); + $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close')); + // If the cell is empty, align left + if ($prev && $next) { + $this->tableCalls[$key-1][1][1] = 'left'; + + // If the previous element was a cell open, align right + } elseif ($prev) { + $this->tableCalls[$key-1][1][1] = 'right'; + + // If the next element is the close of an element, align either center or left + } elseif ($next) { + if ($this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right') { + $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center'; + } else { + $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left'; + } + } + + // Now convert the whitespace back to cdata + $this->tableCalls[$key][0] = 'cdata'; + break; + + case 'colspan': + $this->tableCalls[$key-1][1][0] = false; + + for ($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) { + if ($this->tableCalls[$i][0] == 'tablecell_open' || + $this->tableCalls[$i][0] == 'tableheader_open' + ) { + if (false !== $this->tableCalls[$i][1][0]) { + $this->tableCalls[$i][1][0]++; + break; + } + } + } + + $toDelete[] = $key-1; + $toDelete[] = $key; + $toDelete[] = $key+1; + break; + + case 'rowspan': + if ($this->tableCalls[$key-1][0] == 'cdata') { + // ignore rowspan if previous call was cdata (text mixed with :::) + // we don't have to check next call as that wont match regex + $this->tableCalls[$key][0] = 'cdata'; + } else { + $spanning_cell = null; + + // can't cross thead/tbody boundary + if (!$this->countTableHeadRows || ($lastRow-1 != $this->countTableHeadRows)) { + for ($i = $lastRow-1; $i > 0; $i--) { + if ($this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' || + $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open' + ) { + if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) { + $spanning_cell = $i; + break; + } + } + } + } + if (is_null($spanning_cell)) { + // No spanning cell found, so convert this cell to + // an empty one to avoid broken tables + $this->tableCalls[$key][0] = 'cdata'; + $this->tableCalls[$key][1][0] = ''; + break; + } + $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++; + + $this->tableCalls[$key-1][1][2] = false; + + $toDelete[] = $key-1; + $toDelete[] = $key; + $toDelete[] = $key+1; + } + break; + + case 'tablerow_close': + // Fix broken tables by adding missing cells + $moreCalls = array(); + while (++$lastCell < $this->maxCols) { + $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]); + $moreCalls[] = array('cdata', array(''), $call[2]); + $moreCalls[] = array('tablecell_close', array(), $call[2]); + } + $moreCallsLength = count($moreCalls); + if ($moreCallsLength) { + array_splice($this->tableCalls, $key, 0, $moreCalls); + $key += $moreCallsLength; + } + + if ($this->countTableHeadRows == $lastRow) { + array_splice($this->tableCalls, $key+1, 0, array( + array('tablethead_close', array(), $call[2]))); + } + break; + } + } + + // condense cdata + $cnt = count($this->tableCalls); + for ($key = 0; $key < $cnt; $key++) { + if ($this->tableCalls[$key][0] == 'cdata') { + $ckey = $key; + $key++; + while ($this->tableCalls[$key][0] == 'cdata') { + $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0]; + $toDelete[] = $key; + $key++; + } + continue; + } + } + + foreach ($toDelete as $delete) { + unset($this->tableCalls[$delete]); + } + $this->tableCalls = array_values($this->tableCalls); + } +} |