1<?php 2 3// must be run within Dokuwiki 4if(!defined('DOKU_INC')) die(); 5 6require_once DOKU_PLUGIN . 'latexport/implementation/internal_link.php'; 7require_once DOKU_PLUGIN . 'latexport/implementation/decorator.php'; 8 9/** 10 * Renders internallinks that are alone in an item of unordered lists as 11 * sub-document inclusions. 12 * 13 * Latexport Plugin: Exports to latex 14 * 15 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 16 * @author Jean-Michel Gonet <jmgonet@yahoo.com> 17 */ 18class DecoratorIncluder extends Decorator { 19 20 const NOT_IN_LIST = 999; 21 /** State machine: in a list. */ 22 const IN_LIST = 1000; 23 /** State machine: in an item of a list. */ 24 const IN_ITEM = 1001; 25 /** State machine: in the content of a list item. */ 26 const IN_CONTENT = 1002; 27 /** State machine: in the content of a list item, which contains mixed elements */ 28 const IN_ITEM_MIXED = 1003; 29 /** State machine: in the content of a list item, which contains only internal links.*/ 30 const IN_CONTENT_INTERNAL_LINK = 1004; 31 /** State machine: This list is so mixed up that we just render it. */ 32 const IN_LIST_NESTED = 1005; 33 34 /** State of the state machine. */ 35 private $state; 36 /** Level of the most recent heading. */ 37 private $headingLevel = 0; 38 39 /** Contains the page found in the last processed internal link. */ 40 private $internalLinkToInclude; 41 /** The list of internal links in the current list. */ 42 private $internalLinksToInclude; 43 /** A queue of links to include. */ 44 private $includes; 45 46 /** If mixed content is found, then we'll need to open the list. */ 47 private $needToOpenList; 48 /** If mixed content is found, then we'll need to open the item. */ 49 private $needToOpenItem; 50 /** If mixed content is found, then we'll need to open the content. */ 51 private $needToOpenContent; 52 /** If some items have content, it is necessary to render the listu_close. */ 53 private $someItemsHaveMixedContent; 54 55 56 /** To keep track of nested lists. */ 57 private $listLevel; 58 /** Handy to log errors. */ 59 private $pageId; 60 61 /** 62 * Class constructor. 63 * @param includes A queue of links to include. 64 * @param decorator To send further the decorated events. 65 */ 66 function __construct($includes, $decorator) { 67 parent::__construct($decorator); 68 $this->includes = $includes; 69 $this->state = DecoratorIncluder::NOT_IN_LIST; 70 $this->headingLevel = 0; 71 $this->listLevel = 0; 72 } 73 74 /** 75 * Remembers the current page identifier to log useful error messages. 76 */ 77 function document_start($pageId = null, $recursionLevel = 0) { 78 $this->decorator->document_start($pageId, $recursionLevel); 79 $this->pageId = $pageId; 80 } 81 82 /** 83 * Unordered list item starting with a link, includes the destination page, 84 * using the current level of heading as the base level. 85 */ 86 function header($text, $level, $pos) { 87 $this->headingLevel = $level; 88 $this->decorator->header($text, $level, $pos); 89 } 90 91 /** 92 * Receives the unordered list open notification. 93 * If 94 */ 95 function listu_open() { 96 switch($this->state) { 97 case DecoratorIncluder::NOT_IN_LIST: 98 $this->listLevel = 1; 99 $this->needToOpenList = true; 100 $this->state = DecoratorIncluder::IN_LIST; 101 $this->internalLinksToInclude = []; 102 $this->someItemsHaveMixedContent = false; 103 break; 104 105 case DecoratorIncluder::IN_CONTENT: 106 case DecoratorIncluder::IN_ITEM_MIXED: 107 $this->thereIsMixedContentInThisItem(); 108 $this->decorator->listu_open(); 109 $this->listLevel++; 110 $this->state = DecoratorIncluder::IN_LIST_NESTED; 111 break; 112 113 default: 114 trigger_error("$this->pageId: listu_open unexpected $this->state"); 115 } 116 } 117 118 /** 119 * Open a list item 120 * 121 * @param int $level the nesting level 122 * @param bool $node true when a node; false when a leaf 123 */ 124 function listitem_open($level,$node=false) { 125 126 switch($this->state) { 127 case DecoratorIncluder::NOT_IN_LIST: 128 case DecoratorIncluder::IN_LIST_NESTED: 129 $this->decorator->listitem_open($level, $node); 130 break; 131 132 case DecoratorIncluder::IN_LIST: 133 $this->state = DecoratorIncluder::IN_ITEM; 134 $this->needToOpenItem = true; 135 break; 136 137 default: 138 trigger_error("$this->pageId: listitem_open unexpected - $this->state"); 139 } 140 } 141 142 /** 143 * Start the content of a list item 144 */ 145 function listcontent_open() { 146 switch($this->state) { 147 case DecoratorIncluder::NOT_IN_LIST: 148 case DecoratorIncluder::IN_LIST_NESTED: 149 $this->decorator->listcontent_open(); 150 break; 151 152 case DecoratorIncluder::IN_ITEM: 153 $this->state = DecoratorIncluder::IN_CONTENT; 154 $this->needToOpenContent = true; 155 break; 156 default: 157 trigger_error("$this->pageId: listcontent_open unexpected - $this->state"); 158 } 159 } 160 161 /** 162 * Stop the content of a list item 163 */ 164 function listcontent_close() { 165 switch($this->state) { 166 case DecoratorIncluder::IN_CONTENT_INTERNAL_LINK: 167 $this->internalLinksToInclude[] = $this->internalLinkToInclude; 168 $this->state = DecoratorIncluder::IN_ITEM; 169 break; 170 171 case DecoratorIncluder::IN_ITEM_MIXED: 172 $this->decorator->listcontent_close(); 173 break; 174 175 case DecoratorIncluder::NOT_IN_LIST: 176 case DecoratorIncluder::IN_LIST_NESTED: 177 $this->decorator->listcontent_close(); 178 break; 179 180 default: 181 trigger_error("$this->pageId: listcontent_close unexpected - $this->state"); 182 } 183 } 184 185 /** 186 * Close a list item 187 */ 188 function listitem_close() { 189 switch($this->state) { 190 case DecoratorIncluder::NOT_IN_LIST: 191 case DecoratorIncluder::IN_LIST_NESTED: 192 $this->decorator->listitem_close(); 193 break; 194 195 case DecoratorIncluder::IN_ITEM_MIXED: 196 $this->decorator->listitem_close(); 197 $this->state = DecoratorIncluder::IN_LIST; 198 break; 199 200 case DecoratorIncluder::IN_ITEM: 201 $this->state = DecoratorIncluder::IN_LIST; 202 break; 203 204 default: 205 trigger_error("$this->pageId: listitem_close unexpected - $this->state"); 206 } 207 } 208 209 /** 210 * Close an unordered list 211 */ 212 function listu_close() { 213 switch($this->state) { 214 case DecoratorIncluder::IN_LIST: 215 // Creates an input for each internal link to include, and stores the 216 // destination pages in the queue: 217 foreach($this->internalLinksToInclude as $internalLink) { 218 $this->includes->push($internalLink); 219 $this->decorator->input($this->texifyPageId($internalLink->getLink())); 220 } 221 // Render the list closing if necessary: 222 if ($this->someItemsHaveMixedContent) { 223 $this->decorator->listu_close(); 224 } 225 // Not in list any more: 226 $this->state = DecoratorIncluder::NOT_IN_LIST; 227 $this->listLevel = 0; 228 break; 229 230 case DecoratorIncluder::IN_LIST_NESTED: 231 $this->decorator->listu_close(); 232 $this->listLevel--; 233 if ($this->listLevel == 0) { 234 $this->state = DecoratorIncluder::NOT_IN_LIST; 235 } 236 break; 237 238 case DecoratorIncluder::NOT_IN_LIST: 239 $this->decorator->listu_close(); 240 break; 241 242 default: 243 trigger_error("$this->pageId: listu_close unexpected - $this->state"); 244 } 245 } 246 247 /** 248 * Receives a wiki internal link. 249 * Internal links at the very beginning of an unordered item include 250 * the destination page. If they are in any other position, they are 251 * rendered normally. 252 * @param string $link page ID to link to. eg. 'wiki:syntax' 253 * @param string|array $title name for the link, array for media file 254 */ 255 function internallink($link, $title = null) { 256 switch($this->state) { 257 258 case DecoratorIncluder::IN_CONTENT: 259 $this->internalLinkToInclude = new InternalLink($link, $this->headingLevel, $title); 260 $this->state = DecoratorIncluder::IN_CONTENT_INTERNAL_LINK; 261 break; 262 263 case DecoratorIncluder::IN_CONTENT_INTERNAL_LINK: 264 $this->internalLinkToInclude = null; 265 $this->decorator->internallink($link, $title); 266 $this->state = DecoratorIncluder::IN_ITEM_MIXED; 267 break; 268 269 default: 270 $this->decorator->internallink($link, $title); 271 break; 272 } 273 } 274 275 /** 276 * Renders plain text. 277 */ 278 function cdata($text) { 279 280 // It is very common to place spaces between the unordered list bullet and the content: 281 if ($this->state == DecoratorIncluder::IN_CONTENT) { 282 // We ignore those whites: 283 if (ctype_space($text)) { 284 return; 285 } 286 } 287 288 // Any other kind of content is propagated: 289 $this->thereIsMixedContentInThisItem(); 290 $this->decorator->cdata($text); 291 } 292 293 /** 294 * Any other command means mixed content in this item. 295 */ 296 function any_command() { 297 $this->thereIsMixedContentInThisItem(); 298 } 299 300 private function thereIsMixedContentInThisItem() { 301 $this->someItemsHaveMixedContent = true; 302 303 if ($this->state != DecoratorIncluder::NOT_IN_LIST) { 304 if ($this->needToOpenList) { 305 $this->decorator->listu_open(); 306 $this->needToOpenList = false; 307 } 308 309 if ($this->needToOpenItem) { 310 $this->decorator->listitem_open($this->listLevel); 311 $this->needToOpenItem = false; 312 } 313 314 if ($this->needToOpenContent) { 315 $this->decorator->listcontent_open(); 316 $this->needToOpenContent = false; 317 } 318 319 $this->state = DecoratorIncluder::IN_ITEM_MIXED; 320 } 321 } 322} 323