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