1<?php
2/**
3 * Class to fake a document tree for CSS matching.
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     LarsDW223
7 */
8
9/** Include ecm_interface */
10require_once DOKU_INC.'lib/plugins/odt/helper/ecm_interface.php';
11
12/**
13 * Class css_doc_element
14 *
15 * @package    CSS\CSSDocElement
16 */
17class css_doc_element implements iElementCSSMatchable {
18    /** var Reference to corresponding cssdocument */
19    public $doc = NULL;
20    /** var Index of this element in the corresponding cssdocument */
21    public $index = 0;
22
23    /**
24     * Get the name of this element.
25     *
26     * @return    string
27     */
28    public function iECSSM_getName() {
29        return $this->doc->entries [$this->index]['element'];
30    }
31
32    /**
33     * Get the attributes of this element.
34     *
35     * @return    array
36     */
37    public function iECSSM_getAttributes() {
38        return $this->doc->entries [$this->index]['attributes_array'];
39    }
40
41    /**
42     * Get the parent of this element.
43     *
44     * @return    css_doc_element
45     */
46    public function iECSSM_getParent() {
47        $index = $this->doc->findParent($this->index);
48        if ($index == -1 ) {
49            return NULL;
50        }
51        $element = new css_doc_element();
52        $element->doc = $this->doc;
53        $element->index = $index;
54        return $element;
55    }
56
57    /**
58     * Get the preceding sibling of this element.
59     *
60     * @return    css_doc_element
61     */
62    public function iECSSM_getPrecedingSibling() {
63        $index = $this->doc->getPrecedingSibling($this->index);
64        if ($index == -1 ) {
65            return NULL;
66        }
67        $element = new css_doc_element();
68        $element->doc = $this->doc;
69        $element->index = $index;
70        return $element;
71    }
72
73    /**
74     * Does this element belong to pseudo class $class?
75     *
76     * @param     string  $class
77     * @return    boolean
78     */
79    public function iECSSM_has_pseudo_class($class) {
80        if ($this->doc->entries [$this->index]['pseudo_classes'] == NULL) {
81            return false;
82        }
83        $result = array_search($class,
84            $this->doc->entries [$this->index]['pseudo_classes']);
85        if ($result === false) {
86            return false;
87        }
88        return true;
89    }
90
91    /**
92     * Does this element match the pseudo element $element?
93     *
94     * @param     string  $element
95     * @return    boolean
96     */
97    public function iECSSM_has_pseudo_element($element) {
98        if ($this->doc->entries [$this->index]['pseudo_elements'] == NULL) {
99            return false;
100        }
101        $result = array_search($element,
102            $this->doc->entries [$this->index]['pseudo_elements']);
103        if ($result === false) {
104            return false;
105        }
106        return true;
107    }
108
109    /**
110     * Return the CSS properties assigned to this element.
111     * (from extern via setProperties())
112     *
113     * @return    array
114     */
115    public function getProperties () {
116        return $this->doc->entries [$this->index]['properties'];
117    }
118
119    /**
120     * Set/assign the CSS properties for this element.
121     *
122     * @param     array $properties
123     */
124    public function setProperties (array &$properties) {
125        $this->doc->entries [$this->index]['properties'] = $properties;
126    }
127}
128
129/**
130 * Class cssdocument.
131 *
132 * @package    CSS\CSSDocument
133 */
134class cssdocument {
135    /** var Current size, Index for next entry */
136    public $size = 0;
137    /** var Current nesting level */
138    public $level = 0;
139    /** var Array of entries, see open() */
140    public $entries = array ();
141    /** var Root index, see saveRootIndex() */
142    protected $rootIndex = 0;
143    /** var Root level, see saveRootIndex() */
144    protected $rootLevel = 0;
145
146    /**
147     * Internal function to get the value of an attribute.
148     *
149     * @param     string  $value Value of the attribute
150     * @param     string  $input Code to parse
151     * @param     integer $pos   Current position in $input
152     * @param     integer $max   End of $input
153     * @return    integer Position at which the attribute ends
154     */
155    protected function collect_attribute_value (&$value, $input, $pos, $max) {
156        $value = '';
157        $in_quotes = false;
158        $quote = '';
159        while ($pos < $max) {
160            $sign = $input [$pos];
161            $pos++;
162
163            if ($in_quotes == false) {
164                if ($sign == '"' || $sign == "'") {
165                    $quote = $sign;
166                    $in_quotes = true;
167                }
168            } else {
169                if ($sign == $quote) {
170                    break;
171                }
172                $value .= $sign;
173            }
174        }
175
176        if ($in_quotes == false || $sign != $quote) {
177            // No proper quotes, delete value
178            $value = NULL;
179        }
180
181        return $pos;
182    }
183
184    /**
185     * Internal function to parse $attributes for key="value" pairs
186     * and store the result in an array.
187     *
188     * @param     string  $attributes Code to parse
189     * @return    array Array of attributes
190     */
191    protected function get_attributes_array ($attributes) {
192        if ($attributes == NULL) {
193            return NULL;
194        }
195
196        $result = array();
197        $pos = 0;
198        $max = strlen($attributes);
199        while ($pos < $max) {
200            $equal_sign = strpos ($attributes, '=', $pos);
201            if ($equal_sign === false) {
202                break;
203            }
204            $att_name = substr ($attributes, $pos, $equal_sign-$pos);
205            $att_name = trim ($att_name, ' ');
206
207            $att_end = $this->collect_attribute_value($att_value, $attributes, $equal_sign+1, $max);
208
209            // Add a attribute to array
210            $result [$att_name] = $att_value;
211            $pos = $att_end + 1;
212        }
213        return $result;
214    }
215
216    /**
217     * Save the current position as the root index of the document.
218     * It is guaranteed that elements below the root index will not be
219     * discarded from the cssdocument.
220     */
221    public function saveRootIndex () {
222        $this->rootIndex = $this->getIndexLastOpened ();
223        $this->rootLevel = $this->level-1;
224    }
225
226    /**
227     * Shrinks/cuts the cssdocument down to its root index.
228     */
229    public function restoreToRoot () {
230        for ($index = $this->size-1 ; $index > $this->rootIndex ; $index--) {
231            $this->entries [$index] = NULL;
232        }
233        $this->size = $this->rootIndex + 1;
234        $this->level = $this->rootLevel + 1;
235    }
236
237    /**
238     * Get the current state of the cssdocument.
239     *
240     * @param    array $state    Returned state information
241     */
242    public function getState (array &$state) {
243        $state ['index'] = $this->size-1;
244        $state ['level'] = $this->level;
245    }
246
247    /**
248     * Shrinks/cuts the cssdocument down to the given $state.
249     * ($state must be retrieved by calling getState())
250     *
251     * @param    array $state    State information
252     */
253    public function restoreState (array $state) {
254        for ($index = $this->size-1 ; $index > $state ['index'] ; $index--) {
255            $this->entries [$index] = NULL;
256        }
257        $this->size = $state ['index'] + 1;
258        $this->level = $state ['level'];
259    }
260
261    /**
262     * Open a new element in the cssdocument.
263     *
264     * @param    string $element         The element's name
265     * @param    string $attributes      The element's attributes
266     * @param    string $pseudo_classes  The element's pseudo classes
267     * @param    string $pseudo_elements The element's pseudo elements
268     */
269    public function open ($element, $attributes=NULL, $pseudo_classes=NULL, $pseudo_elements=NULL) {
270        $this->entries [$this->size]['level'] = $this->level;
271        $this->entries [$this->size]['state'] = 'open';
272        $this->entries [$this->size]['element'] = $element;
273        $this->entries [$this->size]['attributes'] = $attributes;
274        if (!empty($pseudo_classes)) {
275            $this->entries [$this->size]['pseudo_classes'] = explode(' ', $pseudo_classes);
276        }
277        if (!empty($pseudo_elements)) {
278            $this->entries [$this->size]['pseudo_elements'] = explode(' ', $pseudo_elements);
279        }
280
281        // Build attribute array/parse attributes
282        if ($attributes != NULL) {
283            $this->entries [$this->size]['attributes_array'] =
284                $this->get_attributes_array ($attributes);
285        }
286
287        $this->size++;
288        $this->level++;
289    }
290
291    /**
292     * Close $element in the cssdocument.
293     *
294     * @param    string $element         The element's name
295     */
296    public function close ($element) {
297        $this->level--;
298        $this->entries [$this->size]['level'] = $this->level;
299        $this->entries [$this->size]['state'] = 'close';
300        $this->entries [$this->size]['element'] = $element;
301        $this->size++;
302    }
303
304    /**
305     * Get the current element.
306     *
307     * @return css_doc_element
308     */
309    public function getCurrentElement() {
310        $index = $this->getIndexLastOpened ();
311        if ($index == -1) {
312            return NULL;
313        }
314        $element = new css_doc_element();
315        $element->doc = $this;
316        $element->index = $index;
317        return $element;
318    }
319
320    /**
321     * Get the entry of internal array $entries at $index.
322     *
323     * @param  integer $index
324     * @return array
325     */
326    public function getEntry ($index) {
327        if ($index >= $this->size ) {
328            return NULL;
329        }
330        return $this->entries [$index];
331    }
332
333    /**
334     * Get the current entry of internal array $entries.
335     *
336     * @return array
337     */
338    public function getCurrentEntry () {
339        if ($this->size == 0) {
340            return NULL;
341        }
342        return $this->entries [$this->size-1];
343    }
344
345    /**
346     * Get the index of the 'open' entry of the latest opened element.
347     *
348     * @return integer
349     */
350    public function getIndexLastOpened () {
351        if ($this->size == 0) {
352            return -1;
353        }
354        for ($index = $this->size-1 ; $index >= 0 ; $index--) {
355            if ($this->entries [$index]['state'] == 'open') {
356                return $index;
357            }
358        }
359        return -1;
360    }
361
362    /**
363     * Find the parent for the entry at index $start.
364     *
365     * @param    integer $start    Starting point
366     */
367    public function findParent ($start) {
368        if ($this->size == 0 || $start >= $this->size) {
369            return -1;
370        }
371        $start_level = $this->entries [$start]['level'];
372        if ($start_level == 0) {
373            return -1;
374        }
375        for ($index = $start-1 ; $index >= 0 ; $index--) {
376            if ($this->entries [$index]['state'] == 'open'
377                &&
378                $this->entries [$index]['level'] == $start_level-1) {
379                return $index;
380            }
381        }
382        return -1;
383    }
384
385    /**
386     * Find the preceding sibling for the entry at index $current.
387     *
388     * @param    integer $current    Starting point
389     */
390    public function getPrecedingSibling ($current) {
391        if ($this->size == 0 || $current >= $this->size || $current == 0) {
392            return -1;
393        }
394        $current_level = $this->entries [$current]['level'];
395        if ($this->entries [$current-1]['level'] == $current_level) {
396            return ($current-1);
397        }
398        return -1;
399    }
400
401    /**
402     * Dump the current elements/entries in this cssdocument.
403     * Only for debugging purposes.
404     */
405    public function getDump () {
406        $dump = '';
407        $dump .= 'RootLevel: '.$this->rootLevel.', RootIndex: '.$this->rootIndex."\n";
408        for ($index = 0 ; $index < $this->size ; $index++) {
409            $element = $this->entries [$index];
410            $dump .= str_repeat(' ', $element ['level'] * 2);
411            if ($this->entries [$index]['state'] == 'open') {
412                $dump .= '<'.$element ['element'];
413                $dump .= ' '.$element ['attributes'].'>';
414            } else {
415                $dump .= '</'.$element ['element'].'>';
416            }
417            $dump .= ' (Level: '.$element ['level'].')';
418            $dump .= "\n";
419        }
420        return $dump;
421    }
422
423    /**
424     * Remove the current entry.
425     */
426    public function removeCurrent () {
427        $index = $this->size-1;
428        if ($index <= $this->rootIndex) {
429            // Do not remove root elements!
430            return;
431        }
432        $this->level = $this->entries [$index]['level'];
433        $this->entries [$index] = NULL;
434        $this->size--;
435    }
436}
437