* */ if (!defined('DOKU_INC')) die(); class syntax_plugin_extlist extends DokuWiki_Syntax_Plugin { public function getType() { // Syntax Type return 'container'; } public function getAllowedTypes() { // Allowed Mode Types return array( 'formatting', 'substition', 'disabled', 'protected', ); } public function getPType() { // Paragraph Type return 'block'; } protected $stack = array(); protected $list_class = array(); // store class specified by macro // Enable hierarchical numbering for nested ordered lists protected $olist_level = 0; protected $olist_info = array(); protected $use_div = true; /** * Connect pattern to lexer */ protected $mode; protected $macro_pattern; protected $entry_pattern, $match_pattern, $extra_pattern, $exit_pattern; public function preConnect() { // drop 'syntax_' from class name $this->mode = substr(get_class($this), 7); // macro to specify list class $this->macro_pattern = '\n(?: {2,}|\t{1,})~~(?:dl|ol|ul):[\w -]*?~~'; // list patterns $olist_pattern = '\-?\d+[.:] |-:?'; // ordered list item $ulist_pattern = '\*:?'; // unordered list items $dlist_pattern = ';;?|::?'; // description list item $continue_pattern = '\+:?'; // continued contents to the previous item $this->entry_pattern = '\n(?: {2,}|\t{1,})'.'(?:' . $olist_pattern .'|' . $ulist_pattern .'|' . $dlist_pattern . ') *'; $this->match_pattern = '\n(?: {2,}|\t{1,})'.'(?:' . $olist_pattern .'|' . $ulist_pattern .'|' . $dlist_pattern .'|' . $continue_pattern . ') *'; // continued item content by indentation $this->extra_pattern = '\n(?: {2,}|\t{1,})(?![-*;:?+~])'; $this->exit_pattern = '\n'; } public function connectTo($mode) { $this->Lexer->addEntryPattern('[ \t]*'.$this->entry_pattern, $mode, $this->mode); // macro syntax to specify class for next list [ul|ol|dl] $this->Lexer->addSpecialPattern('[ \t]*'.$this->macro_pattern, $mode, $this->mode); $this->Lexer->addPattern($this->macro_pattern, $this->mode); } public function postConnect() { // subsequent list item $this->Lexer->addPattern($this->match_pattern, $this->mode); $this->Lexer->addPattern(' ::? ', $this->mode); // dt and dd in one line // continued list item content, indented by at least two spaces $this->Lexer->addPattern($this->extra_pattern, $this->mode); // terminate a list block $this->Lexer->addExitPattern($this->exit_pattern, $this->mode); } public function getSort() { // sort number used to determine priority of this mode return 9; // just before listblock (10) } /** * get markup and depth from the match * * @param $match string * @return array */ protected function interpret($match) { // depth: count double spaces indent after '\n' $depth = substr_count(str_replace("\t", ' ', ltrim($match,' ')),' '); $match = trim($match); $m = array('depth' => $depth); // check order list markup with number if (preg_match('/^(-?\d+)([.:])/', $match, $matches)) { $m += array( 'mk' => ($matches[2] == '.') ? '-' : '-:', 'list' => 'ol', 'item' => 'li', 'num' => $matches[1] ); if ($matches[2] == ':') $m += array('p' => 1); } else { $m += array('mk' => $match); switch (substr($match, 0, 1)) { case '' : $m += array('list' => NULL, 'item' => NULL); break; case '+': $m += array('list' => NULL, 'item' => NULL); if ($match == '+:') { $m += array('p' => 1); } else { $m['mk'] = ''; } break; case '-': // ordered list $m += array('list' => 'ol', 'item' => 'li'); if ($match == '-:') $m += array('p' => 1); break; case '*': // unordered list $m += array('list' => 'ul', 'item' => 'li'); if ($match == '*:') $m += array('p' => 1); break; case ';': // description list term $m += array('list' => 'dl', 'item' => 'dt'); if ($match == ';') $m += array('class' => 'compact'); break; case ':': // description list desc $m += array('list' => 'dl', 'item' => 'dd'); if ($match == '::') $m += array('p' => 1); break; } } //error_log('extlist intpret: $m='.var_export($m,1)); return $m; } /** * check whether list type has changed */ private function isListTypeChanged($m0, $m1) { return (strncmp($m0['list'], $m1['list'], 1) !== 0); } /** * create marker for ordered list items */ private function olist_marker($level) { $num = $this->olist_info[$level]; //error_log('olist lv='.$level.' list_class='.$this->list_class['ol'].' num='.$num); // Parenthesized latin small letter marker: ⒜,⒝,⒞, … ,⒵ if (strpos($this->list_class['ol'], 'alphabet') !== false){ $modulus = ($num -1) % 26; $marker = '&#'.(9372 + $modulus).';'; return $marker; } // Hierarchical numbering (default): eg. 1. | 2.1 | 3.2.9 $marker = $this->olist_info[1]; if ($level == 1) { return $marker.'.'; } else { for ($i = 2; $i <= $level; $i++) { $marker .= '.'.$this->olist_info[$i]; } return $marker; } } /** * srore class attribute for lists [ul|ol|dl] specfied by macro pattern * macro_pattern = ~~(?:dl|ol|ul):[\w -]*?~~ */ private function storeListClass($str) { $str = trim($str); $this->list_class[substr($str,2,2)] = trim(substr($str,5,-2)); } /** * helper function to simplify writing plugin calls to the instruction list * first three arguments are passed to function render as $data * Note: this function was used in the DW exttab3 plugin. */ protected function _writeCall($tag, $attr, $state, $pos, $match, $handler) { $handler->addPluginCall($this->getPluginName(), array($state, $tag, $attr), $state, $pos, $match ); } /** * write call to open a list block [ul|ol|dl] */ private function _openList($m, $pos, $match, $handler) { $tag = $m['list']; // start value only for ordered list if ($tag == 'ol') { $attr = isset($m['num']) ? 'start="'.$m['num'].'"' : ''; $this->olist_level++; // increase olist level } else { $attr = null; } // list class $class = 'extlist'; if (isset($this->list_class[$tag])) { // Note: list_class can be empty $class.= ' '.$this->list_class[$tag]; } else { $class.= ' '.$this->getConf($tag.'_class'); } $class = rtrim($class); $attr.= !empty($attr) ? ' ' : ''; $attr.= ' class="'.$class.'"'; $this->_writeCall($tag,$attr,DOKU_LEXER_ENTER, $pos,$match,$handler); } /** * write call to close a list block [ul|ol|dl] */ private function _closeList($m, $pos, $match, $handler) { $tag = $m['list']; if ($tag == 'ol') { $this->olist_level--; // reduce olist level } $this->_writeCall($tag,'',DOKU_LEXER_EXIT, $pos,$match,$handler); } /** * write call to open a list item [li|dt|dd] */ private function _openItem($m, $pos, $match, $handler) { $tag = $m['item']; switch ($m['mk']) { case '-': case '-:': // prepare hierarchical marker for nested ordered list item $this->olist_info[$this->olist_level] = $m['num']; $lv = $this->olist_level; $attr = ' value="'.$m['num'].'"'; $attr.= ' data-marker="'.$this->olist_marker($lv).'"'; break; case ';': $attr = 'class="'.$m['class'].'"'; break; default: $attr = ''; } $this->_writeCall($tag,$attr,DOKU_LEXER_ENTER, $pos,$match,$handler); } /** * write call to close a list item [li|dt|dd] */ private function _closeItem($m, $pos, $match, $handler) { $tag = $m['item']; $this->_writeCall($tag,'',DOKU_LEXER_EXIT, $pos,$match,$handler); } /** * write call to open inner wrapper [div|span] */ private function _openWrapper($m, $pos, $match, $handler) { switch ($m['mk']) { case ';': // dl dt case ';;': // dl dt, explicitly no-compact $tag = 'span'; $attr = ''; break; case ':': // dl dd case '::': // dl dd p return; break; default: if (!$this->use_div) return; $tag = 'div'; $attr = 'class="li"'; break; } $this->_writeCall($tag,$attr,DOKU_LEXER_ENTER, $pos,$match,$handler); } /** * write call to close inner wrapper [div|span] */ private function _closeWrapper($m, $pos, $match, $handler) { switch ($m['mk']) { case ';': // dl dt case ';;': // dl dt, explicitly no-compact $tag = 'span'; break; case ':': // dl dd case '::': // dl dd p return; break; default: if (!$this->use_div) return; $tag = 'div'; break; } $this->_writeCall($tag,'',DOKU_LEXER_EXIT, $pos,$match,$handler); } /** * write call to open paragraph (p tag) */ private function _openParagraph($pos, $match, $handler) { $this->_writeCall('p','',DOKU_LEXER_ENTER, $pos,$match,$handler); } /** * write call to close paragraph (p tag) */ private function _closeParagraph($pos, $match, $handler) { $this->_writeCall('p','',DOKU_LEXER_EXIT, $pos,$match,$handler); } /** * Handle the match */ public function handle($match, $state, $pos, Doku_Handler $handler) { switch ($state) { case DOKU_LEXER_SPECIAL: // specify class attribute for lists [ul|ol|dl] $this->storeListClass($match); break; case DOKU_LEXER_ENTER: $m1 = $this->interpret($match); if (($m1['list'] == 'ol') && !isset($m1['num'])) { $m1['num'] = 1; } // open list tag [ul|ol|dl] $this->_openList($m1, $pos,$match,$handler); // open item tag [li|dt|dd] $this->_openItem($m1, $pos,$match,$handler); // open inner wrapper [div|span] $this->_openWrapper($m1, $pos,$match,$handler); // open p if necessary if (isset($m1['p'])) $this->_openParagraph($pos,$match,$handler); // add to stack array_push($this->stack, $m1); break; case DOKU_LEXER_UNMATCHED: // cdata --- use base() as _writeCall() is prefixed for private/protected $handler->base($match, $state, $pos); break; case DOKU_LEXER_EXIT: // clear list_class $this->list_class = array(); // do not break here! case DOKU_LEXER_MATCHED: // specify class attribute for lists [ul|ol|dl] if (substr($match, -2) == '~~') { $this->storeListClass($match); break; } // retrieve previous list item from stack $m0 = array_pop($this->stack); $m1 = $this->interpret($match); // set m1 depth if dt and dd are in one line if (($m1['depth'] == 0) && ($m0['item'] == 'dt')) { $m1['depth'] = $m0['depth']; } // continued list item content, indented by at least two spaces if (empty($m1['mk']) && ($m1['depth'] > 0)) { // !!EXPERIMENTAL SCRIPTIO CONTINUA concerns!! // replace indent to single space, but leave it for LineBreak2 plugin $handler->base("\n", DOKU_LEXER_UNMATCHED, $pos); // restore stack array_push($this->stack, $m0); break; } // close p if necessary if (isset($m0['p'])) $this->_closeParagraph($pos,$match,$handler); // close inner wrapper [div|span] if necessary if ($m1['mk'] == '+:') { // Paragraph markup if ($m0['depth'] > $m1['depth']) { $this->_closeWrapper($m0, $pos,$match,$handler); } else { // new paragraph can not be deeper than previous depth // fix current depth quietly $m1['depth'] = min($m0['depth'], $m1['depth']); } // fix previous p type $m0['p'] = 1; } else { // List item markup if ($m0['depth'] >= $m1['depth']) { $this->_closeWrapper($m0, $pos,$match,$handler); } } // List item becomes shallower - close deeper list while (isset($m0['depth']) && ($m0['depth'] > $m1['depth'])) { // close item [li|dt|dd] $this->_closeItem($m0, $pos,$match,$handler); // close list [ul|ol|dl] $this->_closeList($m0, $pos,$match,$handler); $m0 = array_pop($this->stack); } // Break out of switch structure if end of list block if ($state == DOKU_LEXER_EXIT) { break; } // Paragraph markup if ($m1['mk'] == '+:') { $this->_openParagraph($pos,$match,$handler); $m1['depth'] = $m0['depth']; $m1 = $m0 + array('p' => 1); // restore stack array_push($this->stack, $m1); break; } // List item markup if ($m0['depth'] < $m1['depth']) { // list becomes deeper // restore stack array_push($this->stack, $m0); } else if ($m0['depth'] == $m1['depth']) { // close item [li|dt|dd] $this->_closeItem($m0, $pos,$match,$handler); // close list [ul|ol|dl] if necessary if ($this->isListTypeChanged($m0, $m1)) { $this->_closeList($m0, $pos,$match,$handler); $m0['num'] = 0; } } // open list [ul|ol|dl] if necessary if (($m0['depth'] < $m1['depth']) || (isset($m0['num']) && ($m0['num'] === 0))) { if (isset($m1['num']) && !is_numeric($m1['num'])) $m1['num'] = 1; $this->_openList($m1, $pos,$match,$handler); } else { if (isset($m1['num']) && !is_numeric($m1['num'])) $m1['num'] = $m0['num'] +1; } // open item [li|dt|dd] $this->_openItem($m1, $pos,$match,$handler); // open inner wrapper [div|span] $this->_openWrapper($m1, $pos,$match,$handler); // open p if necessary if (isset($m1['p'])) $this->_openParagraph($pos,$match,$handler); // add to stack array_push($this->stack, $m1); } // end of switch return false; } /** * Create output */ public function render($format, Doku_Renderer $renderer, $data) { switch ($format) { case 'xhtml': return $this->render_xhtml($renderer, $data); //case 'latex': // $latex = $this->loadHelper('extlist_latex'); // return $latex->render($renderer, $data); //case 'odt': // $odt = $this->loadHelper('extlist_odt'); // return $odt->render($renderer, $data); default: return false; } } /** * Create xhtml output */ protected function render_xhtml(Doku_Renderer $renderer, $data) { list($state, $tag, $attr) = $data; switch ($state) { case DOKU_LEXER_ENTER: // open tag $renderer->doc.= $this->_open($tag, $attr); break; case DOKU_LEXER_MATCHED: // defensive, shouldn't occur case DOKU_LEXER_UNMATCHED: $renderer->cdata($tag); break; case DOKU_LEXER_EXIT: // close tag $renderer->doc.= $this->_close($tag); break; } return true; } /** * open a tag, a utility for render_xhtml() */ protected function _open($tag, $attr = null) { if (!empty($attr)) $attr = ' '.$attr; list($before, $after) = $this->_tag_indent($tag); return $before.'<'.$tag.$attr.'>'.$after; } /** * close a tag, a utility for render_xhtml() */ protected function _close($tag) { list($before, $after) = $this->_tag_indent('/'.$tag); return $before.''.$after; } /** * prefix and suffix of html tags * * Initialize this array only once instead of each time the method _tag_indent() * is called. */ protected $indent = [ 'ol' => [NL,NL], '/ol' => ['',NL], 'ul' => [NL,NL], '/ul' => ['',NL], 'dl' => [NL,NL], '/dl' => ['',NL], 'li' => [' ',''], '/li' => ['',NL], 'dt' => [' ',''], '/dt' => ['',NL], 'dd' => [' ',NL], '/dd' => ['',NL], 'p' => [NL,''], '/p' => ['',NL], 'div' => [NL,''], '/div' => ['',NL], ]; /** * indent tags for readability if HTML source * * @param string $tag tag name * @return array */ private function _tag_indent($tag) { if (array_key_exists($tag, $this->indent)) return $this->indent[$tag]; return ['','']; } }