vanilla /
ebi
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * @author Todd Burry <[email protected]> |
||
| 4 | * @copyright 2009-2017 Vanilla Forums Inc. |
||
| 5 | * @license MIT |
||
| 6 | */ |
||
| 7 | |||
| 8 | namespace Ebi; |
||
| 9 | |||
| 10 | use DOMAttr; |
||
| 11 | use DOMElement; |
||
| 12 | use DOMNode; |
||
| 13 | use Symfony\Component\ExpressionLanguage\SyntaxError; |
||
| 14 | |||
| 15 | class Compiler { |
||
| 16 | const T_IF = 'x-if'; |
||
| 17 | const T_EACH = 'x-each'; |
||
| 18 | const T_WITH = 'x-with'; |
||
| 19 | const T_LITERAL = 'x-literal'; |
||
| 20 | const T_AS = 'x-as'; |
||
| 21 | const T_COMPONENT = 'x-component'; |
||
| 22 | const T_CHILDREN = 'x-children'; |
||
| 23 | const T_BLOCK = 'x-block'; |
||
| 24 | const T_ELSE = 'x-else'; |
||
| 25 | const T_EMPTY = 'x-empty'; |
||
| 26 | const T_X = 'x'; |
||
| 27 | const T_INCLUDE = 'x-include'; |
||
| 28 | const T_EBI = 'ebi'; |
||
| 29 | const T_UNESCAPE = 'x-unescape'; |
||
| 30 | const T_TAG = 'x-tag'; |
||
| 31 | |||
| 32 | const IDENT_REGEX = '`^([a-z0-9-]+)$`i'; |
||
| 33 | |||
| 34 | protected static $special = [ |
||
| 35 | self::T_COMPONENT => 1, |
||
| 36 | self::T_IF => 2, |
||
| 37 | self::T_ELSE => 3, |
||
| 38 | self::T_EACH => 4, |
||
| 39 | self::T_EMPTY => 5, |
||
| 40 | self::T_CHILDREN => 6, |
||
| 41 | self::T_INCLUDE => 7, |
||
| 42 | self::T_WITH => 8, |
||
| 43 | self::T_BLOCK => 9, |
||
| 44 | self::T_LITERAL => 10, |
||
| 45 | self::T_AS => 11, |
||
| 46 | self::T_UNESCAPE => 12, |
||
| 47 | self::T_TAG => 13 |
||
| 48 | ]; |
||
| 49 | |||
| 50 | protected static $htmlTags = [ |
||
| 51 | 'a' => 'i', |
||
| 52 | 'abbr' => 'i', |
||
| 53 | 'acronym' => 'i', // deprecated |
||
| 54 | 'address' => 'b', |
||
| 55 | // 'applet' => 'i', // deprecated |
||
| 56 | 'area' => 'i', |
||
| 57 | 'article' => 'b', |
||
| 58 | 'aside' => 'b', |
||
| 59 | 'audio' => 'i', |
||
| 60 | 'b' => 'i', |
||
| 61 | 'base' => 'i', |
||
| 62 | // 'basefont' => 'i', |
||
| 63 | 'bdi' => 'i', |
||
| 64 | 'bdo' => 'i', |
||
| 65 | // 'bgsound' => 'i', |
||
| 66 | // 'big' => 'i', |
||
| 67 | 'x' => 'i', |
||
| 68 | // 'blink' => 'i', |
||
| 69 | 'blockquote' => 'b', |
||
| 70 | 'body' => 'b', |
||
| 71 | 'br' => 'i', |
||
| 72 | 'button' => 'i', |
||
| 73 | 'canvas' => 'b', |
||
| 74 | 'caption' => 'i', |
||
| 75 | // 'center' => 'b', |
||
| 76 | 'cite' => 'i', |
||
| 77 | 'code' => 'i', |
||
| 78 | 'col' => 'i', |
||
| 79 | 'colgroup' => 'i', |
||
| 80 | // 'command' => 'i', |
||
| 81 | 'content' => 'i', |
||
| 82 | 'data' => 'i', |
||
| 83 | 'datalist' => 'i', |
||
| 84 | 'dd' => 'b', |
||
| 85 | 'del' => 'i', |
||
| 86 | 'details' => 'i', |
||
| 87 | 'dfn' => 'i', |
||
| 88 | 'dialog' => 'i', |
||
| 89 | // 'dir' => 'i', |
||
| 90 | 'div' => 'i', |
||
| 91 | 'dl' => 'b', |
||
| 92 | 'dt' => 'b', |
||
| 93 | // 'element' => 'i', |
||
| 94 | 'em' => 'i', |
||
| 95 | 'embed' => 'i', |
||
| 96 | 'fieldset' => 'b', |
||
| 97 | 'figcaption' => 'b', |
||
| 98 | 'figure' => 'b', |
||
| 99 | // 'font' => 'i', |
||
| 100 | 'footer' => 'b', |
||
| 101 | 'form' => 'b', |
||
| 102 | 'frame' => 'i', |
||
| 103 | 'frameset' => 'i', |
||
| 104 | 'h1' => 'b', |
||
| 105 | 'h2' => 'b', |
||
| 106 | 'h3' => 'b', |
||
| 107 | 'h4' => 'b', |
||
| 108 | 'h5' => 'b', |
||
| 109 | 'h6' => 'b', |
||
| 110 | 'head' => 'b', |
||
| 111 | 'header' => 'b', |
||
| 112 | 'hgroup' => 'b', |
||
| 113 | 'hr' => 'b', |
||
| 114 | 'html' => 'b', |
||
| 115 | 'i' => 'i', |
||
| 116 | 'iframe' => 'i', |
||
| 117 | 'image' => 'i', |
||
| 118 | 'img' => 'i', |
||
| 119 | 'input' => 'i', |
||
| 120 | 'ins' => 'i', |
||
| 121 | 'isindex' => 'i', |
||
| 122 | 'kbd' => 'i', |
||
| 123 | 'keygen' => 'i', |
||
| 124 | 'label' => 'i', |
||
| 125 | 'legend' => 'i', |
||
| 126 | 'li' => 'i', |
||
| 127 | 'link' => 'i', |
||
| 128 | // 'listing' => 'i', |
||
| 129 | 'main' => 'b', |
||
| 130 | 'map' => 'i', |
||
| 131 | 'mark' => 'i', |
||
| 132 | // 'marquee' => 'i', |
||
| 133 | 'menu' => 'i', |
||
| 134 | 'menuitem' => 'i', |
||
| 135 | 'meta' => 'i', |
||
| 136 | 'meter' => 'i', |
||
| 137 | 'multicol' => 'i', |
||
| 138 | 'nav' => 'b', |
||
| 139 | 'nobr' => 'i', |
||
| 140 | 'noembed' => 'i', |
||
| 141 | 'noframes' => 'i', |
||
| 142 | 'noscript' => 'b', |
||
| 143 | 'object' => 'i', |
||
| 144 | 'ol' => 'b', |
||
| 145 | 'optgroup' => 'i', |
||
| 146 | 'option' => 'b', |
||
| 147 | 'output' => 'i', |
||
| 148 | 'p' => 'b', |
||
| 149 | 'param' => 'i', |
||
| 150 | 'picture' => 'i', |
||
| 151 | // 'plaintext' => 'i', |
||
| 152 | 'pre' => 'b', |
||
| 153 | 'progress' => 'i', |
||
| 154 | 'q' => 'i', |
||
| 155 | 'rp' => 'i', |
||
| 156 | 'rt' => 'i', |
||
| 157 | 'rtc' => 'i', |
||
| 158 | 'ruby' => 'i', |
||
| 159 | 's' => 'i', |
||
| 160 | 'samp' => 'i', |
||
| 161 | 'script' => 'i', |
||
| 162 | 'section' => 'b', |
||
| 163 | 'select' => 'i', |
||
| 164 | // 'shadow' => 'i', |
||
| 165 | 'slot' => 'i', |
||
| 166 | 'small' => 'i', |
||
| 167 | 'source' => 'i', |
||
| 168 | // 'spacer' => 'i', |
||
| 169 | 'span' => 'i', |
||
| 170 | // 'strike' => 'i', |
||
| 171 | 'strong' => 'i', |
||
| 172 | 'style' => 'i', |
||
| 173 | 'sub' => 'i', |
||
| 174 | 'summary' => 'i', |
||
| 175 | 'sup' => 'i', |
||
| 176 | 'table' => 'b', |
||
| 177 | 'tbody' => 'i', |
||
| 178 | 'td' => 'i', |
||
| 179 | 'template' => 'i', |
||
| 180 | 'textarea' => 'i', |
||
| 181 | 'tfoot' => 'b', |
||
| 182 | 'th' => 'i', |
||
| 183 | 'thead' => 'i', |
||
| 184 | 'time' => 'i', |
||
| 185 | 'title' => 'i', |
||
| 186 | 'tr' => 'i', |
||
| 187 | 'track' => 'i', |
||
| 188 | // 'tt' => 'i', |
||
| 189 | 'u' => 'i', |
||
| 190 | 'ul' => 'b', |
||
| 191 | 'var' => 'i', |
||
| 192 | 'video' => 'b', |
||
| 193 | 'wbr' => 'i', |
||
| 194 | |||
| 195 | /// SVG /// |
||
| 196 | 'animate' => 's', |
||
| 197 | 'animateColor' => 's', |
||
| 198 | 'animateMotion' => 's', |
||
| 199 | 'animateTransform' => 's', |
||
| 200 | // 'canvas' => 's', |
||
| 201 | 'circle' => 's', |
||
| 202 | 'desc' => 's', |
||
| 203 | 'defs' => 's', |
||
| 204 | 'discard' => 's', |
||
| 205 | 'ellipse' => 's', |
||
| 206 | 'g' => 's', |
||
| 207 | // 'image' => 's', |
||
| 208 | 'line' => 's', |
||
| 209 | 'marker' => 's', |
||
| 210 | 'mask' => 's', |
||
| 211 | 'missing-glyph' => 's', |
||
| 212 | 'mpath' => 's', |
||
| 213 | 'metadata' => 's', |
||
| 214 | 'path' => 's', |
||
| 215 | 'pattern' => 's', |
||
| 216 | 'polygon' => 's', |
||
| 217 | 'polyline' => 's', |
||
| 218 | 'rect' => 's', |
||
| 219 | 'set' => 's', |
||
| 220 | 'svg' => 's', |
||
| 221 | 'switch' => 's', |
||
| 222 | 'symbol' => 's', |
||
| 223 | 'text' => 's', |
||
| 224 | // 'unknown' => 's', |
||
| 225 | 'use' => 's', |
||
| 226 | ]; |
||
| 227 | |||
| 228 | protected static $boolAttributes = [ |
||
| 229 | 'checked' => 1, |
||
| 230 | 'itemscope' => 1, |
||
| 231 | 'required' => 1, |
||
| 232 | 'selected' => 1, |
||
| 233 | ]; |
||
| 234 | |||
| 235 | /** |
||
| 236 | * @var ExpressionLanguage |
||
| 237 | */ |
||
| 238 | protected $expressions; |
||
| 239 | |||
| 240 | 71 | public function __construct() { |
|
| 241 | 71 | $this->expressions = new ExpressionLanguage(); |
|
| 242 | 71 | $this->expressions->setNamePattern('/[@a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'); |
|
| 243 | 71 | $this->expressions->register( |
|
| 244 | 71 | 'hasChildren', |
|
| 245 | 71 | function ($name = null) { |
|
| 246 | 1 | return empty($name) ? 'isset($children[0])' : "isset(\$children[$name ?: 0])"; |
|
| 247 | 71 | }, |
|
| 248 | 71 | function ($name = null) { |
|
| 249 | return false; |
||
| 250 | 71 | }); |
|
| 251 | 71 | } |
|
| 252 | |||
| 253 | /** |
||
| 254 | * Register a runtime function. |
||
| 255 | * |
||
| 256 | * @param string $name The name of the function. |
||
| 257 | * @param callable $function The function callback. |
||
| 258 | */ |
||
| 259 | 69 | public function defineFunction($name, $function = null) { |
|
| 260 | 69 | if ($function === null) { |
|
| 261 | 1 | $function = $name; |
|
| 262 | } |
||
| 263 | |||
| 264 | 69 | $this->expressions->register( |
|
| 265 | 69 | $name, |
|
| 266 | 69 | $this->getFunctionCompiler($name, $function), |
|
| 267 | 69 | $this->getFunctionEvaluator($function) |
|
| 268 | ); |
||
| 269 | 69 | } |
|
| 270 | |||
| 271 | 69 | private function getFunctionEvaluator($function) { |
|
| 272 | 69 | if ($function === 'empty') { |
|
| 273 | 69 | return function ($expr) { |
|
| 274 | return empty($expr); |
||
| 275 | 69 | }; |
|
| 276 | 68 | } elseif ($function === 'isset') { |
|
| 277 | return function ($expr) { |
||
| 278 | return isset($expr); |
||
| 279 | }; |
||
| 280 | } |
||
| 281 | |||
| 282 | 68 | return $function; |
|
| 283 | } |
||
| 284 | |||
| 285 | 69 | private function getFunctionCompiler($name, $function) { |
|
| 286 | 69 | $var = var_export(strtolower($name), true); |
|
| 287 | 69 | $fn = function ($expr) use ($var) { |
|
| 288 | 1 | return "\$this->call($var, $expr)"; |
|
| 289 | 69 | }; |
|
| 290 | |||
| 291 | 69 | if (is_string($function)) { |
|
| 292 | 69 | $fn = function (...$args) use ($function) { |
|
| 293 | 14 | return $function.'('.implode(', ', $args).')'; |
|
| 294 | 69 | }; |
|
| 295 | 68 | } elseif (is_array($function)) { |
|
| 296 | 68 | if (is_string($function[0])) { |
|
| 297 | $fn = function (...$args) use ($function) { |
||
| 298 | return "$function[0]::$function[1](".implode(', ', $args).')'; |
||
| 299 | }; |
||
| 300 | 68 | } elseif ($function[0] instanceof Ebi) { |
|
| 301 | 68 | $fn = function (...$args) use ($function) { |
|
| 302 | 7 | return "\$this->$function[1](".implode(', ', $args).')'; |
|
| 303 | 68 | }; |
|
| 304 | } |
||
| 305 | } |
||
| 306 | |||
| 307 | 69 | return $fn; |
|
| 308 | } |
||
| 309 | |||
| 310 | 63 | public function compile($src, array $options = []) { |
|
| 311 | 63 | $options += ['basename' => '', 'path' => '', 'runtime' => true]; |
|
| 312 | |||
| 313 | 63 | $src = trim($src); |
|
| 314 | |||
| 315 | 63 | $out = new CompilerBuffer(); |
|
| 316 | |||
| 317 | 63 | $out->setBasename($options['basename']); |
|
| 318 | 63 | $out->setSource($src); |
|
| 319 | 63 | $out->setPath($options['path']); |
|
| 320 | |||
| 321 | 63 | $dom = new \DOMDocument(); |
|
| 322 | |||
| 323 | 63 | $fragment = false; |
|
| 324 | 63 | if (strpos($src, '<html') === false) { |
|
| 325 | 61 | $src = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><html><body>$src</body></html>"; |
|
| 326 | 61 | $fragment = true; |
|
| 327 | } |
||
| 328 | |||
| 329 | 63 | libxml_use_internal_errors(true); |
|
| 330 | 63 | $dom->loadHTML($src, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOCDATA | LIBXML_NOXMLDECL); |
|
| 331 | // $arr = $this->domToArray($dom); |
||
| 332 | |||
| 333 | 63 | if ($options['runtime']) { |
|
| 334 | 62 | $name = var_export($options['basename'], true); |
|
| 335 | 62 | $out->appendCode("\$this->defineComponent($name, function (\$props = [], \$children = []) {\n"); |
|
| 336 | } else { |
||
| 337 | 1 | $out->appendCode("function (\$props = [], \$children = []) {\n"); |
|
| 338 | } |
||
| 339 | |||
| 340 | 63 | $out->pushScope(['this' => 'props']); |
|
| 341 | 63 | $out->indent(+1); |
|
| 342 | |||
| 343 | 63 | $parent = $fragment ? $dom->firstChild->nextSibling->firstChild : $dom; |
|
| 344 | |||
| 345 | 63 | foreach ($parent->childNodes as $node) { |
|
| 346 | 63 | $this->compileNode($node, $out); |
|
| 347 | } |
||
| 348 | |||
| 349 | 57 | $out->indent(-1); |
|
| 350 | 57 | $out->popScope(); |
|
| 351 | |||
| 352 | 57 | if ($options['runtime']) { |
|
| 353 | 56 | $out->appendCode("});"); |
|
| 354 | } else { |
||
| 355 | 1 | $out->appendCode("};"); |
|
| 356 | } |
||
| 357 | |||
| 358 | 57 | $r = $out->flush(); |
|
| 359 | |||
| 360 | 57 | $errs = libxml_get_errors(); |
|
| 361 | |||
| 362 | 57 | return $r; |
|
| 363 | } |
||
| 364 | |||
| 365 | 48 | protected function isComponent($tag) { |
|
| 366 | 48 | return !isset(static::$htmlTags[$tag]); |
|
| 367 | } |
||
| 368 | |||
| 369 | 63 | protected function compileNode(DOMNode $node, CompilerBuffer $out) { |
|
| 370 | 63 | if ($out->getNodeProp($node, 'skip')) { |
|
| 371 | 4 | return; |
|
| 372 | } |
||
| 373 | |||
| 374 | 63 | switch ($node->nodeType) { |
|
| 375 | 63 | case XML_TEXT_NODE: |
|
| 376 | 50 | $this->compileTextNode($node, $out); |
|
| 377 | 50 | break; |
|
| 378 | 60 | case XML_ELEMENT_NODE: |
|
| 379 | /* @var \DOMElement $node */ |
||
| 380 | 60 | $this->compileElementNode($node, $out); |
|
| 381 | 55 | break; |
|
| 382 | 4 | case XML_COMMENT_NODE: |
|
| 383 | /* @var \DOMComment $node */ |
||
| 384 | 1 | $this->compileCommentNode($node, $out); |
|
| 385 | 1 | break; |
|
| 386 | 3 | case XML_DOCUMENT_TYPE_NODE: |
|
| 387 | 1 | $out->echoLiteral("<!DOCTYPE {$node->name}>\n"); |
|
| 388 | 1 | break; |
|
| 389 | 2 | case XML_CDATA_SECTION_NODE: |
|
| 390 | 2 | $this->compileTextNode($node, $out); |
|
| 391 | 2 | break; |
|
| 392 | default: |
||
| 393 | $r = "// Unknown node\n". |
||
| 394 | '// '.str_replace("\n", "\n// ", $node->ownerDocument->saveHTML($node)); |
||
| 395 | } |
||
| 396 | 58 | } |
|
| 397 | |||
| 398 | protected function domToArray(DOMNode $root) { |
||
| 399 | $result = []; |
||
| 400 | |||
| 401 | if ($root->hasAttributes()) { |
||
| 402 | $attrs = $root->attributes; |
||
| 403 | foreach ($attrs as $attr) { |
||
| 404 | $result['@attributes'][$attr->name] = $attr->value; |
||
| 405 | } |
||
| 406 | } |
||
| 407 | |||
| 408 | if ($root->hasChildNodes()) { |
||
| 409 | $children = $root->childNodes; |
||
| 410 | if ($children->length == 1) { |
||
| 411 | $child = $children->item(0); |
||
| 412 | if ($child->nodeType == XML_TEXT_NODE) { |
||
| 413 | $result['_value'] = $child->nodeValue; |
||
| 414 | return count($result) == 1 |
||
| 415 | ? $result['_value'] |
||
| 416 | : $result; |
||
| 417 | } |
||
| 418 | } |
||
| 419 | $groups = []; |
||
| 420 | foreach ($children as $child) { |
||
| 421 | if (!isset($result[$child->nodeName])) { |
||
| 422 | $result[$child->nodeName] = $this->domToArray($child); |
||
| 423 | } else { |
||
| 424 | if (!isset($groups[$child->nodeName])) { |
||
| 425 | $result[$child->nodeName] = [$result[$child->nodeName]]; |
||
| 426 | $groups[$child->nodeName] = 1; |
||
| 427 | } |
||
| 428 | $result[$child->nodeName][] = $this->domToArray($child); |
||
| 429 | } |
||
| 430 | } |
||
| 431 | } |
||
| 432 | |||
| 433 | return $result; |
||
| 434 | } |
||
| 435 | |||
| 436 | 1 | protected function newline(DOMNode $node, CompilerBuffer $out) { |
|
| 437 | 1 | if ($node->previousSibling && $node->previousSibling->nodeType !== XML_COMMENT_NODE) { |
|
| 438 | $out->appendCode("\n"); |
||
| 439 | } |
||
| 440 | 1 | } |
|
| 441 | |||
| 442 | 1 | protected function compileCommentNode(\DOMComment $node, CompilerBuffer $out) { |
|
| 443 | 1 | $comments = explode("\n", trim($node->nodeValue)); |
|
| 444 | |||
| 445 | 1 | $this->newline($node, $out); |
|
| 446 | 1 | foreach ($comments as $comment) { |
|
| 447 | 1 | $out->appendCode("// $comment\n"); |
|
| 448 | } |
||
| 449 | 1 | } |
|
| 450 | |||
| 451 | 52 | protected function compileTextNode(DOMNode $node, CompilerBuffer $out) { |
|
| 452 | 52 | $nodeText = $node->nodeValue; |
|
| 453 | |||
| 454 | 52 | $items = $this->splitExpressions($nodeText); |
|
| 455 | 52 | foreach ($items as $i => list($text, $offset)) { |
|
| 456 | 52 | if (preg_match('`^{\S`', $text)) { |
|
| 457 | 30 | if (preg_match('`^{\s*unescape\((.+)\)\s*}$`', $text, $m)) { |
|
| 458 | 3 | $out->echoCode($this->expr($m[1], $out)); |
|
| 459 | } else { |
||
| 460 | try { |
||
| 461 | 28 | $expr = substr($text, 1, -1); |
|
| 462 | 28 | $out->echoCode($this->compileEscape($this->expr($expr, $out))); |
|
| 463 | 1 | } catch (SyntaxError $ex) { |
|
| 464 | 1 | $nodeLineCount = substr_count($nodeText, "\n"); |
|
|
0 ignored issues
–
show
|
|||
| 465 | 1 | $offsetLineCount = substr_count($nodeText, "\n", 0, $offset); |
|
| 466 | 1 | $line = $node->getLineNo() - $nodeLineCount + $offsetLineCount; |
|
|
0 ignored issues
–
show
Equals sign not aligned with surrounding assignments; expected 12 spaces but found 1 space
This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line. To visualize $a = "a";
$ab = "ab";
$abc = "abc";
will produce issues in the first and second line, while this second example $a = "a";
$ab = "ab";
$abc = "abc";
will produce no issues. Loading history...
|
|||
| 467 | 30 | throw $out->createCompilerException($node, $ex, ['source' => $expr, 'line' => $line]); |
|
| 468 | } |
||
| 469 | } |
||
| 470 | } else { |
||
| 471 | 52 | if ($i === 0) { |
|
| 472 | 52 | $text = $this->ltrim($text, $node, $out); |
|
| 473 | } |
||
| 474 | 52 | if ($i === count($items) - 1) { |
|
| 475 | 52 | $text = $this->rtrim($text, $node, $out); |
|
| 476 | } |
||
| 477 | |||
| 478 | 52 | $out->echoLiteral($text); |
|
| 479 | } |
||
| 480 | } |
||
| 481 | 52 | } |
|
| 482 | |||
| 483 | 60 | protected function compileElementNode(DOMElement $node, CompilerBuffer $out) { |
|
| 484 | 60 | list($attributes, $special) = $this->splitAttributes($node); |
|
| 485 | |||
| 486 | 60 | if ($node->tagName === 'script' && ((isset($attributes['type']) && $attributes['type']->value === self::T_EBI) || !empty($special[self::T_AS]) || !empty($special[self::T_UNESCAPE]))) { |
|
| 487 | 8 | $this->compileExpressionNode($node, $attributes, $special, $out); |
|
| 488 | 53 | } elseif (!empty($special) || $this->isComponent($node->tagName)) { |
|
| 489 | 37 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
| 490 | } else { |
||
| 491 | 29 | $this->compileBasicElement($node, $attributes, $special, $out); |
|
| 492 | } |
||
| 493 | 55 | } |
|
| 494 | |||
| 495 | 52 | protected function splitExpressions($value) { |
|
| 496 | 52 | $values = preg_split('`({\S[^}]*?})`', $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); |
|
| 497 | 52 | return $values; |
|
| 498 | } |
||
| 499 | |||
| 500 | /** |
||
| 501 | * Compile the PHP for an expression |
||
| 502 | * |
||
| 503 | * @param string $expr The expression to compile. |
||
| 504 | * @param CompilerBuffer $out The current output buffer. |
||
| 505 | * @param DOMAttr|null $attr The owner of the expression. |
||
| 506 | * @return string Returns a string of PHP code. |
||
| 507 | */ |
||
| 508 | 55 | protected function expr($expr, CompilerBuffer $out, DOMAttr $attr = null) { |
|
| 509 | 55 | $names = $out->getScopeVariables(); |
|
| 510 | |||
| 511 | try { |
||
| 512 | 55 | $compiled = $this->expressions->compile($expr, function ($name) use ($names) { |
|
| 513 | 49 | if (isset($names[$name])) { |
|
| 514 | 30 | return $names[$name]; |
|
| 515 | 32 | } elseif ($name[0] === '@') { |
|
| 516 | 1 | return 'this->meta['.var_export(substr($name, 1), true).']'; |
|
| 517 | } else { |
||
| 518 | 31 | return $names['this'].'['.var_export($name, true).']'; |
|
| 519 | } |
||
| 520 | 55 | }); |
|
| 521 | 3 | } catch (SyntaxError $ex) { |
|
| 522 | 3 | if ($attr !== null) { |
|
| 523 | 1 | throw $out->createCompilerException($attr, $ex); |
|
| 524 | } else { |
||
| 525 | 2 | throw $ex; |
|
| 526 | } |
||
| 527 | } |
||
| 528 | |||
| 529 | 52 | if ($attr !== null && null !== $fn = $this->getAttributeFunction($attr)) { |
|
| 530 | 4 | $compiled = call_user_func($fn, $compiled); |
|
| 531 | } |
||
| 532 | |||
| 533 | 52 | return $compiled; |
|
| 534 | } |
||
| 535 | |||
| 536 | /** |
||
| 537 | * Get the compiler function to wrap an attribute. |
||
| 538 | * |
||
| 539 | * Attribute functions are regular expression functions, but with a special naming convention. The following naming |
||
| 540 | * conventions are supported: |
||
| 541 | * |
||
| 542 | * - **@tag:attribute**: Applies to an attribute only on a specific tag. |
||
| 543 | * - **@attribute**: Applies to all attributes with a given name. |
||
| 544 | * |
||
| 545 | * @param DOMAttr $attr The attribute to look at. |
||
| 546 | * @return callable|null A function or **null** if the attribute doesn't have a function. |
||
| 547 | */ |
||
| 548 | 30 | private function getAttributeFunction(DOMAttr $attr) { |
|
| 549 | 30 | $keys = ['@'.$attr->ownerElement->tagName.':'.$attr->name, '@'.$attr->name]; |
|
| 550 | |||
| 551 | 30 | foreach ($keys as $key) { |
|
| 552 | 30 | if (null !== $fn = $this->expressions->getFunctionCompiler($key)) { |
|
| 553 | 30 | return $fn; |
|
| 554 | } |
||
| 555 | } |
||
| 556 | 25 | } |
|
| 557 | |||
| 558 | /** |
||
| 559 | * @param DOMElement $node |
||
| 560 | */ |
||
| 561 | 60 | protected function splitAttributes(DOMElement $node) { |
|
| 562 | 60 | $attributes = []; |
|
| 563 | 60 | $special = []; |
|
| 564 | |||
| 565 | 60 | foreach ($node->attributes as $name => $attribute) { |
|
| 566 | 56 | if (isset(static::$special[$name])) { |
|
| 567 | 41 | $special[$name] = $attribute; |
|
| 568 | } else { |
||
| 569 | 56 | $attributes[$name] = $attribute; |
|
| 570 | } |
||
| 571 | } |
||
| 572 | |||
| 573 | 60 | uksort($special, function ($a, $b) { |
|
| 574 | 11 | return strnatcmp(static::$special[$a], static::$special[$b]); |
|
| 575 | 60 | }); |
|
| 576 | |||
| 577 | 60 | return [$attributes, $special]; |
|
| 578 | } |
||
| 579 | |||
| 580 | 37 | protected function compileSpecialNode(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 581 | 37 | $specialName = key($special); |
|
| 582 | |||
| 583 | switch ($specialName) { |
||
| 584 | 37 | case self::T_COMPONENT: |
|
| 585 | 9 | $this->compileComponentRegister($node, $attributes, $special, $out); |
|
| 586 | 9 | break; |
|
| 587 | 37 | case self::T_IF: |
|
| 588 | 10 | $this->compileIf($node, $attributes, $special, $out); |
|
| 589 | 9 | break; |
|
| 590 | 36 | case self::T_EACH: |
|
| 591 | 15 | $this->compileEach($node, $attributes, $special, $out); |
|
| 592 | 14 | break; |
|
| 593 | 25 | case self::T_BLOCK: |
|
| 594 | 1 | $this->compileBlock($node, $attributes, $special, $out); |
|
| 595 | 1 | break; |
|
| 596 | 25 | case self::T_CHILDREN: |
|
| 597 | 3 | $this->compileChildBlock($node, $attributes, $special, $out); |
|
| 598 | 3 | break; |
|
| 599 | 25 | case self::T_INCLUDE: |
|
| 600 | 1 | $this->compileComponentInclude($node, $attributes, $special, $out); |
|
| 601 | 1 | break; |
|
| 602 | 25 | case self::T_WITH: |
|
| 603 | 5 | if ($this->isComponent($node->tagName)) { |
|
| 604 | // With has a special meaning in components. |
||
| 605 | 3 | $this->compileComponentInclude($node, $attributes, $special, $out); |
|
| 606 | } else { |
||
| 607 | 2 | $this->compileWith($node, $attributes, $special, $out); |
|
| 608 | } |
||
| 609 | 4 | break; |
|
| 610 | 23 | case self::T_LITERAL: |
|
| 611 | 2 | $this->compileLiteral($node, $attributes, $special, $out); |
|
| 612 | 2 | break; |
|
| 613 | 21 | case '': |
|
| 614 | 21 | if ($this->isComponent($node->tagName)) { |
|
| 615 | 7 | $this->compileComponentInclude($node, $attributes, $special, $out); |
|
| 616 | } else { |
||
| 617 | 20 | $this->compileElement($node, $attributes, $special, $out); |
|
| 618 | } |
||
| 619 | 21 | break; |
|
| 620 | 1 | case self::T_TAG: |
|
| 621 | default: |
||
| 622 | // This is only a tag node so it just gets compiled as an element. |
||
| 623 | 1 | $this->compileBasicElement($node, $attributes, $special, $out); |
|
| 624 | 1 | break; |
|
| 625 | } |
||
| 626 | 34 | } |
|
| 627 | |||
| 628 | /** |
||
| 629 | * Compile component registering. |
||
| 630 | * |
||
| 631 | * @param DOMElement $node |
||
| 632 | * @param $attributes |
||
| 633 | * @param $special |
||
| 634 | * @param CompilerBuffer $out |
||
| 635 | */ |
||
| 636 | 9 | public function compileComponentRegister(DOMElement $node, $attributes, $special, CompilerBuffer $out) { |
|
| 637 | 9 | $name = strtolower($special[self::T_COMPONENT]->value); |
|
| 638 | 9 | unset($special[self::T_COMPONENT]); |
|
| 639 | |||
| 640 | 9 | $prev = $out->select($name); |
|
| 641 | |||
| 642 | 9 | $varName = var_export($name, true); |
|
| 643 | 9 | $out->appendCode("\$this->defineComponent($varName, function (\$props = [], \$children = []) {\n"); |
|
| 644 | 9 | $out->pushScope(['this' => 'props']); |
|
| 645 | 9 | $out->indent(+1); |
|
| 646 | |||
| 647 | try { |
||
| 648 | 9 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
| 649 | 9 | } finally { |
|
| 650 | 9 | $out->popScope(); |
|
| 651 | 9 | $out->indent(-1); |
|
| 652 | 9 | $out->appendCode("});"); |
|
| 653 | 9 | $out->select($prev); |
|
| 654 | } |
||
| 655 | 9 | } |
|
| 656 | |||
| 657 | 1 | private function compileBlock(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 658 | 1 | $name = strtolower($special[self::T_BLOCK]->value); |
|
| 659 | 1 | unset($special[self::T_BLOCK]); |
|
| 660 | |||
| 661 | 1 | $prev = $out->select($name); |
|
| 662 | |||
| 663 | 1 | $use = '$'.implode(', $', array_unique($out->getScopeVariables())).', $children'; |
|
| 664 | |||
| 665 | 1 | $out->appendCode("function () use ($use) {\n"); |
|
| 666 | 1 | $out->pushScope(['this' => 'props']); |
|
| 667 | 1 | $out->indent(+1); |
|
| 668 | |||
| 669 | try { |
||
| 670 | 1 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
| 671 | 1 | } finally { |
|
| 672 | 1 | $out->indent(-1); |
|
| 673 | 1 | $out->popScope(); |
|
| 674 | 1 | $out->appendCode("}"); |
|
| 675 | 1 | $out->select($prev); |
|
| 676 | } |
||
| 677 | |||
| 678 | 1 | return $out; |
|
| 679 | } |
||
| 680 | |||
| 681 | /** |
||
| 682 | * Compile component inclusion and rendering. |
||
| 683 | * |
||
| 684 | * @param DOMElement $node |
||
| 685 | * @param DOMAttr[] $attributes |
||
| 686 | * @param DOMAttr[] $special |
||
| 687 | * @param CompilerBuffer $out |
||
| 688 | */ |
||
| 689 | 11 | protected function compileComponentInclude(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 690 | // Generate the attributes into a property array. |
||
| 691 | 11 | $props = []; |
|
| 692 | 11 | foreach ($attributes as $name => $attribute) { |
|
| 693 | /* @var DOMAttr $attr */ |
||
| 694 | 5 | if ($this->isExpression($attribute->value)) { |
|
| 695 | 4 | $expr = $this->expr(substr($attribute->value, 1, -1), $out, $attribute); |
|
| 696 | } else { |
||
| 697 | 1 | $expr = var_export($attribute->value, true); |
|
| 698 | } |
||
| 699 | |||
| 700 | 5 | $props[] = var_export($name, true).' => '.$expr; |
|
| 701 | } |
||
| 702 | 11 | $propsStr = '['.implode(', ', $props).']'; |
|
| 703 | |||
| 704 | 11 | if (isset($special[self::T_WITH])) { |
|
| 705 | 3 | $withExpr = $this->expr($special[self::T_WITH]->value, $out, $special[self::T_WITH]); |
|
| 706 | 3 | unset($special[self::T_WITH]); |
|
| 707 | |||
| 708 | 3 | $propsStr = empty($props) ? $withExpr : $propsStr.' + (array)'.$withExpr; |
|
| 709 | } elseif (empty($props)) { |
||
| 710 | // By default the current context is passed to components. |
||
| 711 | 4 | $propsStr = $this->expr('this', $out); |
|
| 712 | } |
||
| 713 | |||
| 714 | // Compile the children blocks. |
||
| 715 | 11 | $blocks = $this->compileComponentBlocks($node, $out); |
|
| 716 | 11 | $blocksStr = $blocks->flush(); |
|
| 717 | |||
| 718 | 11 | if (isset($special[self::T_INCLUDE])) { |
|
| 719 | 1 | $name = $this->expr($special[self::T_INCLUDE]->value, $out, $special[self::T_INCLUDE]); |
|
| 720 | } else { |
||
| 721 | 10 | $name = var_export($node->tagName, true); |
|
| 722 | } |
||
| 723 | |||
| 724 | 11 | $out->appendCode("\$this->write($name, $propsStr, $blocksStr);\n"); |
|
| 725 | 11 | } |
|
| 726 | |||
| 727 | /** |
||
| 728 | * @param DOMElement $parent |
||
| 729 | * @return CompilerBuffer |
||
| 730 | */ |
||
| 731 | 11 | protected function compileComponentBlocks(DOMElement $parent, CompilerBuffer $out) { |
|
| 732 | 11 | $blocksOut = new CompilerBuffer(CompilerBuffer::STYLE_ARRAY, [ |
|
| 733 | 11 | 'baseIndent' => $out->getIndent(), |
|
| 734 | 11 | 'indent' => $out->getIndent() + 1, |
|
| 735 | 11 | 'depth' => $out->getDepth(), |
|
| 736 | 11 | 'scopes' => $out->getAllScopes() |
|
| 737 | ]); |
||
| 738 | |||
| 739 | 11 | if ($this->isEmptyNode($parent)) { |
|
| 740 | 9 | return $blocksOut; |
|
| 741 | } |
||
| 742 | |||
| 743 | 3 | $use = '$'.implode(', $', $blocksOut->getScopeVariables()).', $children'; |
|
| 744 | |||
| 745 | 3 | $blocksOut->appendCode("function () use ($use) {\n"); |
|
| 746 | 3 | $blocksOut->indent(+1); |
|
| 747 | |||
| 748 | try { |
||
| 749 | 3 | foreach ($parent->childNodes as $node) { |
|
| 750 | 3 | $this->compileNode($node, $blocksOut); |
|
| 751 | } |
||
| 752 | 3 | } finally { |
|
| 753 | 3 | $blocksOut->indent(-1); |
|
| 754 | 3 | $blocksOut->appendCode("}"); |
|
| 755 | } |
||
| 756 | |||
| 757 | 3 | return $blocksOut; |
|
| 758 | } |
||
| 759 | |||
| 760 | 28 | protected function compileTagComment(DOMElement $node, $attributes, $special, CompilerBuffer $out) { |
|
| 761 | // Don't double up comments. |
||
| 762 | 28 | if ($node->previousSibling && $node->previousSibling->nodeType === XML_COMMENT_NODE) { |
|
| 763 | return; |
||
| 764 | } |
||
| 765 | |||
| 766 | 28 | $str = '<'.$node->tagName; |
|
| 767 | 28 | foreach ($special as $attr) { |
|
| 768 | /* @var DOMAttr $attr */ |
||
| 769 | 28 | $str .= ' '.$attr->name.(empty($attr->value) ? '' : '="'.htmlspecialchars($attr->value).'"'); |
|
| 770 | } |
||
| 771 | 28 | $str .= '>'; |
|
| 772 | 28 | $comments = explode("\n", $str); |
|
| 773 | 28 | foreach ($comments as $comment) { |
|
| 774 | 28 | $out->appendCode("// $comment\n"); |
|
| 775 | } |
||
| 776 | 28 | } |
|
| 777 | |||
| 778 | /** |
||
| 779 | * @param DOMElement $node |
||
| 780 | * @param DOMAttr[] $attributes |
||
| 781 | * @param DOMAttr[] $special |
||
| 782 | * @param CompilerBuffer $out |
||
| 783 | * @param bool $force |
||
| 784 | */ |
||
| 785 | 50 | protected function compileOpenTag(DOMElement $node, $attributes, $special, CompilerBuffer $out, $force = false) { |
|
| 786 | 50 | $tagNameExpr = !empty($special[self::T_TAG]) ? $special[self::T_TAG]->value : ''; |
|
| 787 | |||
| 788 | 50 | if ($node->tagName === self::T_X && empty($tagNameExpr)) { |
|
| 789 | 5 | return; |
|
| 790 | } |
||
| 791 | |||
| 792 | 48 | if (!empty($tagNameExpr)) { |
|
| 793 | 1 | $tagNameExpr = $this->expr($tagNameExpr, $out, $special[self::T_TAG]); |
|
| 794 | 1 | $out->echoLiteral('<'); |
|
| 795 | 1 | $out->echoCode($tagNameExpr); |
|
| 796 | } else { |
||
| 797 | 48 | $out->echoLiteral('<'.$node->tagName); |
|
| 798 | } |
||
| 799 | |||
| 800 | /* @var DOMAttr $attribute */ |
||
| 801 | 48 | foreach ($attributes as $name => $attribute) { |
|
| 802 | // Check for an attribute expression. |
||
| 803 | 17 | if ($this->isExpression($attribute->value)) { |
|
| 804 | 13 | $out->echoCode( |
|
| 805 | 13 | '$this->attribute('.var_export($name, true).', '. |
|
| 806 | 13 | $this->expr(substr($attribute->value, 1, -1), $out, $attribute). |
|
| 807 | 13 | ')'); |
|
| 808 | 5 | } elseif (null !== $fn = $this->getAttributeFunction($attribute)) { |
|
| 809 | 3 | $value = call_user_func($fn, var_export($attribute->value, true)); |
|
| 810 | |||
| 811 | 3 | $out->echoCode('$this->attribute('.var_export($name, true).', '.$value.')'); |
|
| 812 | 4 | } elseif ((empty($attribute->value) || $attribute->value === $name) && isset(self::$boolAttributes[$name])) { |
|
| 813 | 2 | $out->echoLiteral(' '.$name); |
|
| 814 | } else { |
||
| 815 | 4 | $out->echoLiteral(' '.$name.'="'); |
|
| 816 | 4 | $out->echoLiteral(htmlspecialchars($attribute->value)); |
|
| 817 | 17 | $out->echoLiteral('"'); |
|
| 818 | } |
||
| 819 | } |
||
| 820 | |||
| 821 | 48 | if ($node->hasChildNodes() || $force) { |
|
| 822 | 46 | $out->echoLiteral('>'); |
|
| 823 | } else { |
||
| 824 | 3 | $out->echoLiteral(" />"); |
|
| 825 | } |
||
| 826 | 48 | } |
|
| 827 | |||
| 828 | 20 | private function isExpression($value) { |
|
| 829 | 20 | return preg_match('`^{\S.*}$`', $value); |
|
| 830 | } |
||
| 831 | |||
| 832 | 49 | protected function compileCloseTag(DOMElement $node, $special, CompilerBuffer $out, $force = false) { |
|
| 833 | 49 | if (!$force && !$node->hasChildNodes()) { |
|
| 834 | 3 | return; |
|
| 835 | } |
||
| 836 | |||
| 837 | 47 | $tagNameExpr = !empty($special[self::T_TAG]) ? $special[self::T_TAG]->value : ''; |
|
| 838 | 47 | if (!empty($tagNameExpr)) { |
|
| 839 | 1 | $tagNameExpr = $this->expr($tagNameExpr, $out, $special[self::T_TAG]); |
|
| 840 | 1 | $out->echoLiteral('</'); |
|
| 841 | 1 | $out->echoCode($tagNameExpr); |
|
| 842 | 1 | $out->echoLiteral('>'); |
|
| 843 | 47 | } elseif ($node->tagName !== self::T_X) { |
|
| 844 | 45 | $out->echoLiteral("</{$node->tagName}>"); |
|
| 845 | } |
||
| 846 | 47 | } |
|
| 847 | |||
| 848 | 4 | protected function isEmptyText(DOMNode $node) { |
|
| 849 | 4 | return $node instanceof \DOMText && empty(trim($node->data)); |
|
| 850 | } |
||
| 851 | |||
| 852 | 11 | protected function isEmptyNode(DOMNode $node) { |
|
| 853 | 11 | if (!$node->hasChildNodes()) { |
|
| 854 | 9 | return true; |
|
| 855 | } |
||
| 856 | |||
| 857 | 3 | foreach ($node->childNodes as $childNode) { |
|
| 858 | 3 | if ($childNode instanceof DOMElement) { |
|
| 859 | 1 | return false; |
|
| 860 | } |
||
| 861 | 2 | if ($childNode instanceof \DOMText && !$this->isEmptyText($childNode)) { |
|
| 862 | 2 | return false; |
|
| 863 | } |
||
| 864 | } |
||
| 865 | |||
| 866 | return true; |
||
| 867 | } |
||
| 868 | |||
| 869 | 10 | protected function compileIf(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 870 | 10 | $this->compileTagComment($node, $attributes, $special, $out); |
|
| 871 | 10 | $expr = $this->expr($special[self::T_IF]->value, $out, $special[self::T_IF]); |
|
| 872 | 9 | unset($special[self::T_IF]); |
|
| 873 | |||
| 874 | 9 | $elseNode = $this->findSpecialNode($node, self::T_ELSE, self::T_IF); |
|
| 875 | 9 | $out->setNodeProp($elseNode, 'skip', true); |
|
| 876 | |||
| 877 | 9 | $out->appendCode('if ('.$expr.") {\n"); |
|
| 878 | 9 | $out->indent(+1); |
|
| 879 | |||
| 880 | 9 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
| 881 | |||
| 882 | 9 | $out->indent(-1); |
|
| 883 | |||
| 884 | 9 | if ($elseNode) { |
|
| 885 | 2 | list($attributes, $special) = $this->splitAttributes($elseNode); |
|
| 886 | 2 | unset($special[self::T_ELSE]); |
|
| 887 | |||
| 888 | 2 | $out->appendCode("} else {\n"); |
|
| 889 | |||
| 890 | 2 | $out->indent(+1); |
|
| 891 | 2 | $this->compileSpecialNode($elseNode, $attributes, $special, $out); |
|
| 892 | 2 | $out->indent(-1); |
|
| 893 | } |
||
| 894 | |||
| 895 | 9 | $out->appendCode("}\n"); |
|
| 896 | 9 | } |
|
| 897 | |||
| 898 | 15 | protected function compileEach(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 899 | 15 | $this->compileTagComment($node, $attributes, $special, $out); |
|
| 900 | 15 | $this->compileOpenTag($node, $attributes, $special, $out); |
|
| 901 | |||
| 902 | 15 | $emptyNode = $this->findSpecialNode($node, self::T_EMPTY, self::T_ELSE); |
|
| 903 | 15 | $out->setNodeProp($emptyNode, 'skip', true); |
|
| 904 | |||
| 905 | 15 | if ($emptyNode === null) { |
|
| 906 | 13 | $this->compileEachLoop($node, $attributes, $special, $out); |
|
| 907 | } else { |
||
| 908 | 2 | $expr = $this->expr("empty({$special[self::T_EACH]->value})", $out); |
|
| 909 | |||
| 910 | 2 | list ($emptyAttributes, $emptySpecial) = $this->splitAttributes($emptyNode); |
|
| 911 | 2 | unset($emptySpecial[self::T_EMPTY]); |
|
| 912 | |||
| 913 | 2 | $out->appendCode('if ('.$expr.") {\n"); |
|
| 914 | |||
| 915 | 2 | $out->indent(+1); |
|
| 916 | 2 | $this->compileSpecialNode($emptyNode, $emptyAttributes, $emptySpecial, $out); |
|
| 917 | 2 | $out->indent(-1); |
|
| 918 | |||
| 919 | 2 | $out->appendCode("} else {\n"); |
|
| 920 | |||
| 921 | 2 | $out->indent(+1); |
|
| 922 | 2 | $this->compileEachLoop($node, $attributes, $special, $out); |
|
| 923 | 2 | $out->indent(-1); |
|
| 924 | |||
| 925 | 2 | $out->appendCode("}\n"); |
|
| 926 | } |
||
| 927 | |||
| 928 | 14 | $this->compileCloseTag($node, $special, $out); |
|
| 929 | 14 | } |
|
| 930 | |||
| 931 | 2 | protected function compileWith(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 932 | 2 | $this->compileTagComment($node, $attributes, $special, $out); |
|
| 933 | |||
| 934 | 2 | $out->depth(+1); |
|
| 935 | 2 | $scope = ['this' => $out->depthName('props')]; |
|
| 936 | 2 | if (!empty($special[self::T_AS])) { |
|
| 937 | 2 | if (preg_match(self::IDENT_REGEX, $special[self::T_AS]->value, $m)) { |
|
| 938 | // The template specified an x-as attribute to alias the with expression. |
||
| 939 | 1 | $scope = [$m[1] => $out->depthName('props')]; |
|
| 940 | } else { |
||
| 941 | 1 | throw $out->createCompilerException( |
|
| 942 | 1 | $special[self::T_AS], |
|
| 943 | 1 | new \Exception("Invalid identifier \"{$special[self::T_AS]->value}\" in x-as attribute.") |
|
| 944 | ); |
||
| 945 | } |
||
| 946 | } |
||
| 947 | 1 | $with = $this->expr($special[self::T_WITH]->value, $out); |
|
| 948 | |||
| 949 | 1 | unset($special[self::T_WITH], $special[self::T_AS]); |
|
| 950 | |||
| 951 | 1 | $out->pushScope($scope); |
|
| 952 | 1 | $out->appendCode('$'.$out->depthName('props')." = $with;\n"); |
|
| 953 | |||
| 954 | 1 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
| 955 | |||
| 956 | 1 | $out->depth(-1); |
|
| 957 | 1 | $out->popScope(); |
|
| 958 | 1 | } |
|
| 959 | |||
| 960 | 2 | protected function compileLiteral(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 961 | 2 | $this->compileTagComment($node, $attributes, $special, $out); |
|
| 962 | 2 | unset($special[self::T_LITERAL]); |
|
| 963 | |||
| 964 | 2 | $this->compileOpenTag($node, $attributes, $special, $out); |
|
| 965 | |||
| 966 | 2 | foreach ($node->childNodes as $childNode) { |
|
| 967 | 2 | $html = $childNode->ownerDocument->saveHTML($childNode); |
|
| 968 | 2 | $out->echoLiteral($html); |
|
| 969 | } |
||
| 970 | |||
| 971 | 2 | $this->compileCloseTag($node, $special, $out); |
|
| 972 | 2 | } |
|
| 973 | |||
| 974 | 20 | protected function compileElement(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 975 | 20 | $this->compileOpenTag($node, $attributes, $special, $out); |
|
| 976 | |||
| 977 | 20 | foreach ($node->childNodes as $childNode) { |
|
| 978 | 20 | $this->compileNode($childNode, $out); |
|
| 979 | } |
||
| 980 | |||
| 981 | 20 | $this->compileCloseTag($node, $special, $out); |
|
| 982 | 20 | } |
|
| 983 | |||
| 984 | /** |
||
| 985 | * Find a special node in relation to another node. |
||
| 986 | * |
||
| 987 | * This method is used to find things such as x-empty and x-else elements. |
||
| 988 | * |
||
| 989 | * @param DOMElement $node The node to search in relation to. |
||
| 990 | * @param string $attribute The name of the attribute to search for. |
||
| 991 | * @param string $parentAttribute The name of the parent attribute to resolve conflicts. |
||
| 992 | * @return DOMElement|null Returns the found element node or **null** if not found. |
||
| 993 | */ |
||
| 994 | 23 | protected function findSpecialNode(DOMElement $node, $attribute, $parentAttribute) { |
|
| 995 | // First look for a sibling after the node. |
||
| 996 | 23 | for ($sibNode = $node->nextSibling; $sibNode !== null; $sibNode = $sibNode->nextSibling) { |
|
| 997 | 2 | if ($sibNode instanceof DOMElement && $sibNode->hasAttribute($attribute)) { |
|
| 998 | 2 | return $sibNode; |
|
| 999 | } |
||
| 1000 | |||
| 1001 | // Stop searching if we encounter another node. |
||
| 1002 | 2 | if (!$this->isEmptyText($sibNode)) { |
|
| 1003 | break; |
||
| 1004 | } |
||
| 1005 | } |
||
| 1006 | |||
| 1007 | // Next look inside the node. |
||
| 1008 | 21 | $parentFound = false; |
|
| 1009 | 21 | foreach ($node->childNodes as $childNode) { |
|
| 1010 | 20 | if (!$parentFound && $childNode instanceof DOMElement && $childNode->hasAttribute($attribute)) { |
|
| 1011 | 2 | return $childNode; |
|
| 1012 | } |
||
| 1013 | |||
| 1014 | 20 | if ($childNode instanceof DOMElement) { |
|
| 1015 | 14 | $parentFound = $childNode->hasAttribute($parentAttribute); |
|
| 1016 | 9 | } elseif ($childNode instanceof \DOMText && !empty(trim($childNode->data))) { |
|
| 1017 | 20 | $parentFound = false; |
|
| 1018 | } |
||
| 1019 | } |
||
| 1020 | |||
| 1021 | 19 | return null; |
|
| 1022 | } |
||
| 1023 | |||
| 1024 | /** |
||
| 1025 | * @param DOMElement $node |
||
| 1026 | * @param array $attributes |
||
| 1027 | * @param array $special |
||
| 1028 | * @param CompilerBuffer $out |
||
| 1029 | */ |
||
| 1030 | 15 | private function compileEachLoop(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 1031 | 15 | $each = $this->expr($special[self::T_EACH]->value, $out); |
|
| 1032 | 15 | unset($special[self::T_EACH]); |
|
| 1033 | |||
| 1034 | 15 | $as = ['', $out->depthName('props', 1)]; |
|
| 1035 | 15 | $scope = ['this' => $as[1]]; |
|
| 1036 | 15 | if (!empty($special[self::T_AS])) { |
|
| 1037 | 8 | if (preg_match('`^(?:([a-z0-9]+)\s+)?([a-z0-9]+)$`i', $special[self::T_AS]->value, $m)) { |
|
| 1038 | 7 | $scope = [$m[2] => $as[1]]; |
|
| 1039 | 7 | if (!empty($m[1])) { |
|
| 1040 | 7 | $scope[$m[1]] = $as[0] = $out->depthName('i', 1); |
|
| 1041 | } |
||
| 1042 | } else { |
||
| 1043 | 1 | throw $out->createCompilerException( |
|
| 1044 | 1 | $special[self::T_AS], |
|
| 1045 | 1 | new \Exception("Invalid identifier \"{$special[self::T_AS]->value}\" in x-as attribute.") |
|
| 1046 | ); |
||
| 1047 | } |
||
| 1048 | } |
||
| 1049 | 14 | unset($special[self::T_AS]); |
|
| 1050 | 14 | if (empty($as[0])) { |
|
| 1051 | 10 | $out->appendCode("foreach ($each as \${$as[1]}) {\n"); |
|
| 1052 | } else { |
||
| 1053 | 4 | $out->appendCode("foreach ($each as \${$as[0]} => \${$as[1]}) {\n"); |
|
| 1054 | } |
||
| 1055 | 14 | $out->depth(+1); |
|
| 1056 | 14 | $out->indent(+1); |
|
| 1057 | 14 | $out->pushScope($scope); |
|
| 1058 | |||
| 1059 | 14 | foreach ($node->childNodes as $childNode) { |
|
| 1060 | 14 | $this->compileNode($childNode, $out); |
|
| 1061 | } |
||
| 1062 | |||
| 1063 | 14 | $out->indent(-1); |
|
| 1064 | 14 | $out->depth(-1); |
|
| 1065 | 14 | $out->popScope(); |
|
| 1066 | 14 | $out->appendCode("}\n"); |
|
| 1067 | 14 | } |
|
| 1068 | |||
| 1069 | 52 | protected function ltrim($text, \DOMNode $node, CompilerBuffer $out) { |
|
| 1070 | 52 | if ($this->inPre($node)) { |
|
| 1071 | return $text; |
||
| 1072 | } |
||
| 1073 | |||
| 1074 | 52 | $sib = $node->previousSibling ?: $node->parentNode; |
|
| 1075 | 52 | if ($sib === null || !$sib instanceof \DOMElement || $out->getNodeProp($sib, 'skip') || $sib->tagName === self::T_X) { |
|
| 1076 | 8 | return ltrim($text); |
|
| 1077 | } |
||
| 1078 | |||
| 1079 | 50 | $text = preg_replace('`^\s*\n\s*`', "\n", $text, -1, $count); |
|
| 1080 | 50 | if ($count === 0) { |
|
| 1081 | 48 | $text = preg_replace('`^\s+`', ' ', $text); |
|
| 1082 | } |
||
| 1083 | |||
| 1084 | // if ($sib !== null && ($sib->nodeType === XML_COMMENT_NODE || in_array($sib->tagName, static::$blocks))) { |
||
| 1085 | // return ltrim($text); |
||
| 1086 | // } |
||
| 1087 | 50 | return $text; |
|
| 1088 | } |
||
| 1089 | |||
| 1090 | 52 | protected function rtrim($text, \DOMNode $node, CompilerBuffer $out) { |
|
| 1091 | 52 | if ($this->inPre($node)) { |
|
| 1092 | return $text; |
||
| 1093 | } |
||
| 1094 | |||
| 1095 | 52 | $sib = $node->nextSibling ?: $node->parentNode; |
|
| 1096 | |||
| 1097 | 52 | if ($sib === null || !$sib instanceof \DOMElement || $out->getNodeProp($sib, 'skip') || $sib->tagName === self::T_X) { |
|
| 1098 | 7 | return rtrim($text); |
|
| 1099 | } |
||
| 1100 | |||
| 1101 | 49 | $text = preg_replace('`\s*\n\s*$`', "\n", $text, -1, $count); |
|
| 1102 | 49 | if ($count === 0) { |
|
| 1103 | 48 | $text = preg_replace('`\s+$`', ' ', $text); |
|
| 1104 | } |
||
| 1105 | |||
| 1106 | // if ($sib !== null && ($sib->nodeType === XML_COMMENT_NODE || in_array($sib->tagName, static::$blocks))) { |
||
| 1107 | // return rtrim($text); |
||
| 1108 | // } |
||
| 1109 | 49 | return $text; |
|
| 1110 | } |
||
| 1111 | |||
| 1112 | 52 | protected function inPre(\DOMNode $node) { |
|
| 1113 | 52 | for ($node = $node->parentNode; $node !== null; $node = $node->parentNode) { |
|
| 1114 | 52 | if (in_array($node->nodeType, ['code', 'pre'], true)) { |
|
| 1115 | return true; |
||
| 1116 | } |
||
| 1117 | } |
||
| 1118 | 52 | return false; |
|
| 1119 | } |
||
| 1120 | |||
| 1121 | 3 | private function compileChildBlock(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 1122 | /* @var DOMAttr $child */ |
||
| 1123 | 3 | $child = $special[self::T_CHILDREN]; |
|
| 1124 | 3 | unset($special[self::T_CHILDREN]); |
|
| 1125 | |||
| 1126 | 3 | $key = $child->value === '' ? 0 : $child->value; |
|
| 1127 | 3 | $keyStr = var_export($key, true); |
|
| 1128 | |||
| 1129 | 3 | $this->compileOpenTag($node, $attributes, $special, $out, true); |
|
| 1130 | |||
| 1131 | 3 | $out->appendCode("if (isset(\$children[{$keyStr}])) {\n"); |
|
| 1132 | 3 | $out->indent(+1); |
|
| 1133 | 3 | $out->appendCode("\$children[{$keyStr}]();\n"); |
|
| 1134 | 3 | $out->indent(-1); |
|
| 1135 | 3 | $out->appendCode("}\n"); |
|
| 1136 | |||
| 1137 | 3 | $this->compileCloseTag($node, $special, $out, true); |
|
| 1138 | 3 | } |
|
| 1139 | |||
| 1140 | /** |
||
| 1141 | * Compile an x-expr node. |
||
| 1142 | * |
||
| 1143 | * @param DOMElement $node The node to compile. |
||
| 1144 | * @param DOMAttr[] $attributes The node's attributes. |
||
| 1145 | * @param DOMAttr[] $special An array of special attributes. |
||
| 1146 | * @param CompilerBuffer $out The compiler output. |
||
| 1147 | */ |
||
| 1148 | 8 | private function compileExpressionNode(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
| 1149 | 8 | $str = $raw = $node->nodeValue; |
|
| 1150 | |||
| 1151 | try { |
||
| 1152 | 8 | $expr = $this->expr($str, $out); |
|
| 1153 | 1 | } catch (SyntaxError $ex) { |
|
| 1154 | 1 | throw $out->createCompilerException($node, $ex); |
|
| 1155 | } |
||
| 1156 | |||
| 1157 | 7 | if (!empty($special[self::T_AS])) { |
|
| 1158 | 5 | if (preg_match(self::IDENT_REGEX, $special[self::T_AS]->value, $m)) { |
|
| 1159 | // The template specified an x-as attribute to alias the with expression. |
||
| 1160 | 4 | $out->depth(+1); |
|
| 1161 | 4 | $scope = [$m[1] => $out->depthName('expr')]; |
|
| 1162 | 4 | $out->pushScope($scope); |
|
| 1163 | 4 | $out->appendCode('$'.$out->depthName('expr')." = $expr;\n"); |
|
| 1164 | } else { |
||
| 1165 | 1 | throw $out->createCompilerException( |
|
| 1166 | 1 | $special[self::T_AS], |
|
| 1167 | 5 | new \Exception("Invalid identifier \"{$special[self::T_AS]->value}\" in x-as attribute.") |
|
| 1168 | ); |
||
| 1169 | } |
||
| 1170 | 2 | } elseif (!empty($special[self::T_UNESCAPE])) { |
|
| 1171 | 1 | $out->echoCode($expr); |
|
| 1172 | } else { |
||
| 1173 | 1 | $out->echoCode($this->compileEscape($expr)); |
|
| 1174 | } |
||
| 1175 | 6 | } |
|
| 1176 | |||
| 1177 | /** |
||
| 1178 | * @param DOMElement $node |
||
| 1179 | * @param $attributes |
||
| 1180 | * @param $special |
||
| 1181 | * @param CompilerBuffer $out |
||
| 1182 | */ |
||
| 1183 | 29 | protected function compileBasicElement(DOMElement $node, $attributes, $special, CompilerBuffer $out) { |
|
| 1184 | 29 | $this->compileOpenTag($node, $attributes, $special, $out); |
|
| 1185 | |||
| 1186 | 29 | foreach ($node->childNodes as $childNode) { |
|
| 1187 | 27 | $this->compileNode($childNode, $out); |
|
| 1188 | } |
||
| 1189 | |||
| 1190 | 29 | $this->compileCloseTag($node, $special, $out); |
|
| 1191 | 29 | } |
|
| 1192 | |||
| 1193 | 28 | protected function compileEscape($php) { |
|
| 1194 | // return 'htmlspecialchars('.$php.')'; |
||
|
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
56% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them. Loading history...
|
|||
| 1195 | 28 | return '$this->escape('.$php.')'; |
|
| 1196 | } |
||
| 1197 | } |
||
| 1198 |
This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.
To visualize
will produce issues in the first and second line, while this second example
will produce no issues.