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