|
|
@ -7,18 +7,41 @@ namespace App\Engine; |
|
|
|
use App\Dom\Document; |
|
|
|
use App\Dom\Node; |
|
|
|
use App\Dom\NodeFunction; |
|
|
|
use App\Dom\NodeFunctionValue; |
|
|
|
use App\Dom\NodeValue; |
|
|
|
|
|
|
|
/** |
|
|
|
* Compiler that implements the cfnpp "language" and allows you to |
|
|
|
* apply stacked documents on top of each other, transforming them. |
|
|
|
* |
|
|
|
* @author Adam Pippin <hello@adampippin.ca> |
|
|
|
*/ |
|
|
|
class Cfnpp implements ICompile |
|
|
|
{ |
|
|
|
/** @var Document */ |
|
|
|
/** |
|
|
|
* Current document state. |
|
|
|
* @var Document |
|
|
|
*/ |
|
|
|
protected $document; |
|
|
|
|
|
|
|
/** @var array<string, callable> */ |
|
|
|
/** |
|
|
|
* Functions that can be called by the document. |
|
|
|
* |
|
|
|
* Signature: |
|
|
|
* callable(Node $node, Node $function) |
|
|
|
* |
|
|
|
* @var array<string, callable> |
|
|
|
*/ |
|
|
|
protected $functions; |
|
|
|
|
|
|
|
/** @var array<string, callable> */ |
|
|
|
/** |
|
|
|
* Functions that merge two documents, and will be |
|
|
|
* called with a node from the current state and |
|
|
|
* document being compiled. |
|
|
|
* |
|
|
|
* Signature: |
|
|
|
* callable(Node $current_state, Node $new_document, Node $function) |
|
|
|
* |
|
|
|
* @var array<string, callable> |
|
|
|
*/ |
|
|
|
protected $merge_functions; |
|
|
|
|
|
|
|
public function __construct() |
|
|
@ -26,6 +49,8 @@ class Cfnpp implements ICompile |
|
|
|
$this->document = new Document(); |
|
|
|
$this->functions = []; |
|
|
|
|
|
|
|
// Suppress, we'll move these out of here later and clean them up. |
|
|
|
// @phan-suppress-next-line PhanPluginUnknownClosureReturnType |
|
|
|
$this->registerMergeFunction('Replace', static function(Node $orig_p, Node $tgt_p, Node $t) { |
|
|
|
// todo: nodefunctionvalue |
|
|
|
|
|
|
@ -33,30 +58,64 @@ class Cfnpp implements ICompile |
|
|
|
$repl->setChildren($t->getChildren()); |
|
|
|
return $repl; |
|
|
|
}); |
|
|
|
// @phan-suppress-next-line PhanPluginUnknownClosureReturnType |
|
|
|
$this->registerFunction('Unset', static function(Node $node, Node $func) { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Register a function that can be called by the document. |
|
|
|
* |
|
|
|
* @param string $name |
|
|
|
* @param callable $callback callback(Node $node, Node $function): ?Node |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
public function registerFunction(string $name, callable $callback): void |
|
|
|
{ |
|
|
|
$this->functions[$name] = $callback; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Register a merge function that can be called by the document and passed |
|
|
|
* both the current state and the new document. |
|
|
|
* |
|
|
|
* @param string $name |
|
|
|
* @param callable $callback callback(Node $current, Node $new_doc, Node $function): ?Node |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
public function registerMergeFunction(string $name, callable $callback): void |
|
|
|
{ |
|
|
|
$this->merge_functions[$name] = $callback; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Set the current state that new compilations will be applied to. |
|
|
|
* |
|
|
|
* @param Document $document |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
public function setDocument(Document $document): void |
|
|
|
{ |
|
|
|
$this->document = $document; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Get the document representing the current state. |
|
|
|
* |
|
|
|
* @return Document |
|
|
|
*/ |
|
|
|
public function getDocument(): Document |
|
|
|
{ |
|
|
|
return $this->document; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Apply a new document to the current state. |
|
|
|
* |
|
|
|
* @param Document $document |
|
|
|
* @param IOptions $options |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
public function compile(Document $document, IOptions $options): void |
|
|
|
{ |
|
|
|
$this->runMergeFunctions($this->document, $document); |
|
|
@ -64,6 +123,24 @@ class Cfnpp implements ICompile |
|
|
|
$this->runFunctions($this->document); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Performs a basic recursive merge of two dom trees with target overwriting |
|
|
|
* original. |
|
|
|
* |
|
|
|
* - Maps will be merged |
|
|
|
* - Arrays will be appended |
|
|
|
* - Other values will be overwritten |
|
|
|
* - If there's any mismatch (e.g., array in original + map in target), then |
|
|
|
* everything's just replaced with whatever's in the target. |
|
|
|
* |
|
|
|
* The original DOM tree will represent the final state. The target tree will |
|
|
|
* be clobbered, as many nodes are simply reparented into the original tree |
|
|
|
* where appropriate. |
|
|
|
* |
|
|
|
* @param Node $original |
|
|
|
* @param Node $target |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
protected function merge(Node $original, Node $target): void |
|
|
|
{ |
|
|
|
if ($original->isArray() && $target->isArray()) |
|
|
@ -110,6 +187,15 @@ class Cfnpp implements ICompile |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Walks through two dom trees simultaneously, adds missing nodes from |
|
|
|
* target to original, and executes any 'merge functions' it encounters |
|
|
|
* along the way. |
|
|
|
* |
|
|
|
* @param Node $original |
|
|
|
* @param Node $target |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
protected function runMergeFunctions(Node $original, Node $target): void |
|
|
|
{ |
|
|
|
if ($target->isFunctionParent() && isset($this->merge_functions[$target[0]->getName()])) |
|
|
@ -155,6 +241,12 @@ class Cfnpp implements ICompile |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Walks through the DOM tree and executes any functions it encounters. |
|
|
|
* |
|
|
|
* @param Node $node |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
protected function runFunctions(Node $node): void |
|
|
|
{ |
|
|
|
if ($node->isFunctionParent() && isset($this->functions[$node[0]->getName()])) |
|
|
@ -181,11 +273,28 @@ class Cfnpp implements ICompile |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Runs the function referenced by the NodeFunction node, and returns the |
|
|
|
* result. |
|
|
|
* |
|
|
|
* @param Node $node |
|
|
|
* @param NodeFunction $function |
|
|
|
* @return ?Node |
|
|
|
*/ |
|
|
|
protected function runFunction(Node $node, NodeFunction $function): ?Node |
|
|
|
{ |
|
|
|
return $this->functions[$function->getName()]($node, $function); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Runs a merge function referenced by the NodeFunction, and returns the |
|
|
|
* result. |
|
|
|
* |
|
|
|
* @param Node $original |
|
|
|
* @param Node $target |
|
|
|
* @param NodeFunction $function |
|
|
|
* @return ?Node |
|
|
|
*/ |
|
|
|
protected function runMergeFunction(Node $original, Node $target, NodeFunction $function): ?Node |
|
|
|
{ |
|
|
|
return $this->merge_functions[$function->getName()]($original, $target, $function); |
|
|
|