indent = $indent; return $this; } public function initialize(): void { $this->indentDepth = 0; $this->outputJs = ''; // define __PHP object for relevant properties from PHP $this->addLine('var __PHP = {};'); $this->addLine('__PHP.__properties = PHP.__properties;'); $this->addLine('__PHP.__methods = PHP.__methods;'); $this->addLine('__PHP.__contextProperties = PHP.__contextProperties;'); $this->addLine('var __buildXML = function() {'); // clear custom global variables (see https://github.com/phpv8/v8js/pull/340) // skip properties, methods and contextProperties and: exit,sleep,print,var_dump,require,global,PHP,isFalse,strval,__XMLWriter,__buildXML,__reportCounters,console $this->addLine("for (const prop in global) { if (!Object.keys(__PHP.__properties).includes(prop) && !Object.keys(__PHP.__methods).includes(prop) && !Object.keys(__PHP.__contextProperties).includes(prop) && !['exit', 'sleep', 'print', 'var_dump', 'require', 'global', '__PHP', 'isFalse', 'strval' , '__XMLWriter', '__buildXML', '__reportCounters', 'reportCounter', 'console', 'dayjs', 'utils'].includes(prop)) { delete global[prop]; }}"); $this->addLine('var __xmlWriter = new __XMLWriter('.($this->indent ? 'true' : 'false').');'); } public function getOutput(): string { $this->indentDepth = 0; $this->addLine('return __xmlWriter.toString();'); $this->addLine('};'); return $this->outputJs; } /** * @param INode[] $nodes */ private function processNodes(array $nodes, int $depth = 0) { $close = true; foreach ($nodes as $node) { $this->indentDepth = $depth; if ($node->accept($this) && $node->hasNodes()) { $this->processNodes($node->getNodes(), $depth + 1); } $this->indentDepth = $depth; $node->accept($this, $close); } } public function visitConfiguration(Configuration $configuration, &$handle = null) { $this->processNodes($configuration->getNodes()); } public function visitAttribute(AttributeNode $node, &$handle) { if (empty($node->getValue())) { return null; } if ($handle) { $this->addLine('__xmlWriter.endAttribute();'); return null; } $this->addLine('__xmlWriter.startAttribute('.json_encode($node->getValue()).');'); return true; } public function visitCondition(ConditionNode $node, &$handle) { if (empty($node->getValue())) { return false; } if ($handle) { $this->addLine('}'); return false; } // Immediately invoked function execution $this->addLine('if ((function(){'); // maintain backward compatibility: zero in a string should evaluate to false $this->addLine(' const __condResult = '.$node->getValue().';'); $this->addLine(' return __condResult === "0" ? false : __condResult;'); $this->addLine('})()) {'); return true; } public function visitCycle(CycleNode $node, &$handle) { if (empty($node->getValue())) { return false; } if ($handle) { $this->addLine('}'); // $this->addLine('}})();'); return false; } // temporary variables approach // for `photos.slice(0, Math.min(photos.length, 10))` to work is still required to have separate wrapper for each object in MultiWrapper // $iterableName = '___iterable'.$this->indentDepth; // $indexName = !empty($node->getIndexAccessName()) ? $node->getIndexAccessName() : '___index'.$this->indentDepth;; // $this->addLine('(function(){ const '.$iterableName.' = (function(){ return '.$node->getValue().'; })()'); // $this->addLine('for (const '.$indexName.' in '.$iterableName.') {'); // if (!empty($node->getValueAccessName())) { // $this->addLine('const '.$node->getValueAccessName().' = '.$iterableName.'['.$indexName.'];'); // } // Object.entries() approach if (empty($node->getValueAccessName()) && empty($node->getIndexAccessName())) { // iterate without access to index/value $this->addLine('for (const [] of Object.entries((function(){'); $this->addLine(' return '.$node->getValue().';'); $this->addLine('})())) {'); } else { $this->addLine('for (const ['.($node->getIndexAccessName() ?: 'undefined').', '.($node->getValueAccessName() ?: 'undefined').'] of Object.entries((function(){'); $this->addLine(' return '.$node->getValue().';'); $this->addLine('})())) {'); } return true; } public function visitElement(ElementNode $node, &$handle) { if (empty($node->getValue())) { return false; } if ($handle) { $this->addLine('__xmlWriter.endElement();'); return false; } // TODO: support dynamic element/attribute names? $this->addLine('__xmlWriter.startElement('.json_encode($node->getValue()).');'); return true; } public function visitExpression(ExpressionNode $node, &$handle): bool { if ($handle || empty($node->getValue())) { return false; } // Immediately invoked function execution $this->addLine('__xmlWriter.text((function(){'); $this->addLine(' return '.$node->getValue().';'); $this->addLine('})());'); return false; } public function visitMultiline(MultilineNode $node, &$handle): bool { if ($handle || empty($node->getValue())) { return false; } // Immediately invoked function execution $this->addLine('__xmlWriter.text((function(){'); // indent content $this->addLine(' '.join(PHP_EOL.str_repeat(' ', $this->indentDepth + 1), explode(PHP_EOL, $node->getValue()))); $this->addLine(' return "";'); $this->addLine('})());'); return false; } public function visitValue(ValueNode $node, &$handle) { if (!$handle) { $this->addLine('__xmlWriter.text('.json_encode($node->getValue() ?? '').');'); } return false; } public function visitDescription(DescriptionNode $node, &$handle) { return true; } protected function addLine(string $line): void { $this->outputJs .= PHP_EOL.str_repeat(' ', $this->indentDepth).$line; } }