Skip to content

Commit

Permalink
Stand-alone compilation & CLI!
Browse files Browse the repository at this point in the history
- Removed: handleErrors. Had almost no sense right now.
- Moved: Compiler helpers now reside in an own file and are represented
as functions (#8)
- Added: Stand-alone compilation by appending the helper file
automatically
- Added: Really basic command line utility (#12)
- Changed: You can now compile any file with an absolute path, no need
to pass a paths-array first
- One or two really, really minor bugfixes
  • Loading branch information
Torben Köhn committed Dec 3, 2015
1 parent 5e00c58 commit 960dfa0
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 236 deletions.
266 changes: 33 additions & 233 deletions Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,10 @@ class Compiler
* include-filters [extension => filter]
* escapeSequences: The escape-sequences that are possible in
* scalar strings
* handleErrors: Should the error-handler-helper be appended
* to the PTHML or not
* compileUncalledMixins: Always compile all mixins or leave out
* those that aren't called?
* standAlone: Allows the rendered files to be called
* without any requirements
* allowImports: Set to false to disable imports for this
* compiler instance. Importing will throw an
* exception. Great for demo-pages
Expand Down Expand Up @@ -307,8 +307,8 @@ public function __construct(array $options = null, Parser $parser = null, Lexer
'\r' => "\r",
'\t' => "\t"
],
'handleErrors' => true,
'compileUncalledMixins' => false,
'standAlone' => false,
'allowImports' => true,
'defaultTag' => 'div',
'quoteStyle' => '"',
Expand Down Expand Up @@ -455,14 +455,20 @@ public function compile($input, $path = null)
//Reset the level again for our next operations
$this->_level = 0;
//Now we append/prepend specific stuff (like mixin functions and helpers)
$errorHandler = $this->compileErrorHandlerHelper();
$mixins = $this->compileMixins();

$helpers = '';
if ($this->_options['standAlone']) {

$helpers = file_get_contents(__DIR__.'/Compiler/functions.php')."\n?>\n";
$helpers .= $this->createCode('namespace {');
}

//Put everything together
$phtml = implode('', [$errorHandler, $mixins, $phtml]);
$phtml = implode('', [$helpers, $mixins, $phtml]);

if ($this->_options['handleErrors'])
$phtml .= $this->createCode('restore_error_handler(); unset($__errorHandler);');
if ($this->_options['standAlone'])
$phtml .= $this->createCode('}');

//Reset the files after compilation so that compileFile may resolve correctly
//Happens when you call compileFile twice on different files
Expand Down Expand Up @@ -825,15 +831,21 @@ public function resolvePath($path, $extension = null)
if (substr($path, -strlen($ext)) !== $ext)
$path .= $ext;

//Check static path
if (file_exists($path))
return $path;

if (count($paths) < 1) {

//We got no paths to search in. We use the include-path in that case
$paths = explode(\PATH_SEPARATOR, get_include_path());
}

//Add the path were currently compiling in (e.g. include, extends)
if (count($this->_files) > 0)
$paths[] = dirname(end($this->_files));

//Iterate paths and check file existence via realpath
foreach ($paths as $directory) {

$fullPath = realpath(rtrim($directory, '/\\').'/'.ltrim($path, '/\\'));
Expand Down Expand Up @@ -1009,6 +1021,7 @@ protected function handleBlock(Node $node)

switch ($mode) {
default:
/** @noinspection PhpMissingBreakStatementInspection */
case 'replace':

$node->children = [];
Expand Down Expand Up @@ -1043,6 +1056,8 @@ protected function handleBlock(Node $node)

$block->mode = 'ignore';
}

return $this;
}

/**
Expand Down Expand Up @@ -1107,7 +1122,10 @@ protected function handleMixin(Node $node)
//Detach
$node->parent->remove($node);

$this->_mixins[$node->name] = ['node' => $node, 'phtml' => $this->compileChildren($node->children, false)];
$this->_mixins[$node->name] = [
'node' => $node,
'phtml' => $this->compileChildren($node->children, false)
];

return $this;
}
Expand Down Expand Up @@ -1154,7 +1172,6 @@ protected function compileMixins()
$i++;
}

$variadic = '';
if ($variadicIndex !== null) {

$args[$variadicName] = 'array_slice(\$__arguments, $variadicIndex);';
Expand Down Expand Up @@ -1532,8 +1549,6 @@ protected function compileFor(Node $node)
/**
* Compiles a do-instruction into PHTML.
*
* @todo Check for while-node with $node->next()
*
* @param Node $node the do-node to compile
*
* @return string The compiled PHTML
Expand Down Expand Up @@ -1817,22 +1832,22 @@ protected function compileElement(Node $node)
}

$quot = $this->_options['quoteStyle'];
$builder = '\Tale\Jade\Compiler::buildValue';
$builder = '\\Tale\\Jade\\Compiler\\build_value';

//Handle specific attribute styles for HTML
if ($this->_options['mode'] === self::MODE_HTML) {

switch ($name) {
case 'class':
$builder = '\Tale\Jade\Compiler::buildClassValue';
$builder = '\\Tale\\Jade\\Compiler\\build_class_value';
break;
case 'style':
$builder = '\Tale\Jade\Compiler::buildStyleValue';
$builder = '\\Tale\\Jade\\Compiler\\build_style_value';
break;
}

if (strncmp($name, 'data-', 5) === 0)
$builder = '\Tale\Jade\Compiler::buildDataValue';
$builder = '\\Tale\\Jade\\Compiler\\build_data_value';
}

//If all values are scalar, we don't do any kind of resolution for
Expand Down Expand Up @@ -1870,15 +1885,15 @@ protected function compileElement(Node $node)

$pair = $this->createCode(
'$__value = '.$values[0].'; '
.'if (!\\Tale\\Jade\\Compiler::isNullOrFalse($__value)) '
.'if (!\\Tale\\Jade\\Compiler\\is_null_or_false($__value)) '
."echo ' $name='.$builder(\$__value, '$quot', $escaped); "
.'unset($__value);'
);
} else {

$pair = $this->createCode(
'$__values = ['.implode(', ', $values).']; '
.'if (!\\Tale\\Jade\\Compiler::isArrayNullOrFalse($__values)) '
.'if (!\\Tale\\Jade\\Compiler\\is_array_null_or_false($__values)) '
."echo ' $name='.$builder(\$__values, '$quot', $escaped); "
.'unset($__values);'
);
Expand Down Expand Up @@ -2063,32 +2078,6 @@ protected function exportScalar($scalar, $quoteStyle = '\'')
return $quoteStyle.$this->compileScalar($scalar).$quoteStyle;
}

/**
* Compiles a simple error helper in a string to be prepended to the final PHTML.
*
* @return string The compiled PHTML for the error handler
*/
protected function compileErrorHandlerHelper()
{

$phtml = '';
if ($this->_options['handleErrors']) {

$phtml = $this->createCode(
'$__errorHandler = function($code, $message, $file, $line) {
if (!(error_reporting() & $code))
return;
throw new \ErrorException($message, 0, $code, $file, $line);
};
set_error_handler($__errorHandler);'
).$this->newLine();
}

return $phtml;
}

/**
* Throws a Compiler-Exception.
*
Expand All @@ -2097,7 +2086,7 @@ protected function compileErrorHandlerHelper()
*
* @throws Exception
*/
protected function throwException($message, \Tale\Jade\Parser\Node $relatedNode = null)
protected function throwException($message, Node $relatedNode = null)
{

if ($relatedNode)
Expand All @@ -2109,193 +2098,4 @@ protected function throwException($message, \Tale\Jade\Parser\Node $relatedNode
"Failed to compile Jade: $message"
);
}


/**
* Builds an attribute or argument value.
*
* Objects get converted to arrays
* Arrays will be imploded by '' (values are concatenated)
*
* ['a', 'b', ['c', ['d']]]
* will become
* 'abcd'
*
* The result will be enclosed by the quotes passed to $quoteStyle
*
* @param mixed $value The value to build
* @param string $quoteStyle The quoting style to use
* @param bool $escaped Escape the value or not
*
* @return string The built value
*/
public static function buildValue($value, $quoteStyle, $escaped)
{

if (is_object($value))
$value = (array)$value;

return $quoteStyle.($escaped ? htmlentities(is_array($value) ? self::flatten($value, '') : $value, \ENT_QUOTES) : ((string)$value)).$quoteStyle;
}

/**
* Builds a data-attribute value.
*
* If it's an object or an array, it gets converted to JSON automatically
* If not, the value stays scalar
*
* JSON will automatically be enclosed by ', other results will use
* $quoteStyle respectively
*
* 'a'
* will become
* 'a'
*
* ['a', 'b']
* will become
* '["a", "b"]' (JSON)
*
* @param mixed $value The value to build
* @param string $quoteStyle The quoting style to use
* @param bool $escaped Escape the value or not
*
* @return string The built value
*/
public static function buildDataValue($value, $quoteStyle, $escaped)
{

if (self::isObjectOrArray($value))
return '\''.json_encode($value).'\'';

return $quoteStyle.($escaped ? htmlentities($value, \ENT_QUOTES) : ((string)$value)).$quoteStyle;
}

/**
* Builds a style-attribute string from a value.
*
* ['color' => 'red', 'width: 100%', ['height' => '20px']]
* will become
* 'color: red; width: 100%; height: 20px;'
*
* @param mixed $value The value to build
* @param string $quoteStyle The quoting style to use
*
* @return string The built value
*/
public static function buildStyleValue($value, $quoteStyle)
{

if (is_object($value))
$value = (array)$value;

if (is_array($value))
$value = self::flatten($value, '; ', ': ');

return $quoteStyle.((string)$value).$quoteStyle;
}

/**
* Builds a class-attribute string from a value.
*
*['a', 'b', ['c', ['d', 'e']]]
* will become
* 'a b c d e'
*
* @param mixed $value The value to build
* @param string $quoteStyle The quoting style to use
*
* @return string The built value
*/
public static function buildClassValue($value, $quoteStyle)
{

if (is_object($value))
$value = (array)$value;

if (is_array($value))
$value = self::flatten($value);

return $quoteStyle.((string)$value).$quoteStyle;
}

/**
* Checks if a value is _exactly_ either null or false.
*
* @param mixed $value The value to check
*
* @return bool
*/
public static function isNullOrFalse($value)
{

return $value === null || $value === false;
}

/**
* Checks if a whole array is _exactly_ null or false.
*
* Not the array itself, but all values in the array
*
* @param array $value The array to check
*
* @return bool
*/
public static function isArrayNullOrFalse(array $value)
{

return count(array_filter($value, [self::class, 'isNullOrFalse'])) === count($value);
}

/**
* Checks if a value is either an object or an array.
*
* Kind of like !isScalar && !isExpression
*
* @param mixed $value The value to check
*
* @return bool
*/
public static function isObjectOrArray($value)
{

return is_object($value) || is_array($value);
}

/**
* Flattens an array and combines found values with $separator.
*
* If there are string-keys and an $argSeparator is set, it will
* also implode those to to a single value
*
* With the default options
* ['a', 'b' => 'c', ['d', 'e' => 'f', ['g' => 'h']]]
* will become
* 'a b=c d e=f g=h'
*
* @param array $array The array to flatten
* @param string $separator The separator to implode pairs with
* @param string $argSeparator The separator to implode keys and values with
*
* @return string The compiled string
*/
public static function flatten(array $array, $separator = ' ', $argSeparator = '=')
{

$items = [];
foreach ($array as $key => $value) {

if (is_object($value))
$value = (array)$value;

if (is_array($value))
$value = self::flatten($value, $separator, $argSeparator);

if (is_string($key))
$items[] = "$key$argSeparator$value";
else
$items[] = $value;
}

return implode($separator, $items);
}
}
Loading

0 comments on commit 960dfa0

Please sign in to comment.