src/Phug/Compiler/Compiler.php
<?php
namespace Phug;
// Node compilers
use Closure;
use Phug\Compiler\Element\BlockElement;
use Phug\Compiler\Event\CompileEvent;
use Phug\Compiler\Event\ElementEvent;
use Phug\Compiler\Event\NodeEvent;
use Phug\Compiler\Event\OutputEvent;
use Phug\Compiler\Layout;
use Phug\Compiler\Locator\FileLocator;
use Phug\Compiler\LocatorInterface;
use Phug\Compiler\NodeCompiler\AssignmentListNodeCompiler;
use Phug\Compiler\NodeCompiler\AssignmentNodeCompiler;
use Phug\Compiler\NodeCompiler\AttributeListNodeCompiler;
use Phug\Compiler\NodeCompiler\AttributeNodeCompiler;
use Phug\Compiler\NodeCompiler\BlockNodeCompiler;
use Phug\Compiler\NodeCompiler\CaseNodeCompiler;
use Phug\Compiler\NodeCompiler\CodeNodeCompiler;
use Phug\Compiler\NodeCompiler\CommentNodeCompiler;
use Phug\Compiler\NodeCompiler\ConditionalNodeCompiler;
use Phug\Compiler\NodeCompiler\DoctypeNodeCompiler;
use Phug\Compiler\NodeCompiler\DocumentNodeCompiler;
use Phug\Compiler\NodeCompiler\DoNodeCompiler;
use Phug\Compiler\NodeCompiler\EachNodeCompiler;
use Phug\Compiler\NodeCompiler\ElementNodeCompiler;
use Phug\Compiler\NodeCompiler\ExpressionNodeCompiler;
use Phug\Compiler\NodeCompiler\FilterNodeCompiler;
use Phug\Compiler\NodeCompiler\ForNodeCompiler;
use Phug\Compiler\NodeCompiler\ImportNodeCompiler;
use Phug\Compiler\NodeCompiler\KeywordNodeCompiler;
use Phug\Compiler\NodeCompiler\MixinCallNodeCompiler;
use Phug\Compiler\NodeCompiler\MixinNodeCompiler;
use Phug\Compiler\NodeCompiler\TextNodeCompiler;
use Phug\Compiler\NodeCompiler\VariableNodeCompiler;
use Phug\Compiler\NodeCompiler\WhenNodeCompiler;
use Phug\Compiler\NodeCompiler\WhileNodeCompiler;
// Nodes
use Phug\Compiler\NodeCompiler\YieldNodeCompiler;
use Phug\Compiler\NodeCompilerInterface;
use Phug\Compiler\NormalizerInterface;
use Phug\Compiler\Util\YieldHandlerTrait;
use Phug\Compiler\WithUpperLocatorInterface;
use Phug\Formatter\AbstractElement;
use Phug\Formatter\Element\TextElement;
use Phug\Formatter\ElementInterface;
use Phug\Parser\Node\AssignmentListNode;
use Phug\Parser\Node\AssignmentNode;
use Phug\Parser\Node\AttributeListNode;
use Phug\Parser\Node\AttributeNode;
use Phug\Parser\Node\BlockNode;
use Phug\Parser\Node\CaseNode;
use Phug\Parser\Node\CodeNode;
use Phug\Parser\Node\CommentNode;
use Phug\Parser\Node\ConditionalNode;
use Phug\Parser\Node\DoctypeNode;
use Phug\Parser\Node\DocumentNode;
use Phug\Parser\Node\DoNode;
use Phug\Parser\Node\EachNode;
use Phug\Parser\Node\ElementNode;
use Phug\Parser\Node\ExpressionNode;
use Phug\Parser\Node\FilterNode;
use Phug\Parser\Node\ForNode;
use Phug\Parser\Node\ImportNode;
use Phug\Parser\Node\KeywordNode;
use Phug\Parser\Node\MixinCallNode;
use Phug\Parser\Node\MixinNode;
use Phug\Parser\Node\TextNode;
use Phug\Parser\Node\VariableNode;
use Phug\Parser\Node\WhenNode;
use Phug\Parser\Node\WhileNode;
use Phug\Parser\Node\YieldNode;
use Phug\Parser\NodeInterface;
// Utils
use Phug\Util\ModuleContainerInterface;
use Phug\Util\Partial\ModuleContainerTrait;
use Phug\Util\SourceLocation;
class Compiler implements ModuleContainerInterface, CompilerInterface, WithUpperLocatorInterface
{
use ModuleContainerTrait;
use YieldHandlerTrait;
/**
* @var array<Formatter>
*/
private $formatters;
/**
* @var Parser
*/
private $parser;
/**
* @var LocatorInterface
*/
private $locator;
/**
* @var LocatorInterface|null
*/
private $upperLocator;
/**
* @var string
*/
private $path;
/**
* @var array
*/
private $importPaths;
/**
* @var array
*/
private $nodeCompilers;
/**
* @var array
*/
private $namedCompilers;
/**
* @var array[array[Block]]
*/
private $namedBlocks;
/**
* @var Layout
*/
private $layout;
/**
* @var NodeInterface
*/
private $currentNode;
/**
* @var CompilerInterface
*/
private $parentCompiler;
public function __construct($options = null)
{
$this->setOptionsDefaults($options ?: [], [
'paths' => [],
'debug' => false,
'default_tag' => 'div',
'default_doctype' => 'html',
'extensions' => ['', '.pug', '.jade'],
'get_file_contents' => 'file_get_contents',
'scope_each_variables' => true,
'on_compile' => null,
'on_output' => null,
'on_node' => null,
'on_element' => null,
'filename' => null,
'filters' => [],
'filter_resolvers' => [],
'includes' => [],
'parser_class_name' => Parser::class,
'formatter_class_name' => Formatter::class,
'locator_class_name' => FileLocator::class,
'compiler_modules' => [],
'allow_composite_extensions' => false,
'node_compilers' => [
AssignmentListNode::class => AssignmentListNodeCompiler::class,
AssignmentNode::class => AssignmentNodeCompiler::class,
AttributeListNode::class => AttributeListNodeCompiler::class,
AttributeNode::class => AttributeNodeCompiler::class,
BlockNode::class => BlockNodeCompiler::class,
YieldNode::class => YieldNodeCompiler::class,
CaseNode::class => CaseNodeCompiler::class,
CodeNode::class => CodeNodeCompiler::class,
CommentNode::class => CommentNodeCompiler::class,
ConditionalNode::class => ConditionalNodeCompiler::class,
DoctypeNode::class => DoctypeNodeCompiler::class,
DocumentNode::class => DocumentNodeCompiler::class,
DoNode::class => DoNodeCompiler::class,
EachNode::class => EachNodeCompiler::class,
KeywordNode::class => KeywordNodeCompiler::class,
ElementNode::class => ElementNodeCompiler::class,
ExpressionNode::class => ExpressionNodeCompiler::class,
FilterNode::class => FilterNodeCompiler::class,
ForNode::class => ForNodeCompiler::class,
ImportNode::class => ImportNodeCompiler::class,
MixinCallNode::class => MixinCallNodeCompiler::class,
MixinNode::class => MixinNodeCompiler::class,
TextNode::class => TextNodeCompiler::class,
VariableNode::class => VariableNodeCompiler::class,
WhenNode::class => WhenNodeCompiler::class,
WhileNode::class => WhileNodeCompiler::class,
],
]);
//Initialize parser to parse source code into an AST
$parserClassName = $this->getOption('parser_class_name');
if (!is_a($parserClassName, Parser::class, true)) {
throw new \InvalidArgumentException(
"Passed parser class $parserClassName is ".
'not a valid '.Parser::class
);
}
$this->parser = new $parserClassName($this->getOptions());
//Initialize the formatter to turn elements into PHTML
$this->initializeFormatter();
//Initialize the Locator to locate sources
$locatorClassName = $this->getOption('locator_class_name');
if (!is_a($locatorClassName, LocatorInterface::class, true)) {
throw new \InvalidArgumentException(
"Passed locator class $locatorClassName is ".
'not a valid '.LocatorInterface::class
);
}
$this->locator = new $locatorClassName($this->getOptions());
$this->nodeCompilers = [];
$this->namedCompilers = [];
$this->importPaths = [];
$this->parentCompiler = null;
foreach ($this->getOption('node_compilers') as $className => $handler) {
$this->setNodeCompiler($className, $handler);
}
if ($onCompile = $this->getOption('on_compile')) {
$this->attach(CompilerEvent::COMPILE, $onCompile);
}
if ($onOutput = $this->getOption('on_output')) {
$this->attach(CompilerEvent::OUTPUT, $onOutput);
}
if ($onNode = $this->getOption('on_node')) {
$this->attach(CompilerEvent::NODE, $onNode);
}
if ($onElement = $this->getOption('on_element')) {
$this->attach(CompilerEvent::ELEMENT, $onElement);
}
$this->addModules($this->getOption('compiler_modules'));
}
/**
* Reset layout and compilers cache on clone.
*/
public function reset()
{
$this->layout = null;
$this->namedCompilers = [];
$this->importPaths = [];
$this->importNodeYielded = false;
$this->importNode = null;
$this->parentCompiler = null;
}
/**
* Reset layout and compilers cache on clone.
*/
public function __clone()
{
$this->reset();
}
/**
* Get parent compiler (compiler of the file that included/extended the current file)
* or null if the file is not compiled via an import.
*
* @return CompilerInterface|null
*/
public function getParentCompiler()
{
return $this->parentCompiler;
}
/**
* Set the parent compiler (should be used to declare an import relation such as
* include/extend).
*
* @param CompilerInterface $compiler
*
* @return $this
*/
public function setParentCompiler(CompilerInterface $compiler)
{
$this->parentCompiler = $compiler;
return $this;
}
/**
* Set a master locator to use before the internal one.
*
* @param LocatorInterface|null $upperLocator
*/
public function setUpperLocator($upperLocator)
{
$this->upperLocator = $upperLocator;
}
/**
* Locate a file for a given path. Returns null if
* not found.
*
* @param string $path
* @param array $paths
*
* @return string|null
*/
public function locate($path, $paths = null)
{
$paths = $paths ?: $this->getOption('paths');
$extensions = $this->getOption('extensions');
$upperPath = $this->upperLocator
? $this->upperLocator->locate($path, $paths, $extensions)
: null;
return $upperPath ?: $this->locator->locate($path, $paths, $extensions);
}
/**
* Resolve a path using the base directories. Throw
* an exception if not found.
*
* @param string $path
* @param array $paths
*
* @throws CompilerException
*
* @return string
*/
public function resolve($path, $paths = null)
{
$resolvePath = $this->locate($path, $paths);
$this->assert(
$resolvePath || $this->hasOption('not_found_template'),
function () use ($path) {
return sprintf(
"Source file %s not found \nPaths: %s \nExtensions: %s",
$path,
implode(', ', $this->getOption('paths')),
implode(', ', $this->getOption('extensions'))
);
}
);
return $resolvePath;
}
/**
* Normalize the string of a relative or absolute path.
*
* @param string $path the path to normalize.
*
* @return string
*/
public function normalizePath($path)
{
if ($this->locator instanceof NormalizerInterface) {
return $this->locator->normalize($path);
}
return $path;
}
/**
* Return the contents for a given file path.
*
* @param string $path
*
* @return string
*/
public function getFileContents($path)
{
if ($path) {
return call_user_func($this->getOption('get_file_contents'), $path);
}
return $this->getOption('not_found_template');
}
/**
* Return the current layout extended if set.
*
* @return Layout
*/
public function getLayout()
{
return $this->layout;
}
/**
* Set the current layout to extend.
*
* @param Layout $layout layout extended
*
* @return $this
*/
public function setLayout(Layout $layout)
{
$this->layout = $layout;
return $this;
}
/**
* Returns the currently used Parser instance.
*
* @return Parser
*/
public function getParser()
{
return $this->parser;
}
/**
* Returns the currently used Formatter instance.
*
* @return Formatter
*/
public function getFormatter()
{
$debug = $this->getOption('debug');
return isset($this->formatters[$debug])
? $this->formatters[$debug]
: $this->initializeFormatter();
}
/**
* Set the node compiler for a given node class name.
*
* @param string $className node class name
* @param NodeCompilerInterface|string $handler handler
*
* @return $this
*/
public function setNodeCompiler($className, $handler)
{
if (!is_subclass_of($handler, NodeCompilerInterface::class)) {
throw new \InvalidArgumentException(
'Passed node compiler needs to implement '.
NodeCompilerInterface::class.'. '.
(is_object($handler) ? get_class($handler) : $handler).
' given.'
);
}
$this->nodeCompilers[$className] = $handler;
return $this;
}
/**
* Return list of blocks for a given name.
*
* @param $name
*
* @return mixed
*/
public function &getBlocksByName($name)
{
if (!isset($this->namedBlocks[$name])) {
$this->namedBlocks[$name] = [];
}
return $this->namedBlocks[$name];
}
/**
* Returns lists of blocks grouped by name.
*
* @return array
*/
public function getBlocks()
{
return $this->namedBlocks;
}
/**
* Returns PHTML from pug node input.
*
* @param NodeInterface $node input
* @param ElementInterface $parent optional parent element
*
* @throws CompilerException
*
* @return ElementInterface
*/
public function compileNode(NodeInterface $node, ElementInterface $parent = null)
{
$nodeEvent = new NodeEvent($node);
$this->trigger($nodeEvent);
$node = $nodeEvent->getNode();
$this->currentNode = $node;
foreach ($this->nodeCompilers as $className => $compiler) {
if (is_a($node, $className)) {
if (!($compiler instanceof NodeCompilerInterface)) {
$compiler = $this->getNamedCompiler($compiler);
}
$element = $compiler->compileNode($node, $parent);
if ($element instanceof ElementInterface && !($element instanceof BlockElement)) {
$elementEvent = new ElementEvent($nodeEvent, $element);
$this->trigger($elementEvent);
$element = $elementEvent->getElement();
}
return $element;
}
}
$this->throwException(
'No compiler found able to compile '.get_class($node),
$node
);
}
/**
* Replace a block by its nodes.
*
* @param BlockElement $block
* @param array $children
*/
public function replaceBlock(BlockElement $block, array $children = null)
{
if ($parent = $block->getParent()) {
foreach (array_reverse($children ?: $block->getChildren()) as $child) {
$parent->insertAfter($block, $child);
}
$previous = $block->getPreviousSibling();
if ($previous instanceof TextElement) {
$previous->setEnd(true);
}
$block->remove();
}
}
/**
* Replace each block by its compiled children.
*
* @throws CompilerException
*
* @return $this
*/
public function compileBlocks()
{
do {
$blockProceeded = 0;
foreach ($this->getBlocks() as $name => $blocks) {
foreach ($blocks as $block) {
$this->assert(
$block instanceof BlockElement,
'Unexpected block for the name '.$name,
$block instanceof AbstractElement ? $block->getOriginNode() : null
);
/** @var BlockElement $block */
if ($block->hasParent()) {
$this->replaceBlock($block);
$blockProceeded++;
}
}
}
} while ($blockProceeded);
return $this;
}
/**
* Import blocks named lists into the compiler.
*
* @param array $blocks
*
* @return $this
*/
public function importBlocks(array $blocks)
{
foreach ($blocks as $name => $list) {
foreach ($list as $block) {
/* @var BlockElement $block */
$block->addCompiler($this);
}
}
return $this;
}
/**
* Register a path as an imported path of the current
* compiling path.
*
* @param $path
*/
public function registerImportPath($path)
{
$current = $this->getPath();
if (!isset($this->importPaths[$current])) {
$this->importPaths[$current] = [];
}
$this->importPaths[$current][] = $path;
if ($this->parentCompiler) {
$this->parentCompiler->registerImportPath($path);
}
return $this;
}
/**
* The the paths imported (stored in local memory) by a given path.
*
* @param string|null $path
*
* @return array
*/
public function getImportPaths($path = null)
{
if ($path === null) {
return $this->importPaths;
}
return isset($this->importPaths[$path])
? $this->importPaths[$path]
: [];
}
/**
* The the paths imported (stored in local memory) for the current path.
*
* @return array
*/
public function getCurrentImportPaths()
{
return $this->getImportPaths($this->getPath());
}
/**
* Dump a debug tre for a given pug input.
*
* @param string $input pug input
* @param string $path optional path of the compiled source
*
* @return string
*/
public function dump($input, $path = null)
{
$element = $this->compileDocument($input, $path);
return $element instanceof ElementInterface
? $element->dump()
: var_export($element, true);
}
/**
* Dump a debug tre for a given pug input.
*
* @param string $path pug input file
*
* @return string
*/
public function dumpFile($path)
{
$path = $this->resolve($path);
return $this->dump($this->getFileContents($path), $path);
}
/**
* Returns ElementInterface from pug input with all layouts and
* blocks compiled.
*
* @param string $input pug input
* @param string $path optional path of the compiled source
*
* @throws CompilerException
*
* @return null|ElementInterface
*/
public function compileDocument($input, $path = null)
{
$element = $this->compileIntoElement($input, $path);
$layout = $this->getLayout();
$blocksCompiler = $this;
if ($layout) {
$element = $layout->getDocument();
$blocksCompiler = $layout->getCompiler();
}
$blocksCompiler->compileBlocks();
return $element;
}
/**
* Returns PHTML from pug input.
*
* @param string $input pug input
* @param string $path optional path of the compiled source
*
* @return string
*/
public function compile($input, $path = null)
{
$compileEvent = new CompileEvent($input, $path ?: $this->getOption('filename'));
$this->trigger($compileEvent);
$input = $compileEvent->getInput();
$path = $compileEvent->getPath();
$this->importPaths = [];
$includes = [];
foreach ($this->getOption('includes') as $include) {
$includes[] = $this->compileDocument(file_get_contents($include), $include);
}
$element = $this->compileDocument($input, $path);
foreach (array_reverse($includes) as $include) {
$element->prependChild($include);
}
$output = $this->getFormatter()->format($element);
$output = $this->getFormatter()->formatDependencies().$output;
$outputEvent = new OutputEvent($compileEvent, $output);
$this->trigger($outputEvent);
return $outputEvent->getOutput();
}
/**
* Returns PHTML from pug input file.
*
* @param string $path path of the compiled source
*
* @return string
*/
public function compileFile($path)
{
$path = $this->resolve($path);
return $this->compile($this->getFileContents($path), $path);
}
/**
* Returns ElementInterface from pug input.
*
* @param string $input pug input
* @param string $path optional path of the compiled source
*
* @throws \Exception
*
* @return null|ElementInterface
*/
public function compileIntoElement($input, $path = null)
{
$this->path = $path;
$this->namedBlocks = [];
$node = $this->parser->parse($input, $path); //Let exceptions fall through
$element = $this->compileNode($node);
if ($element && !($element instanceof ElementInterface)) {
$this->throwException(
get_class($node).
' compiled into a value that does not implement ElementInterface: '.
(is_object($element) ? get_class($element) : gettype($element)),
$node
);
}
return $element;
}
/**
* Returns ElementInterface from pug input file.
*
* @param string $path path of the compiled source
*
* @return ElementInterface
*/
public function compileFileIntoElement($path)
{
$path = $this->resolve($path);
return $this->compileIntoElement($this->getFileContents($path), $path);
}
/**
* Get the current file path compiling.
*
* @return string
*/
public function getPath()
{
return $this->path ?: $this->getOption('filename');
}
/**
* Get the class the compiler expects for its modules.
*
* @return string
*/
public function getModuleBaseClassName()
{
return CompilerModuleInterface::class;
}
/**
* Check if a filter is in use in the compiler.
*
* @see https://en.phug-lang.com/#filters
*
* @param string $name
*
* @return bool
*/
public function hasFilter($name)
{
return $this->getFilter($name) !== null;
}
/**
* Get a filter by name.
*
* @see https://en.phug-lang.com/#filters
*
* @param string $name
*
* @return callable|mixed|null
*/
public function getFilter($name)
{
$filters = $this->getOption('filters');
if (isset($filters[$name])) {
return $filters[$name];
}
foreach ($this->getOption('filter_resolvers') ?: [] as $resolver) {
$filter = call_user_func($resolver, $name);
if ($filter !== null) {
return $filter;
}
}
return null;
}
/**
* Set a filter for the current compiler.
*
* @see https://en.phug-lang.com/#filters
*
* @param string $name
* @param callable $filter
*
* @return Compiler|CompilerInterface
*/
public function setFilter($name, $filter)
{
return $this->setOption(['filters', $name], $filter);
}
/**
* Remove a filter from the compiler.
*
* @see https://en.phug-lang.com/#filters
*
* @param string $name
*
* @return Compiler|CompilerInterface
*/
public function unsetFilter($name)
{
return $this->unsetOption(['filters', $name]);
}
/**
* Throws a compiler-exception.
*
* The current file, line and offset of the exception
* get automatically appended to the exception
*
* @param string $message A meaningful error message
* @param NodeInterface $node Node generating the error
* @param int $code Error code
* @param \Throwable $previous Source error
*
* @throws CompilerException
*/
public function throwException($message, $node = null, $code = 0, $previous = null)
{
$location = $node ? $node->getSourceLocation() : null;
$path = $location ? $location->getPath() : $this->getPath();
$line = $location ? $location->getLine() : 0;
$offset = $location ? $location->getOffset() : 0;
$offsetLength = $location ? $location->getOffsetLength() : 0;
throw new CompilerException(
new SourceLocation($path, $line, $offset, $offsetLength),
CompilerException::message($message, [
'path' => $path,
'line' => $line,
'offset' => $offset,
]),
$code,
$previous
);
}
/**
* Throw an exception if given condition is false.
*
* @param bool $condition condition to validate
* @param string|Closure $message message to throw if condition isn't validated or a closure that returns it
* @param null $node optional node to get code position in error details
* @param int $code optional error code
* @param null $previous optional link to previous exception
*
* @throws CompilerException
*/
public function assert($condition, $message, $node = null, $code = 0, $previous = null)
{
if (!$condition) {
$this->throwException($message instanceof Closure ? $message() : $message, $node, $code, $previous);
}
}
/**
* Create a new compiler instance by name or return the previous
* instance with the same name.
*
* @param string $compiler name
*
* @return NodeCompilerInterface
*/
private function getNamedCompiler($compiler)
{
if (!isset($this->namedCompilers[$compiler])) {
$this->namedCompilers[$compiler] = new $compiler($this);
}
return $this->namedCompilers[$compiler];
}
/**
* Create the instance of formatter.
*
* @return Formatter
*/
private function initializeFormatter()
{
$formatterClassName = $this->getOption('formatter_class_name');
if (!is_a($formatterClassName, Formatter::class, true)) {
throw new \InvalidArgumentException(
"Passed formatter class $formatterClassName is ".
'not a valid '.Formatter::class
);
}
$debug = $this->getOption('debug');
$this->formatters[$debug] = new $formatterClassName($this->getOptions());
return $this->formatters[$debug];
}
}