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 */
10 require_once DOKU_INC.'lib/plugins/odt/helper/ecm_interface.php';
11 
12 /**
13  * Class css_doc_element
14  *
15  * @package    CSS\CSSDocElement
16  */
17 class 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  */
138 class 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