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