Skip to content

Commit

Permalink
#22 Custom keywords (#28)
Browse files Browse the repository at this point in the history
* Implement custom keywords
* Allow to customize the class attribute
* Document custom keywords
  • Loading branch information
kylekatarnls committed Jul 19, 2016
1 parent 5069a20 commit 109f69b
Show file tree
Hide file tree
Showing 13 changed files with 671 additions and 138 deletions.
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,78 @@ http://pug-filters.selfbuild.fr/

https://github.com/kylekatarnls/jade-filter-base#readme

### Supports for custom keywords

You can add custom keywords, here are some examples:

**Anonymous function**:
```php
$pug->addKeyword('for', function ($args) {
return array(
'beginPhp' => 'for (' . $args . ') {',
'endPhp' => '}',
);
});

$pug->render('
for $i = 1; $i <= 3; $i++
p= i
');
```

This will render:
```html
<p>1</p>
<p>2</p>
<p>3</p>
```

Note that the existing ```for..in``` operator will have the precedance on this custom ```for``` keyword.

**Invokable class**:
```php
class UserKeyword
{
public function __invoke($arguments, $block, $keyWord)
{
$badges = array();
foreach ($block->nodes as $index => $tag) {
if ($tag->name === 'badge') {
$href = $tag->getAttribute('color');
$badges[] = $href['value'];
unset($block->nodes[$index]);
}
}

return array(
'begin' => '<div class="' . $keyWord . '" data-name="' . $arguments . '" data-badges="[' . implode(',', $badges) . ']">',
'end' => '</div>',
);
}
}

$pug->addKeyword('user', new UserKeyword());

$pug->render('
user Bob
badge(color="blue")
badge(color="red")
em Registered yesterday
');
```

This will render:
```html
<div class="user" data-name="Bob" data-badges="['blue', 'red']">
<em>Registered yesterday</em>
</div>
```

A keyword must return an array (containing **begin** and/or **end** entires) or a string (used as a **begin** entry).

The **begin** and **end** are rendered as raw HTML, but you can also use **beginPhp** and **endPhp** as in the first example to render PHP codes that will wrap the rendered block if there is one.


### Check requirements

To check if all requirements are ready to use Pug, use the requirements method:
Expand Down
5 changes: 5 additions & 0 deletions src/Jade/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ class Compiler extends MixinVisitor
* @var bool
*/
protected $restrictedScope = false;
/**
* @var array
*/
protected $customKeywords = array();
/**
* @var Jade
*/
Expand Down Expand Up @@ -100,6 +104,7 @@ protected function setOptions($options)
'restrictedScope' => 'boolean',
'indentSize' => 'integer',
'indentChar' => 'string',
'customKeywords' => 'array',
);

if ($options instanceof Jade) {
Expand Down
5 changes: 4 additions & 1 deletion src/Jade/Compiler/AttributesCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ protected function getClassesDisplayCode()
{
return trim($this->createCode(
'if (!empty($__classes)) { ' .
'?> class=' . $this->quote . '<?php echo $__classes; ?>' . $this->quote . '<?php ' .
'?> ' . (isset($this->options['classAttribute'])
? $this->options['classAttribute']
: 'class'
) . '=' . $this->quote . '<?php echo $__classes; ?>' . $this->quote . '<?php ' .
'} ' .
'unset($__classes); '
));
Expand Down
189 changes: 189 additions & 0 deletions src/Jade/Compiler/KeywordsCompiler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

namespace Jade\Compiler;

use Jade\Nodes\CaseNode;
use Jade\Nodes\CustomKeyword;
use Jade\Nodes\Each;
use Jade\Nodes\Filter;
use Jade\Nodes\When;

abstract class KeywordsCompiler extends AttributesCompiler
{
/**
* @param Nodes\CaseNode $node
*/
protected function visitCasenode(CaseNode $node)
{
$this->switchNode = $node;
$this->visit($node->block);

if (!isset($this->switchNode)) {
unset($this->switchNode);
$this->indents--;

$code = $this->createCode('}');
$this->buffer($code);
}
}

/**
* @param string $expression
* @param &array $arguments
*
* @return string
*/
protected function visitCase($expression, &$arguments)
{
if ('default' === $expression) {
return 'default:';
}

$arguments[] = $expression;

return 'case %s:';
}

/**
* @param Nodes\When $node
*/
protected function visitWhen(When $node)
{
$code = '';
$arguments = array();

if (isset($this->switchNode)) {
$code .= 'switch (%s) {';
$arguments[] = $this->switchNode->expr;
unset($this->switchNode);

$this->indents++;
}

$code .= $this->visitCase($node->expr, $arguments);

array_unshift($arguments, $code);

$code = call_user_func_array(array($this, 'createCode'), $arguments);

$this->buffer($code);

$this->visit($node->block);

$code = $this->createCode('break;');
$this->buffer($code . $this->newline());
}

/**
* @param Nodes\Filter $node
*
* @throws \InvalidArgumentException
*/
protected function visitFilter(Filter $node)
{
$filter = $this->getFilter($node->name);

// Filters can be either a iFilter implementation, nor a callable
if (is_string($filter) && class_exists($filter)) {
$filter = new $filter();
}
if (!is_callable($filter)) {
throw new \InvalidArgumentException($node->name . ': Filter must be callable', 18);
}
$this->buffer($filter($node, $this));
}

/**
* @param Nodes\Each $node
*/
protected function visitEach(Each $node)
{
//if (is_numeric($node->obj)) {
//if (is_string($node->obj)) {
//$serialized = serialize($node->obj);
if (isset($node->alternative)) {
$this->buffer($this->createCode(
'if (isset(%s) && %s) {',
$node->obj, $node->obj
));
$this->indents++;
}

$this->buffer(isset($node->key) && strlen($node->key) > 0
? $this->createCode(
'foreach (%s as %s => %s) {',
$node->obj, $node->key, $node->value
)
: $this->createCode(
'foreach (%s as %s) {',
$node->obj, $node->value
)
);

$this->indents++;
$this->visit($node->block);
$this->indents--;

$this->buffer($this->createCode('}'));

if (isset($node->alternative)) {
$this->indents--;
$this->buffer($this->createCode('} else {'));
$this->indents++;

$this->visit($node->alternative);
$this->indents--;

$this->buffer($this->createCode('}'));
}
}

protected function bufferCustomKeyword($data, $block)
{
if (isset($data['begin'])) {
$this->buffer($data['begin']);
}

if ($block) {
$this->indents++;
$this->visit($block);
$this->indents--;
}

if (isset($data['end'])) {
$this->buffer($data['end']);
}
}

/**
* @param Nodes\CustomKeyword $node
*/
protected function visitCustomKeyword(CustomKeyword $node)
{
$action = $this->options['customKeywords'][$node->keyWord];

$data = $action($node->args, $node->block, $node->keyWord);

if (is_string($data)) {
$data = array(
'begin' => $data,
);
}

if (!is_array($data) && !($data instanceof \ArrayAccess)) {
throw new \ErrorException("The keyword {$node->keyWord} returned an invalid value type, string or array was expected.", 33);
}

foreach (array('begin', 'end') as $key) {
$data[$key] = (isset($data[$key . 'Php'])
? $this->createCode($data[$key . 'Php'])
: ''
) . (isset($data[$key])
? $data[$key]
: ''
);
}

$this->bufferCustomKeyword($data, $node->block);
}
}
Loading

0 comments on commit 109f69b

Please sign in to comment.