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