Files
2025-08-02 16:30:27 +02:00

217 lines
7.5 KiB
PHP

<?php
namespace KupShop\FeedGeneratorBundle\Configuration;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\AttributeNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\ConditionNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\CycleNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\DescriptionNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\ElementNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\ExpressionNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\INode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\MultilineNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\ValueNode;
class V8BuildingVisitor implements IConfigurationVisitor
{
protected bool $indent = false;
protected $outputJs;
protected $indentDepth = 0;
public function setIndent(bool $indent): self
{
$this->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;
}
}