*/ class DecoratorIncluder extends Decorator { const NOT_IN_LIST = 999; /** State machine: in a list. */ const IN_LIST = 1000; /** State machine: in an item of a list. */ const IN_ITEM = 1001; /** State machine: in the content of a list item. */ const IN_CONTENT = 1002; /** State machine: in the content of a list item, which contains mixed elements */ const IN_ITEM_MIXED = 1003; /** State machine: in the content of a list item, which contains only internal links.*/ const IN_CONTENT_INTERNAL_LINK = 1004; /** State machine: This list is so mixed up that we just render it. */ const IN_LIST_NESTED = 1005; /** State of the state machine. */ private $state; /** Level of the most recent heading. */ private $headingLevel = 0; /** Contains the page found in the last processed internal link. */ private $internalLinkToInclude; /** The list of internal links in the current list. */ private $internalLinksToInclude; /** A queue of links to include. */ private $includes; /** If mixed content is found, then we'll need to open the list. */ private $needToOpenList; /** If mixed content is found, then we'll need to open the item. */ private $needToOpenItem; /** If mixed content is found, then we'll need to open the content. */ private $needToOpenContent; /** If some items have content, it is necessary to render the listu_close. */ private $someItemsHaveMixedContent; /** To keep track of nested lists. */ private $listLevel; /** Handy to log errors. */ private $pageId; /** * Class constructor. * @param includes A queue of links to include. * @param decorator To send further the decorated events. */ function __construct($includes, $decorator) { parent::__construct($decorator); $this->includes = $includes; $this->state = DecoratorIncluder::NOT_IN_LIST; $this->headingLevel = 0; $this->listLevel = 0; } /** * Remembers the current page identifier to log useful error messages. */ function document_start($pageId = null, $recursionLevel = 0) { $this->decorator->document_start($pageId, $recursionLevel); $this->pageId = $pageId; } /** * Unordered list item starting with a link, includes the destination page, * using the current level of heading as the base level. */ function header($text, $level, $pos) { $this->headingLevel = $level; $this->decorator->header($text, $level, $pos); } /** * Receives the unordered list open notification. * If */ function listu_open() { switch($this->state) { case DecoratorIncluder::NOT_IN_LIST: $this->listLevel = 1; $this->needToOpenList = true; $this->state = DecoratorIncluder::IN_LIST; $this->internalLinksToInclude = []; $this->someItemsHaveMixedContent = false; break; case DecoratorIncluder::IN_CONTENT: case DecoratorIncluder::IN_ITEM_MIXED: $this->thereIsMixedContentInThisItem(); $this->decorator->listu_open(); $this->listLevel++; $this->state = DecoratorIncluder::IN_LIST_NESTED; break; default: trigger_error("$this->pageId: listu_open unexpected $this->state"); } } /** * Open a list item * * @param int $level the nesting level * @param bool $node true when a node; false when a leaf */ function listitem_open($level,$node=false) { switch($this->state) { case DecoratorIncluder::NOT_IN_LIST: case DecoratorIncluder::IN_LIST_NESTED: $this->decorator->listitem_open($level, $node); break; case DecoratorIncluder::IN_LIST: $this->state = DecoratorIncluder::IN_ITEM; $this->needToOpenItem = true; break; default: trigger_error("$this->pageId: listitem_open unexpected - $this->state"); } } /** * Start the content of a list item */ function listcontent_open() { switch($this->state) { case DecoratorIncluder::NOT_IN_LIST: case DecoratorIncluder::IN_LIST_NESTED: $this->decorator->listcontent_open(); break; case DecoratorIncluder::IN_ITEM: $this->state = DecoratorIncluder::IN_CONTENT; $this->needToOpenContent = true; break; default: trigger_error("$this->pageId: listcontent_open unexpected - $this->state"); } } /** * Stop the content of a list item */ function listcontent_close() { switch($this->state) { case DecoratorIncluder::IN_CONTENT_INTERNAL_LINK: $this->internalLinksToInclude[] = $this->internalLinkToInclude; $this->state = DecoratorIncluder::IN_ITEM; break; case DecoratorIncluder::IN_ITEM_MIXED: $this->decorator->listcontent_close(); break; case DecoratorIncluder::NOT_IN_LIST: case DecoratorIncluder::IN_LIST_NESTED: $this->decorator->listcontent_close(); break; default: trigger_error("$this->pageId: listcontent_close unexpected - $this->state"); } } /** * Close a list item */ function listitem_close() { switch($this->state) { case DecoratorIncluder::NOT_IN_LIST: case DecoratorIncluder::IN_LIST_NESTED: $this->decorator->listitem_close(); break; case DecoratorIncluder::IN_ITEM_MIXED: $this->decorator->listitem_close(); $this->state = DecoratorIncluder::IN_LIST; break; case DecoratorIncluder::IN_ITEM: $this->state = DecoratorIncluder::IN_LIST; break; default: trigger_error("$this->pageId: listitem_close unexpected - $this->state"); } } /** * Close an unordered list */ function listu_close() { switch($this->state) { case DecoratorIncluder::IN_LIST: // Creates an input for each internal link to include, and stores the // destination pages in the queue: foreach($this->internalLinksToInclude as $internalLink) { $this->includes->push($internalLink); $this->decorator->input($this->texifyPageId($internalLink->getLink())); } // Render the list closing if necessary: if ($this->someItemsHaveMixedContent) { $this->decorator->listu_close(); } // Not in list any more: $this->state = DecoratorIncluder::NOT_IN_LIST; $this->listLevel = 0; break; case DecoratorIncluder::IN_LIST_NESTED: $this->decorator->listu_close(); $this->listLevel--; if ($this->listLevel == 0) { $this->state = DecoratorIncluder::NOT_IN_LIST; } break; case DecoratorIncluder::NOT_IN_LIST: $this->decorator->listu_close(); break; default: trigger_error("$this->pageId: listu_close unexpected - $this->state"); } } /** * Receives a wiki internal link. * Internal links at the very beginning of an unordered item include * the destination page. If they are in any other position, they are * rendered normally. * @param string $link page ID to link to. eg. 'wiki:syntax' * @param string|array $title name for the link, array for media file */ function internallink($link, $title = null) { switch($this->state) { case DecoratorIncluder::IN_CONTENT: $this->internalLinkToInclude = new InternalLink($link, $this->headingLevel, $title); $this->state = DecoratorIncluder::IN_CONTENT_INTERNAL_LINK; break; case DecoratorIncluder::IN_CONTENT_INTERNAL_LINK: $this->internalLinkToInclude = null; $this->decorator->internallink($link, $title); $this->state = DecoratorIncluder::IN_ITEM_MIXED; break; default: $this->decorator->internallink($link, $title); break; } } /** * Renders plain text. */ function cdata($text) { // It is very common to place spaces between the unordered list bullet and the content: if ($this->state == DecoratorIncluder::IN_CONTENT) { // We ignore those whites: if (ctype_space($text)) { return; } } // Any other kind of content is propagated: $this->thereIsMixedContentInThisItem(); $this->decorator->cdata($text); } /** * Any other command means mixed content in this item. */ function any_command() { $this->thereIsMixedContentInThisItem(); } private function thereIsMixedContentInThisItem() { $this->someItemsHaveMixedContent = true; if ($this->state != DecoratorIncluder::NOT_IN_LIST) { if ($this->needToOpenList) { $this->decorator->listu_open(); $this->needToOpenList = false; } if ($this->needToOpenItem) { $this->decorator->listitem_open($this->listLevel); $this->needToOpenItem = false; } if ($this->needToOpenContent) { $this->decorator->listcontent_open(); $this->needToOpenContent = false; } $this->state = DecoratorIncluder::IN_ITEM_MIXED; } } }