diff options
author | Andreas Gohr <gohr@cosmocode.de> | 2019-10-10 09:55:14 +0200 |
---|---|---|
committer | Andreas Gohr <gohr@cosmocode.de> | 2019-10-10 09:55:14 +0200 |
commit | 31a58aba4c24b34c34ad5764d1a35b7c398c3a2c (patch) | |
tree | 7f4d1546fbb69863a7d366fc1ff647f784853b68 /inc/Parsing/Handler | |
parent | af7ba5aa0bd10fc0ad9ef983006305b4c5a8ed42 (diff) | |
parent | c0c77cd20b23921c9e893bb70b99f38be153875a (diff) | |
download | dokuwiki-31a58aba4c24b34c34ad5764d1a35b7c398c3a2c.tar.gz dokuwiki-31a58aba4c24b34c34ad5764d1a35b7c398c3a2c.zip |
Merge branch 'psr2'
* psr2: (160 commits)
fixed merge error
Moved parts of the Asian word handling to its own class
ignore snake_case error of substr_replace
fixed some line length errors
ignore PSR2 in the old form class
fix PSR2 error in switch statement
replaced deprecated utf8 functions
formatting cleanup
mark old utf8 functions deprecated
some more PSR2 cleanup
Some cleanup for the UTF-8 stuff
Moved all utf8 methods to their own namespaced classes
Create separate table files for UTF-8 handling
Ignore mixed concerns in loader
Use type safe comparisons in loader
Remove obsolete include
adjust phpcs exclude patterns for new plugin classes
🚚 Move Subscription class to deprecated.php
♻️ Split up ChangesSubscriptionSender into multiple classes
Minor optimizations in PluginController
...
Diffstat (limited to 'inc/Parsing/Handler')
-rw-r--r-- | inc/Parsing/Handler/Block.php | 211 | ||||
-rw-r--r-- | inc/Parsing/Handler/CallWriter.php | 40 | ||||
-rw-r--r-- | inc/Parsing/Handler/CallWriterInterface.php | 30 | ||||
-rw-r--r-- | inc/Parsing/Handler/Lists.php | 213 | ||||
-rw-r--r-- | inc/Parsing/Handler/Nest.php | 83 | ||||
-rw-r--r-- | inc/Parsing/Handler/Preformatted.php | 76 | ||||
-rw-r--r-- | inc/Parsing/Handler/Quote.php | 110 | ||||
-rw-r--r-- | inc/Parsing/Handler/ReWriterInterface.php | 29 | ||||
-rw-r--r-- | inc/Parsing/Handler/Table.php | 345 |
9 files changed, 1137 insertions, 0 deletions
diff --git a/inc/Parsing/Handler/Block.php b/inc/Parsing/Handler/Block.php new file mode 100644 index 000000000..4cfa686d4 --- /dev/null +++ b/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/inc/Parsing/Handler/CallWriter.php b/inc/Parsing/Handler/CallWriter.php new file mode 100644 index 000000000..2457143ed --- /dev/null +++ b/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/inc/Parsing/Handler/CallWriterInterface.php b/inc/Parsing/Handler/CallWriterInterface.php new file mode 100644 index 000000000..1ade7c060 --- /dev/null +++ b/inc/Parsing/Handler/CallWriterInterface.php @@ -0,0 +1,30 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +interface CallWriterInterface +{ + /** + * Add a call to our call list + * + * @param $call the call to be added + */ + public function writeCall($call); + + /** + * Append a list of calls to our call list + * + * @param $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/inc/Parsing/Handler/Lists.php b/inc/Parsing/Handler/Lists.php new file mode 100644 index 000000000..c4428fe46 --- /dev/null +++ b/inc/Parsing/Handler/Lists.php @@ -0,0 +1,213 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +class Lists implements ReWriterInterface +{ + + /** @var CallWriterInterface original call writer */ + protected $callWriter; + + protected $calls = array(); + protected $listCalls = array(); + protected $listStack = array(); + + protected $initialDepth = 0; + + const NODE = 1; + + + /** @inheritdoc */ + public function __construct(CallWriterInterface $CallWriter) + { + $this->callWriter = $CallWriter; + } + + /** @inheritdoc */ + public function writeCall($call) + { + $this->calls[] = $call; + } + + /** + * @inheritdoc + * Probably not needed but just in case... + */ + public function writeCalls($calls) + { + $this->calls = array_merge($this->calls, $calls); + } + + /** @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/inc/Parsing/Handler/Nest.php b/inc/Parsing/Handler/Nest.php new file mode 100644 index 000000000..b0044a3cb --- /dev/null +++ b/inc/Parsing/Handler/Nest.php @@ -0,0 +1,83 @@ +<?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 implements ReWriterInterface +{ + + /** @var CallWriterInterface original CallWriter */ + protected $callWriter; + + protected $calls = array(); + 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") + { + $this->callWriter = $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; + } + + 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/inc/Parsing/Handler/Preformatted.php b/inc/Parsing/Handler/Preformatted.php new file mode 100644 index 000000000..a668771a7 --- /dev/null +++ b/inc/Parsing/Handler/Preformatted.php @@ -0,0 +1,76 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +class Preformatted implements ReWriterInterface +{ + + /** @var CallWriterInterface original call writer */ + protected $callWriter; + + protected $calls = array(); + protected $pos; + protected $text =''; + + /** + * @inheritdoc + */ + public function __construct(CallWriterInterface $CallWriter) + { + $this->callWriter = $CallWriter; + } + + /** @inheritdoc */ + public function writeCall($call) + { + $this->calls[] = $call; + } + + /** + * @inheritdoc + * Probably not needed but just in case... + */ + public function writeCalls($calls) + { + $this->calls = array_merge($this->calls, $calls); + } + + /** @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/inc/Parsing/Handler/Quote.php b/inc/Parsing/Handler/Quote.php new file mode 100644 index 000000000..a786d10c0 --- /dev/null +++ b/inc/Parsing/Handler/Quote.php @@ -0,0 +1,110 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +class Quote implements ReWriterInterface +{ + + /** @var CallWriterInterface original CallWriter */ + protected $callWriter; + + protected $calls = array(); + + protected $quoteCalls = array(); + + /** @inheritdoc */ + public function __construct(CallWriterInterface $CallWriter) + { + $this->callWriter = $CallWriter; + } + + /** @inheritdoc */ + public function writeCall($call) + { + $this->calls[] = $call; + } + + /** + * @inheritdoc + * + * Probably not needed but just in case... + */ + public function writeCalls($calls) + { + $this->calls = array_merge($this->calls, $calls); + } + + /** @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; + } + + protected function getDepth($marker) + { + preg_match('/>{1,}/', $marker, $matches); + $quoteLength = strlen($matches[0]); + return $quoteLength; + } +} diff --git a/inc/Parsing/Handler/ReWriterInterface.php b/inc/Parsing/Handler/ReWriterInterface.php new file mode 100644 index 000000000..13f7b48e3 --- /dev/null +++ b/inc/Parsing/Handler/ReWriterInterface.php @@ -0,0 +1,29 @@ +<?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. + */ +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(); +} diff --git a/inc/Parsing/Handler/Table.php b/inc/Parsing/Handler/Table.php new file mode 100644 index 000000000..6759ea798 --- /dev/null +++ b/inc/Parsing/Handler/Table.php @@ -0,0 +1,345 @@ +<?php + +namespace dokuwiki\Parsing\Handler; + +class Table implements ReWriterInterface +{ + + /** @var CallWriterInterface original CallWriter */ + protected $callWriter; + + protected $calls = array(); + 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 __construct(CallWriterInterface $CallWriter) + { + $this->callWriter = $CallWriter; + } + + /** @inheritdoc */ + public function writeCall($call) + { + $this->calls[] = $call; + } + + /** + * @inheritdoc + * Probably not needed but just in case... + */ + public function writeCalls($calls) + { + $this->calls = array_merge($this->calls, $calls); + } + + /** @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); + } +} |