1<?php
2
3require_once DOKU_PLUGIN.'odt/ODT/elements/ODTStateElement.php';
4require_once DOKU_PLUGIN.'odt/ODT/elements/ODTRoot.php';
5require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementSpan.php';
6require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementParagraph.php';
7require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementList.php';
8require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementListItem.php';
9require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementListHeader.php';
10require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementTable.php';
11require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementTableColumn.php';
12require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementTableRow.php';
13require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementTableCell.php';
14require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementTableHeaderCell.php';
15require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementFrame.php';
16require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementTextBox.php';
17require_once DOKU_PLUGIN.'odt/ODT/elements/ODTElementNote.php';
18require_once DOKU_PLUGIN.'odt/ODT/css/cssdocument.php';
19
20/**
21 * ODTState: class for maintaining the ODT state stack.
22 *
23 * In general this is a setter/getter class for ODT states.
24 * The intention is to get rid of some global state variables.
25 * Especially the global error-prone $in_paragraph which easily causes
26 * a document to become invalid if once set wrong. Now each state/element
27 * can set their own instance of $in_paragraph which hopefully makes it use
28 * a bit safer. E.g. for a new table-cell or list-item it can be set to false
29 * because they allow creation of a new paragraph. On leave() we throw the
30 * current state variables away and are safe back from where we came from.
31 * So we also don't need to worry about correct re-initialization of global
32 * variables anymore.
33 *
34 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
35 * @author  LarsDW223
36 */
37class ODTState
38{
39    // The ODT document to which this state belongs
40    protected $document = NULL;
41    protected $stack = array();
42    protected $size = 0;
43    protected $element_counter = array();
44
45    /**
46     * Constructor. Set initial 'root' state.
47     */
48    public function __construct() {
49        // Stack for maintaining our ODT elements
50        $this->stack [$this->size] = new ODTElementRoot();
51        $this->size++;
52    }
53
54    /**
55     * Get current list item.
56     * If the function returns NULL then that means that we are
57     * currently not in a list item.
58     *
59     * @return ODTStateElement|NULL
60     */
61    public function getCurrentListItem() {
62        return $this->findClosestWithClass ('list-item');
63    }
64
65    /**
66     * Get current frame.
67     * If the function returns NULL then that means that we are
68     * currently not in a frame.
69     *
70     * @return ODTStateElement|NULL
71     */
72    public function getCurrentFrame() {
73        return $this->findClosestWithClass ('frame');
74    }
75
76    /**
77     * Get current list.
78     * If the function returns NULL then that means that we are
79     * currently not in a list.
80     *
81     * @return ODTStateElement|NULL
82     */
83    public function getCurrentList() {
84        return $this->findClosestWithClass ('list');
85    }
86
87    /**
88     * Get current paragraph.
89     * If the function returns NULL then that means that we are
90     * currently not in a paragraph.
91     *
92     * @return ODTStateElement|NULL
93     */
94    public function getCurrentParagraph() {
95        // Only search for the paragraph if the current element tells
96        // us that we are in one. Otherwise we may find the paragraph
97        // around this current element which might lead to double
98        // closing of a paragraph == invalid/broken ODT document!
99        if ($this->getInParagraph()) {
100            return $this->findClosestWithClass ('paragraph');
101        }
102        return NULL;
103    }
104
105    /**
106     * Get current table.
107     * If the function returns NULL then that means that we are
108     * currently not in a table.
109     *
110     * @return ODTStateElement|NULL
111     */
112    public function getCurrentTable() {
113        return $this->findClosestWithClass ('table');
114    }
115
116    /**
117     * Enter a new state with element name $element and class $clazz.
118     * E.g. 'text:p' and 'paragraph'.
119     *
120     * @param string $element
121     * @param string $clazz
122     */
123    public function enter(ODTStateElement $element, $attributes=NULL) {
124        if ( !isset($element) ) {
125            return;
126        }
127        $name = $element->getElementName();
128
129        // Increase the counter for that element
130        if ( !isset($this->element_counter [$name]) ) {
131            $this->element_counter [$name] = 1;
132        } else {
133            $this->element_counter [$name]++;
134        }
135        $element->setCount($this->element_counter [$name]);
136
137        // Get the current element
138        $previous = $this->stack [$this->size-1];
139
140        // Add new element to stack
141        $this->stack [$this->size] = $element;
142        $this->size++;
143
144        // Set the elements style object
145        if (isset($this->document)) {
146            $styleObj = $this->document->getStyle($element->getStyleName());
147            $element->setStyle($styleObj);
148        }
149
150        // Let the element find its parent
151        $element->determineParent ($previous);
152    }
153
154    /**
155     * Get current element on top of the stack.
156     *
157     * @return ODTStateElement
158     */
159    public function getCurrent() {
160        return $this->stack [$this->size-1];
161    }
162
163    /**
164     * Leave current state. All data of the curent state is thrown away.
165     */
166    public function leave() {
167        // We always will keep the initial state.
168        // That means we do nothing if size is 0. This would be a fault anyway.
169        if ($this->size > 1) {
170            unset ($this->stack [$this->size-1]);
171            $this->size--;
172        }
173    }
174
175    /**
176     * Reset the state stack/go back to the initial state.
177     * All states except the root state will be discarded.
178     */
179    public function reset() {
180        // Throw away any states except the initial state.
181        // Reset size to 1.
182        for ($reset = 1 ; $reset < $this->size ; $reset++) {
183            unset ($this->stack [$reset]);
184        }
185        $this->size = 1;
186    }
187
188    /**
189     * Find the closest state with class $clazz.
190     *
191     * @param string $clazz
192     * @return ODTStateEntry|NULL
193     */
194    public function findClosestWithClass($clazz) {
195        for ($search = $this->size-1 ; $search > 0 ; $search--) {
196            if ($this->stack [$search]->getClass() == $clazz) {
197                return $this->stack [$search];
198            }
199        }
200        // Nothing found.
201        return NULL;
202    }
203
204    /**
205     * Find the closest state with class $clazz, return $index.
206     *
207     * @param string $clazz
208     * @param integer|false &$index Index of the found element or false
209     * @return ODTStateEntry|NULL
210     */
211    public function findClosestWithClassGetIndex($clazz, &$index) {
212        $index = false;
213        for ($search = $this->size-1 ; $search > 0 ; $search--) {
214            if ($this->stack [$search]->getClass() == $clazz) {
215                $index = $search;
216                return $this->stack [$search];
217            }
218        }
219        // Nothing found.
220        return NULL;
221    }
222
223    /**
224     * toString() function. Only for creating debug dumps.
225     *
226     * @return string
227     */
228    public function toString () {
229        $indent = '';
230        $string = "Stackdump:\n";
231        for ($search = 0 ; $search < $this->size ; $search++) {
232            $string .= $indent . $this->stack [$search]->getElementName().";\n";
233            $indent .= '    ';
234        }
235        return $string;
236    }
237
238    /**
239     * Find the closest state with class $clazz.
240     *
241     * @param string $clazz
242     * @return ODTStateEntry|NULL
243     */
244    public function countClass($clazz) {
245        $count = 0;
246        for ($search = $this->size-1 ; $search > 0 ; $search--) {
247            if ($this->stack [$search]->getClass() == $clazz) {
248                $count++;
249            }
250        }
251        return $count;
252    }
253
254    /**
255     * Find the closest element with element name $name.
256     *
257     * @param string $name
258     * @return ODTStateElement|NULL
259     */
260    public function findClosestWithName($name) {
261        for ($search = $this->size-1 ; $search > 0 ; $search--) {
262            if ($this->stack [$search]->getElementName() == $name) {
263                return $this->stack [$search];
264            }
265        }
266        // Nothing found.
267        return NULL;
268    }
269
270    /**
271     * Are we in a table row?
272     *
273     * @return bool
274     */
275    public function getInTableRow() {
276        $this->findClosestWithClassGetIndex('table-row', $tableRowIndex);
277        $this->findClosestWithClassGetIndex('table', $tableIndex);
278        if ($tableRowIndex > $tableIndex) {
279            return true;
280        }
281        return false;
282    }
283
284    /**
285     * Are we in a table cell?
286     *
287     * @return bool
288     */
289    public function getInTableCell() {
290        $this->findClosestWithClassGetIndex('table-cell', $tableCellIndex);
291        $this->findClosestWithClassGetIndex('table-row', $tableRowIndex);
292        if ($tableCellIndex > $tableRowIndex) {
293            return true;
294        }
295        return false;
296    }
297
298    /**
299     * Are we in a list item?
300     *
301     * @return bool
302     */
303    public function getInListItem() {
304        $this->findClosestWithClassGetIndex('list-item', $listItemIndex);
305        $this->findClosestWithClassGetIndex('list', $listIndex);
306        if ($listItemIndex > $listIndex) {
307            return true;
308        }
309        return false;
310    }
311
312    /**
313     * Are we in list content?
314     *
315     * @return bool
316     */
317    public function getInListContent() {
318        // listContentOpen == paragraphOpen,
319        // so we can simply call getInParagraph()
320        return $this->getInParagraph();
321    }
322
323    /**
324     * Are we in a paragraph?
325     *
326     * @return bool
327     */
328    public function getInParagraph() {
329        // Ask the current element
330        if ($this->size > 0) {
331            return $this->stack [$this->size-1]->getInParagraph();
332        } else {
333            return false;
334        }
335    }
336
337    /**
338     * Set the ODTDocument to which this state belongs.
339     *
340     * @param ODTDocument $doc
341     */
342    public function setDocument($doc) {
343        $this->document = $doc;
344    }
345
346    public function getHTMLElement() {
347        // Ask the current element
348        if ($this->size > 0) {
349            return $this->stack [$this->size-1]->getHTMLElement();
350        } else {
351            return NULL;
352        }
353    }
354
355    public function getElementCount($element) {
356        return $this->element_counter [$element]++;
357    }
358}
359