feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run

- Base: wintercms/winter branch 1.2 (full framework)
- Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS
- Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication
- Partials: hero (offline-first), features, modes (offline/nube toggle),
  screenshots, pricing (3 planes), comparison, FAQ, CTA
- Plugin VivesPOS.Site with ContactForm
- Dockerfile: PHP 8.2 Apache, port 80, healthcheck
- Added winter/wn-pages, blog, sitemap, seo plugins
- Active theme set to vivespos
This commit is contained in:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a component node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class ComponentNode extends TwigNode
{
public function __construct(TwigNode $nodes, $paramNames, $lineno, $tag = 'component')
{
parent::__construct(['nodes' => $nodes], ['names' => $paramNames], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$compiler->addDebugInfo($this);
$compiler->write("\$context['__cms_component_params'] = [];\n");
for ($i = 1; $i < count($this->getNode('nodes')); $i++) {
$compiler->write("\$context['__cms_component_params']['".$this->getAttribute('names')[$i-1]."'] = ");
$compiler->subcompile($this->getNode('nodes')->getNode($i));
$compiler->write(";\n");
}
$compiler
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->componentFunction(")
->subcompile($this->getNode('nodes')->getNode(0))
->write(", \$context['__cms_component_params']")
->write(");\n")
;
$compiler->write("unset(\$context['__cms_component_params']);\n");
}
}

View File

@@ -0,0 +1,70 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
use Twig\Error\SyntaxError as TwigErrorSyntax;
/**
* Parser for the `{% component %}` Twig tag.
*
* {% component "pluginComponent" %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class ComponentTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return TwigNode A TwigNode instance
*/
public function parse(TwigToken $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $this->parser->getExpressionParser()->parseExpression();
$paramNames = [];
$nodes = [$name];
$end = false;
while (!$end) {
$current = $stream->next();
switch ($current->getType()) {
case TwigToken::NAME_TYPE:
$paramNames[] = $current->getValue();
$stream->expect(TwigToken::OPERATOR_TYPE, '=');
$nodes[] = $this->parser->getExpressionParser()->parseExpression();
break;
case TwigToken::BLOCK_END_TYPE:
$end = true;
break;
default:
throw new TwigErrorSyntax(
sprintf('Invalid syntax in the component tag. Line %s', $lineno),
$stream->getCurrent()->getLine(),
$stream->getSourceContext()
);
break;
}
}
return new ComponentNode(new TwigNode($nodes), $paramNames, $token->getLine(), $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'component';
}
}

View File

@@ -0,0 +1,48 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a content node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class ContentNode extends TwigNode
{
public function __construct(TwigNode $nodes, $paramNames, $lineno, $tag = 'content')
{
parent::__construct(['nodes' => $nodes], ['names' => $paramNames], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$compiler->addDebugInfo($this);
$compiler->write("\$context['__cms_content_params'] = [];\n");
for ($i = 1; $i < count($this->getNode('nodes')); $i++) {
$compiler->write("\$context['__cms_content_params']['".$this->getAttribute('names')[$i-1]."'] = ");
$compiler->write('twig_escape_filter($this->env, ');
$compiler->subcompile($this->getNode('nodes')->getNode($i));
$compiler->write(")");
$compiler->write(";\n");
}
$compiler
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->contentFunction(")
->subcompile($this->getNode('nodes')->getNode(0))
->write(", \$context['__cms_content_params']")
->write(", true")
->write(");\n")
;
$compiler->write("unset(\$context['__cms_content_params']);\n");
}
}

View File

@@ -0,0 +1,74 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
use Twig\Error\SyntaxError as TwigErrorSyntax;
/**
* Parser for the `{% content %}` Twig tag.
*
* {% content "intro.htm" %}
*
* {% content "intro.md" name='John' %}
*
* {% content "intro/txt" name='John', year=2013 %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class ContentTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return TwigNode A TwigNode instance
*/
public function parse(TwigToken $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $this->parser->getExpressionParser()->parseExpression();
$paramNames = [];
$nodes = [$name];
$end = false;
while (!$end) {
$current = $stream->next();
switch ($current->getType()) {
case TwigToken::NAME_TYPE:
$paramNames[] = $current->getValue();
$stream->expect(TwigToken::OPERATOR_TYPE, '=');
$nodes[] = $this->parser->getExpressionParser()->parseExpression();
break;
case TwigToken::BLOCK_END_TYPE:
$end = true;
break;
default:
throw new TwigErrorSyntax(
sprintf('Invalid syntax in the content tag. Line %s', $lineno),
$stream->getCurrent()->getLine(),
$stream->getSourceContext()
);
break;
}
}
return new ContentNode(new TwigNode($nodes), $paramNames, $token->getLine(), $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'content';
}
}

View File

@@ -0,0 +1,597 @@
<?php namespace Cms\Twig;
use Twig\Template as TwigTemplate;
use Twig\Extension\AbstractExtension as TwigExtension;
use Twig\Environment as TwigEnvironment;
use Twig\TwigFunction as TwigSimpleFunction;
use Cms\Classes\ComponentBase;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Winter\Storm\Database\Model;
class DebugExtension extends TwigExtension
{
const PAGE_CAPTION = 'Page variables';
const ARRAY_CAPTION = 'Array variables';
const OBJECT_CAPTION = 'Object variables';
const COMPONENT_CAPTION = 'Component variables';
/**
* @var integer Helper for rendering table row styles.
*/
protected $zebra = 1;
/**
* @var boolean If no variable is passed, true.
*/
protected $variablePrefix = false;
/**
* @var array Collection of method/property comments.
*/
protected $commentMap = [];
/**
* @var array Blocked object methods that should not be included in the dump.
*/
protected $blockMethods = [
'componentDetails',
'defineProperties',
'getPropertyOptions',
'offsetExists',
'offsetGet',
'offsetSet',
'offsetUnset'
];
/**
* Returns a list of global functions to add to the existing list.
* @return array An array of global functions
*/
public function getFunctions()
{
return [
new TwigSimpleFunction('dump', [$this, 'runDump'], [
'is_safe' => ['html'],
'needs_context' => true,
'needs_environment' => true
]),
];
}
/**
* Processes the dump variables, if none is supplied, all the twig
* template variables are used
* @param TwigEnvironment $env
* @param array $context
* @return string
*/
public function runDump(TwigEnvironment $env, $context)
{
if (!$env->isDebug()) {
return;
}
$result = '';
$count = func_num_args();
if ($count == 2) {
$this->variablePrefix = true;
$vars = [];
foreach ($context as $key => $value) {
if (!$value instanceof TwigTemplate) {
$vars[$key] = $value;
}
}
$result .= $this->dump($vars, static::PAGE_CAPTION);
}
else {
$this->variablePrefix = false;
for ($i = 2; $i < $count; $i++) {
$var = func_get_arg($i);
if ($var instanceof ComponentBase) {
$caption = [static::COMPONENT_CAPTION, get_class($var)];
}
elseif (is_array($var)) {
$caption = static::ARRAY_CAPTION;
}
elseif (is_object($var)) {
$caption = [static::OBJECT_CAPTION, get_class($var)];
}
else {
$caption = [static::OBJECT_CAPTION, gettype($var)];
}
$result .= $this->dump($var, $caption);
}
}
return $result;
}
/**
* Dump information about a variable
* @param mixed $variables Variable to dump
* @param mixed $caption Caption [and subcaption] of the dump
* @return void
*/
public function dump($variables = null, $caption = null)
{
$this->commentMap = [];
$this->zebra = 1;
$info = [];
if (!is_array($variables)) {
if ($variables instanceof Paginator) {
$variables = $this->paginatorToArray($variables);
}
elseif (is_object($variables)) {
$variables = $this->objectToArray($variables);
}
else {
$variables = [$variables];
}
}
$output = [];
$output[] = '<table>';
if ($caption) {
$output[] = $this->makeTableHeader($caption);
}
$output[] = '<tbody>';
foreach ($variables as $key => $item) {
$output[] = $this->makeTableRow($key, $item);
}
$output[] = '</tbody>';
$output[] = '</table>';
$html = implode(PHP_EOL, $output);
return '<pre style="' . $this->getContainerCss() . '">' . $html . '</pre>';
}
/**
* Builds the HTML used for the table header.
* @param mixed $caption Caption [and subcaption] of the dump
* @return string
*/
protected function makeTableHeader($caption)
{
if (is_array($caption)) {
list($caption, $subcaption) = $caption;
}
$output = [];
$output[] = '<thead>';
$output[] = '<tr>';
$output[] = '<th colspan="3" style="'.$this->getHeaderCss().'">';
$output[] = $caption;
if (isset($subcaption)) {
$output[] = '<div style="'.$this->getSubheaderCss().'">'.$subcaption.'</div>';
}
$output[] = '</th>';
$output[] = '</tr>';
$output[] = '</thead>';
return implode(PHP_EOL, $output);
}
/**
* Builds the HTML used for each table row.
* @param mixed $key
* @param mixed $variable
* @return string
*/
protected function makeTableRow($key, $variable)
{
$this->zebra = $this->zebra ? 0 : 1;
$css = $this->getDataCss($variable);
$output = [];
$output[] = '<tr>';
$output[] = '<td style="'.$css.';cursor:pointer" onclick="'.$this->evalToggleDumpOnClick().'">'.$this->evalKeyLabel($key).'</td>';
$output[] = '<td style="'.$css.'">'.$this->evalVarLabel($variable).'</td>';
$output[] = '<td style="'.$css.'">'.$this->evalVarDesc($variable, $key).'</td>';
$output[] = '</tr>';
$output[] = '<tr>';
$output[] = '<td colspan="3">'.$this->evalVarDump($variable).'</td>';
$output[] = '</tr>';
return implode(PHP_EOL, $output);
}
/**
* Builds JavaScript for toggling the dump container
* @return string
*/
protected function evalToggleDumpOnClick()
{
$output = "var d=this.parentElement.nextElementSibling.getElementsByTagName('div')[0];";
$output .= "d.style.display=='none'?d.style.display='block':d.style.display='none'";
return $output;
}
/**
* Dumps a variable using HTML Dumper, wrapped in a hidden DIV element.
* @param mixed $variable
* @return string
*/
protected function evalVarDump($variable)
{
$dumper = new HtmlDumper;
$cloner = new VarCloner;
$output = '<div style="display:none">';
$output .= $dumper->dump($cloner->cloneVar($variable), true);
$output .= '</div>';
return $output;
}
/**
* Returns a variable name as HTML friendly.
* @param string $key
* @return string
*/
protected function evalKeyLabel($key)
{
if ($this->variablePrefix === true) {
$output = '{{ <span>%s</span> }}';
}
elseif (is_array($this->variablePrefix)) {
$prefix = implode('.', $this->variablePrefix);
$output = '{{ <span>'.$prefix.'.%s</span> }}';
}
elseif ($this->variablePrefix) {
$output = '{{ <span>'.$this->variablePrefix.'.%s</span> }}';
}
else {
$output = '%s';
}
return sprintf($output, $key);
}
/**
* Evaluate the variable description
* @param mixed $variable
* @return string
*/
protected function evalVarLabel($variable)
{
$type = $this->getType($variable);
switch ($type) {
case 'object':
return $this->evalObjLabel($variable);
case 'array':
return $type . '('.count($variable).')';
default:
return $type;
}
}
/**
* Evaluate an object type for label
* @param object $variable
* @return string
*/
protected function getType($variable)
{
$type = gettype($variable);
if ($type == 'string' && substr($variable, 0, 12) == '___METHOD___') {
return 'method';
}
return $type;
}
/**
* Evaluate an object type for label
* @param object $variable
* @return string
*/
protected function evalObjLabel($variable)
{
$class = get_class($variable);
$label = class_basename($variable);
if ($variable instanceof ComponentBase) {
$label = '<strong>Component</strong>';
}
elseif ($variable instanceof Collection) {
$label = 'Collection('.$variable->count().')';
}
elseif ($variable instanceof Paginator) {
$label = 'Paged Collection('.$variable->count().')';
}
elseif ($variable instanceof Model) {
$label = 'Model';
}
return '<abbr title="'.e($class).'">'.$label.'</abbr>';
}
/**
* Evaluate the variable description
* @param mixed $variable
* @return string
*/
protected function evalVarDesc($variable, $key)
{
$type = $this->getType($variable);
if ($type == 'method') {
return $this->evalMethodDesc($variable);
}
if (isset($this->commentMap[$key])) {
return $this->commentMap[$key];
}
if ($type == 'array') {
return $this->evalArrDesc($variable);
}
if ($type == 'object') {
return $this->evalObjDesc($variable);
}
return '';
}
/**
* Evaluate an method type for description
* @param object $variable
* @return string
*/
protected function evalMethodDesc($variable)
{
$parts = explode('|', $variable);
if (count($parts) < 2) {
return null;
}
$method = $parts[1];
return $this->commentMap[$method] ?? null;
}
/**
* Evaluate an array type for description
* @param array $variable
* @return string
*/
protected function evalArrDesc($variable)
{
$output = [];
foreach ($variable as $key => $value) {
$output[] = '<abbr title="'.e(gettype($value)).'">'.$key.'</abbr>';
}
return implode(', ', $output);
}
/**
* Evaluate an object type for description
* @param array $variable
* @return string
*/
protected function evalObjDesc($variable)
{
$output = [];
if ($variable instanceof ComponentBase) {
$details = $variable->componentDetails();
$output[] = '<abbr title="'.array_get($details, 'description').'">';
$output[] = array_get($details, 'name');
$output[] = '</abbr>';
}
return implode('', $output);
}
//
// Object helpers
//
/**
* Returns default comment information for a paginator object.
* @param Illuminate\Pagination\Paginator $paginator
* @return array
*/
protected function paginatorToArray(Paginator $paginator)
{
$this->commentMap = [
'links()' => 'Renders links for navigating the collection',
'currentPage' => 'Get the current page for the request.',
'lastPage' => 'Get the last page that should be available.',
'perPage' => 'Get the number of items to be displayed per page.',
'total' => 'Get the total number of items in the complete collection.',
'from' => 'Get the number of the first item on the paginator.',
'to' => 'Get the number of the last item on the paginator.',
'count' => 'Returns the number of items in this collection',
];
return [
'links' => '___METHOD___|links()',
'currentPage' => '___METHOD___|currentPage',
'lastPage' => '___METHOD___|lastPage',
'perPage' => '___METHOD___|perPage',
'total' => '___METHOD___|total',
'from' => '___METHOD___|from',
'to' => '___METHOD___|to',
'count' => '___METHOD___|count',
];
}
/**
* Returns a map of an object as an array, containing methods and properties.
* @param mixed $object
* @return array
*/
protected function objectToArray($object)
{
$class = get_class($object);
$info = new \ReflectionClass($object);
$this->commentMap[$class] = [];
$methods = [];
foreach ($info->getMethods() as $method) {
if (!$method->isPublic()) {
continue; // Only public
}
if ($method->class != $class) {
continue; // Only locals
}
$name = $method->getName();
if (in_array($name, $this->blockMethods)) {
continue; // Blocked methods
}
if (preg_match('/^on[A-Z]{1}[\w+]*$/', $name)) {
continue; // AJAX methods
}
if (preg_match('/^get[A-Z]{1}[\w+]*Options$/', $name)) {
continue; // getSomethingOptions
}
if (substr($name, 0, 1) == '_') {
continue; // Magic/hidden method
}
$name .= '()';
$methods[$name] = '___METHOD___|'.$name;
$this->commentMap[$name] = $this->evalDocBlock($method);
}
$vars = [];
foreach ($info->getProperties() as $property) {
if ($property->isStatic()) {
continue; // Only non-static
}
if (!$property->isPublic()) {
continue; // Only public
}
if ($property->class != $class) {
continue; // Only locals
}
$name = $property->getName();
$vars[$name] = $object->{$name};
$this->commentMap[$name] = $this->evalDocBlock($property);
}
return $methods + $vars;
}
/**
* Extracts the comment from a DocBlock
* @param ReflectionClass $reflectionObj
* @return string
*/
protected function evalDocBlock($reflectionObj)
{
$comment = $reflectionObj->getDocComment();
$comment = substr($comment, 3, -2);
$parts = explode('@', $comment);
$comment = array_shift($parts);
$comment = trim(trim($comment), '*');
$comment = implode(' ', array_map('trim', explode('*', $comment)));
return $comment;
}
//
// Style helpers
//
/**
* Get the CSS string for the output data
* @param mixed $variable
* @return string
*/
protected function getDataCss($variable)
{
$css = [
'padding' => '7px',
'background-color' => $this->zebra ? '#D8D9DB' : '#FFF',
'color' => '#405261',
];
$type = gettype($variable);
if ($type == 'NULL') {
$css['color'] = '#999';
}
return $this->arrayToCss($css);
}
/**
* Get the CSS string for the output container
* @return string
*/
protected function getContainerCss()
{
return $this->arrayToCss([
'background-color' => '#F3F3F3',
'border' => '1px solid #bbb',
'border-radius' => '4px',
'font-size' => '12px',
'line-height' => '18px',
'margin' => '20px',
'padding' => '7px',
'display' => 'inline-block',
]);
}
/**
* Get the CSS string for the output header
* @return string
*/
protected function getHeaderCss()
{
return $this->arrayToCss([
'font-size' => '18px',
'font-weight' => 'normal',
'margin' => '0',
'padding' => '10px',
'background-color' => '#7B8892',
'color' => '#FFF',
]);
}
/**
* Get the CSS string for the output subheader
* @return string
*/
protected function getSubheaderCss()
{
return $this->arrayToCss([
'font-size' => '12px',
'font-weight' => 'normal',
'font-style' => 'italic',
'margin' => '0',
'padding' => '0',
'background-color' => '#7B8892',
'color' => '#FFF',
]);
}
/**
* Convert a key/value pair array into a CSS string
* @param array $rules List of rules to process
* @return string
*/
protected function arrayToCss(array $rules)
{
$strings = [];
foreach ($rules as $key => $value) {
$strings[] = $key . ': ' . $value;
}
return implode('; ', $strings);
}
}

View File

@@ -0,0 +1,31 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a "default" node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class DefaultNode extends TwigNode
{
public function __construct($lineno, $tag = 'default')
{
parent::__construct([], [], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$compiler
->addDebugInfo($this)
->write("echo '<!-- X_WINTER_DEFAULT_BLOCK_CONTENT -->';\n")
;
}
}

View File

@@ -0,0 +1,41 @@
<?php namespace Cms\Twig;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
/**
* Parser for the `{% default %}` Twig tag.
*
* {% put head %}
* <link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet"/>
* {% default %}
* {% endput %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class DefaultTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return Twig\Node\Node A Twig\Node\Node instance
*/
public function parse(TwigToken $token)
{
$stream = $this->parser->getStream();
$stream->expect(TwigToken::BLOCK_END_TYPE);
return new DefaultNode($token->getLine(), $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'default';
}
}

View File

@@ -0,0 +1,233 @@
<?php namespace Cms\Twig;
use Block;
use Cms\Classes\Controller;
use Event;
use System\Classes\Asset\Vite;
use Twig\Extension\AbstractExtension as TwigExtension;
use Twig\TwigFilter as TwigSimpleFilter;
use Twig\TwigFunction as TwigSimpleFunction;
/**
* The CMS Twig extension class implements the basic CMS Twig functions and filters.
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class Extension extends TwigExtension
{
/**
* The instanciated CMS controller
*/
protected Controller $controller;
/**
* Sets the CMS controller instance
*/
public function setController(Controller $controller)
{
$this->controller = $controller;
}
/**
* Gets the CMS controller instance
*/
public function getController(): Controller
{
return $this->controller;
}
/**
* Returns an array of functions to add to the existing list.
*/
public function getFunctions(): array
{
$options = [
'is_safe' => ['html'],
];
return [
new TwigSimpleFunction('page', [$this, 'pageFunction'], $options),
new TwigSimpleFunction('partial', [$this, 'partialFunction'], $options),
new TwigSimpleFunction('content', [$this, 'contentFunction'], $options),
new TwigSimpleFunction('component', [$this, 'componentFunction'], $options),
new TwigSimpleFunction('placeholder', [$this, 'placeholderFunction'], ['is_safe' => ['html']]),
new TwigSimpleFunction('viteReactRefresh', [$this, 'viteReactRefreshFunction'], $options),
new TwigSimpleFunction('vite', [$this, 'viteFunction'], $options),
];
}
/**
* Returns an array of filters this extension provides.
*/
public function getFilters(): array
{
$options = [
'is_safe' => ['html'],
];
return [
new TwigSimpleFilter('page', [$this, 'pageFilter'], $options),
new TwigSimpleFilter('theme', [$this, 'themeFilter'], $options),
];
}
/**
* Returns an array of token parsers this extension provides.
*/
public function getTokenParsers(): array
{
return [
new PageTokenParser,
new PartialTokenParser,
new ContentTokenParser,
new PutTokenParser,
new PlaceholderTokenParser,
new DefaultTokenParser,
new FrameworkTokenParser,
new SnowboardTokenParser,
new ComponentTokenParser,
new FlashTokenParser,
new ScriptsTokenParser,
new StylesTokenParser,
];
}
/**
* Renders a page; used in the layout code to output the requested page.
*/
public function pageFunction(): string
{
return $this->controller->renderPage();
}
/**
* Renders the requested partial with the provided parameters. Optionally throw an exception if the partial cannot be found
*/
public function partialFunction(string $name, array $parameters = [], bool $throwException = false): string|bool
{
return $this->controller->renderPartial($name, $parameters, $throwException);
}
/**
* Renders the requested content file.
*/
public function contentFunction(string $name, array $parameters = [], bool $throwException = false): string|bool
{
return $this->controller->renderContent($name, $parameters, $throwException);
}
/**
* Renders a component's default partial.
*/
public function componentFunction(string $name, array $parameters = []): string
{
return $this->controller->renderComponent($name, $parameters);
}
/**
* Renders registered assets of a given type or all types if $type not provided
*/
public function assetsFunction(?string $type = null): ?string
{
return $this->controller->makeAssets($type);
}
/**
* Renders placeholder content, without removing the block, must be called before the placeholder tag itself
*/
public function placeholderFunction(string $name, ?string $default = null): ?string
{
if (($result = Block::get($name)) === null) {
return null;
}
$result = str_replace('<!-- X_WINTER_DEFAULT_BLOCK_CONTENT -->', trim($default), $result);
return $result;
}
/**
* Returns the relative URL for the provided page
*
* @param mixed $name Specifies the Cms Page file name.
* @param array|bool $parameters Route parameters to consider in the URL. If boolean will be used as the value for $routePersistence
* @param bool $routePersistence Set to false to exclude the existing routing parameters from the generated URL
*/
public function pageFilter($name, $parameters = [], $routePersistence = true): ?string
{
return $this->controller->pageUrl($name, $parameters, $routePersistence);
}
/**
* Converts supplied URL to a theme URL relative to the website root. If the URL provided is an
* array then the files will be combined.
*
* @param mixed $url Specifies the input to be turned into a URL (arrays will be passed to the AssetCombiner)
*/
public function themeFilter($url): string
{
return $this->controller->themeUrl($url);
}
/**
* Generates Vite tags via Laravel's Vite Object.
*/
public function viteFunction(array $entrypoints, string $package, ?string $buildDirectory = null): \Illuminate\Support\HtmlString
{
return Vite::tags($entrypoints, $package, $buildDirectory);
}
/**
* Generates Vite React Refresh tags via Laravel's Vite Object.
*/
public function viteReactRefreshFunction(string $package, ?string $buildDirectory = null): ?\Illuminate\Support\HtmlString
{
return Vite::reactRefreshTag($package, $buildDirectory);
}
/**
* Opens a layout block.
*/
public function startBlock(string $name): void
{
Block::startBlock($name);
}
/**
* Returns a layout block contents (or null if it doesn't exist) and removes the block.
*/
public function displayBlock(string $name, ?string $default = null): ?string
{
if (($result = Block::placeholder($name)) === null) {
return $default;
}
/**
* @event cms.block.render
* Provides an opportunity to modify the rendered block content
*
* Example usage:
*
* Event::listen('cms.block.render', function ((string) $name, (string) $result) {
* if ($name === 'myBlockName') {
* return 'my custom content';
* }
* });
*
*/
if ($event = Event::fire('cms.block.render', [$name, $result], true)) {
$result = $event;
}
$result = str_replace('<!-- X_WINTER_DEFAULT_BLOCK_CONTENT -->', trim($default), $result);
return $result;
}
/**
* Closes a layout block.
*/
public function endBlock($append = true): void
{
Block::endBlock($append);
}
}

View File

@@ -0,0 +1,67 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a flash node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class FlashNode extends TwigNode
{
public function __construct($name, TwigNode $body, $lineno, $tag = 'flash')
{
parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$attrib = $this->getAttribute('name');
$compiler
->write('$_type = isset($context["type"]) ? $context["type"] : null;')
->write('$_message = isset($context["message"]) ? $context["message"] : null;')
;
if ($attrib == 'all') {
$compiler
->addDebugInfo($this)
->write('foreach (Flash::all() as $type => $message) {'.PHP_EOL)
->indent()
->write('$context["type"] = $type;')
->write('$context["message"] = $message;')
->subcompile($this->getNode('body'))
->outdent()
->write('}'.PHP_EOL)
;
}
else {
$compiler
->addDebugInfo($this)
->write('$context["type"] = ')
->string($attrib)
->write(';')
->write('foreach (Flash::')
->raw($attrib)
->write('() as $message) {'.PHP_EOL)
->indent()
->write('$context["message"] = $message;')
->subcompile($this->getNode('body'))
->outdent()
->write('}'.PHP_EOL)
;
}
$compiler
->write('$context["type"] = $_type;')
->write('$context["message"] = $_message;')
;
}
}

View File

@@ -0,0 +1,55 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
/**
* Parser for the {% flash %} Twig tag.
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class FlashTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
*
* @return TwigNode A TwigNode instance
*/
public function parse(TwigToken $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
if ($token = $stream->nextIf(TwigToken::NAME_TYPE)) {
$name = $token->getValue();
}
else {
$name = 'all';
}
$stream->expect(TwigToken::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideIfEnd'], true);
$stream->expect(TwigToken::BLOCK_END_TYPE);
return new FlashNode($name, $body, $lineno, $this->getTag());
}
public function decideIfEnd(TwigToken $token)
{
return $token->test(['endflash']);
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'flash';
}
}

View File

@@ -0,0 +1,62 @@
<?php namespace Cms\Twig;
use System\Models\Parameter;
use System\Classes\CombineAssets;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
use Url;
/**
* Represents a "framework" node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class FrameworkNode extends TwigNode
{
public function __construct($name, $lineno, $tag = 'framework')
{
parent::__construct([], ['name' => $name], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$build = Parameter::get('system::core.build', 'winter');
$cacheBust = '?v=' . $build;
$attrib = $this->getAttribute('name');
$includeExtras = strtolower(trim($attrib)) === 'extras';
$compiler
->addDebugInfo($this)
->write("\$_minify = ".CombineAssets::class."::instance()->useMinify;" . PHP_EOL);
$basePath = rtrim(Url::asset(''), '/');
if ($includeExtras) {
$compiler
->write("if (\$_minify) {" . PHP_EOL)
->indent()
->write("echo '<script src=\"{$basePath}/modules/system/assets/js/framework.combined-min.js$cacheBust\"></script>'.PHP_EOL;" . PHP_EOL)
->outdent()
->write("}" . PHP_EOL)
->write("else {" . PHP_EOL)
->indent()
->write("echo '<script src=\"{$basePath}/modules/system/assets/js/framework.js$cacheBust\"></script>'.PHP_EOL;" . PHP_EOL)
->write("echo '<script src=\"{$basePath}/modules/system/assets/js/framework.extras.js$cacheBust\"></script>'.PHP_EOL;" . PHP_EOL)
->outdent()
->write("}" . PHP_EOL)
->write("echo '<link rel=\"stylesheet\" property=\"stylesheet\" href=\"{$basePath}/modules/system/assets/css/framework.extras'.(\$_minify ? '-min' : '').'.css$cacheBust\">'.PHP_EOL;" . PHP_EOL)
;
}
else {
$compiler->write("echo '<script src=\"{$basePath}/modules/system/assets/js/framework'.(\$_minify ? '-min' : '').'.js$cacheBust\"></script>'.PHP_EOL;" . PHP_EOL);
}
$compiler->write('unset($_minify);' . PHP_EOL);
}
}

View File

@@ -0,0 +1,45 @@
<?php namespace Cms\Twig;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
/**
* Parser for the `{% framework %}` Twig tag.
*
* {% framework %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class FrameworkTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return Twig\Node\Node A Twig\Node\Node instance
*/
public function parse(TwigToken $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = null;
if ($token = $stream->nextIf(TwigToken::NAME_TYPE)) {
$name = $token->getValue();
}
$stream->expect(TwigToken::BLOCK_END_TYPE);
return new FrameworkNode($name, $lineno, $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'framework';
}
}

165
modules/cms/twig/Loader.php Normal file
View File

@@ -0,0 +1,165 @@
<?php namespace Cms\Twig;
use Event;
use Twig\Source as TwigSource;
use Twig\Loader\LoaderInterface as TwigLoaderInterface;
use Cms\Contracts\CmsObject;
use System\Twig\Loader as LoaderBase;
use Cms\Classes\Partial as CmsPartial;
/**
* This class implements a Twig template loader for the CMS.
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class Loader extends LoaderBase implements TwigLoaderInterface
{
/**
* @var \Cms\Classes\CmsCompoundObject A CMS object to load the template from.
*/
protected $obj;
/**
* @var array Cache
*/
protected $fallbackCache = [];
/**
* Sets a CMS object to load the template from.
*
* @param \Cms\Contracts\CmsObject $obj Specifies the CMS object.
* @return void
*/
public function setObject(CmsObject $obj)
{
$this->obj = $obj;
}
/**
* Returns the Twig content string.
* This step is cached internally by Twig.
*/
public function getSourceContext(string $name): TwigSource
{
if (!$this->validateCmsObject($name)) {
return parent::getSourceContext($name);
}
$content = $this->obj->getTwigContent();
/**
* @event cms.template.processTwigContent
* Provides an opportunity to modify Twig content before being processed by Twig. `$dataHolder` = {content: $twigContent}
*
* Example usage:
*
* Event::listen('cms.template.processTwigContent', function ((\Cms\Classes\CmsObject) $thisObject, (object) $dataHolder) {
* $dataHolder->content = "NO CONTENT FOR YOU!";
* });
*
*/
$dataHolder = (object) ['content' => $content];
Event::fire('cms.template.processTwigContent', [$this->obj, $dataHolder]);
return new TwigSource((string) $dataHolder->content, $name);
}
/**
* Returns the Twig cache key.
*/
public function getCacheKey(string $name): string
{
if (!$this->validateCmsObject($name)) {
return parent::getCacheKey($name);
}
return $this->obj->getTwigCacheKey();
}
/**
* Determines if the content is fresh.
*
* @param string $name The template name
* @param mixed $time The time to check against the template
* @return bool
*/
public function isFresh(string $name, int $time): bool
{
if (!$this->validateCmsObject($name)) {
return parent::isFresh($name, $time);
}
return $this->obj->mtime <= $time;
}
/**
* Returns the file name of the loaded template.
*/
public function getFilename(string $name): string
{
if (!$this->validateCmsObject($name)) {
return parent::getFilename($name);
}
return $this->obj->getFilePath();
}
/**
* Checks that the template exists.
*/
public function exists(string $name): bool
{
if (!$this->validateCmsObject($name)) {
return parent::exists($name);
}
return $this->obj->exists;
}
/**
* Internal method that checks if the template name matches
* the loaded object, with fallback support to partials.
*/
protected function validateCmsObject(string $name): bool
{
if ($this->obj && $name === $this->obj->getFilePath()) {
return true;
}
if ($fallbackObj = $this->findFallbackObject($name)) {
$this->obj = $fallbackObj;
return true;
}
return false;
}
/**
* Looks up a fallback CMS partial object.
*
* @param string $name The filename to attempt to load a fallback CMS partial for
* @return Cms\Classes\Partial|bool Returns false if a CMS partial can't be found
*/
protected function findFallbackObject($name)
{
// Ignore Laravel views
if (strpos($name, '::') !== false) {
return false;
}
// Check the cache
if (array_key_exists($name, $this->fallbackCache)) {
return $this->fallbackCache[$name];
}
// Attempt to load the path as a CMS Partial object
try {
$partial = CmsPartial::find($name);
} catch (\Exception $e) {
return false;
}
return $this->fallbackCache[$name] = $partial;
}
}

View File

@@ -0,0 +1,31 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a page node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class PageNode extends TwigNode
{
public function __construct($lineno, $tag = 'page')
{
parent::__construct([], [], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$compiler
->addDebugInfo($this)
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->pageFunction();\n")
;
}
}

View File

@@ -0,0 +1,38 @@
<?php namespace Cms\Twig;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
/**
* Parser for the `{% page %}` Twig tag.
*
* {% page %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class PageTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return Twig\Node\Node A Twig\Node\Node instance
*/
public function parse(TwigToken $token)
{
$stream = $this->parser->getStream();
$stream->expect(TwigToken::BLOCK_END_TYPE);
return new PageNode($token->getLine(), $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'page';
}
}

View File

@@ -0,0 +1,46 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a partial node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class PartialNode extends TwigNode
{
public function __construct(TwigNode $nodes, $paramNames, $lineno, $tag = 'partial')
{
parent::__construct(['nodes' => $nodes], ['names' => $paramNames], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$compiler->addDebugInfo($this);
$compiler->write("\$context['__cms_partial_params'] = [];\n");
for ($i = 1; $i < count($this->getNode('nodes')); $i++) {
$compiler->write("\$context['__cms_partial_params']['".$this->getAttribute('names')[$i-1]."'] = ");
$compiler->subcompile($this->getNode('nodes')->getNode($i));
$compiler->write(";\n");
}
$compiler
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->partialFunction(")
->subcompile($this->getNode('nodes')->getNode(0))
->write(", \$context['__cms_partial_params']")
->write(", true")
->write(");\n")
;
$compiler->write("unset(\$context['__cms_partial_params']);\n");
}
}

View File

@@ -0,0 +1,74 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
use Twig\Error\SyntaxError as TwigErrorSyntax;
/**
* Parser for the `{% partial %}` Twig tag.
*
* {% partial "sidebar" %}
*
* {% partial "sidebar" name='John' %}
*
* {% partial "sidebar" name='John', year=2013 %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class PartialTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return TwigNode A TwigNode instance
*/
public function parse(TwigToken $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $this->parser->getExpressionParser()->parseExpression();
$paramNames = [];
$nodes = [$name];
$end = false;
while (!$end) {
$current = $stream->next();
switch ($current->getType()) {
case TwigToken::NAME_TYPE:
$paramNames[] = $current->getValue();
$stream->expect(TwigToken::OPERATOR_TYPE, '=');
$nodes[] = $this->parser->getExpressionParser()->parseExpression();
break;
case TwigToken::BLOCK_END_TYPE:
$end = true;
break;
default:
throw new TwigErrorSyntax(
sprintf('Invalid syntax in the partial tag. Line %s', $lineno),
$stream->getCurrent()->getLine(),
$stream->getSourceContext()
);
break;
}
}
return new PartialNode(new TwigNode($nodes), $paramNames, $token->getLine(), $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'partial';
}
}

View File

@@ -0,0 +1,83 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a placeholder node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class PlaceholderNode extends TwigNode
{
public function __construct($name, $paramValues, $body, $lineno, $tag = 'placeholder')
{
$nodes = [];
if ($body) {
$nodes['default'] = $body;
}
$attributes = $paramValues;
$attributes['name'] = $name;
parent::__construct($nodes, $attributes, $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$hasBody = $this->hasNode('default');
$varId = '__placeholder_'.$this->getAttribute('name').'_default_contents';
$compiler
->addDebugInfo($this)
->write("\$context[")
->raw("'".$varId."'")
->raw("] = null;");
if ($hasBody) {
$compiler
->addDebugInfo($this)
->write('ob_start();')
->subcompile($this->getNode('default'))
->write("\$context[")
->raw("'".$varId."'")
->raw("] = ob_get_clean();");
}
$isText = $this->hasAttribute('type') && $this->getAttribute('type') == 'text';
$compiler->addDebugInfo($this);
if (!$isText) {
$compiler->write("echo \$this->env->getExtension('Cms\Twig\Extension')->displayBlock(");
}
else {
$compiler->write("echo twig_escape_filter(\$this->env, \$this->env->getExtension('Cms\Twig\Extension')->displayBlock(");
}
$compiler
->raw("'".$this->getAttribute('name')."', ")
->raw("\$context[")
->raw("'".$varId."'")
->raw("]")
->raw(")");
if (!$isText) {
$compiler->raw(";\n");
}
else {
$compiler->raw(");\n");
}
$compiler
->addDebugInfo($this)
->write("unset(\$context[")
->raw("'".$varId."'")
->raw("]);");
}
}

View File

@@ -0,0 +1,98 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
use Twig\Error\SyntaxError as TwigErrorSyntax;
/**
* Parser for the `{% placeholder %}` Twig tag.
*
* {% placeholder head %}
*
* or - use default placeholder content
*
* {% placeholder head %}
* <link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet"/>
* {% endshowblock %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class PlaceholderTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return TwigNode A TwigNode instance
*/
public function parse(TwigToken $token)
{
$stream = $this->parser->getStream();
$name = $stream->expect(TwigToken::NAME_TYPE)->getValue();
$body = null;
$params = [];
if ($stream->test(TwigToken::NAME_TYPE, 'default')) {
$stream->next();
$params = $this->loadParams($stream);
$body = $this->parser->subparse([$this, 'decidePlaceholderEnd'], true);
$stream->expect(TwigToken::BLOCK_END_TYPE);
}
else {
$params = $this->loadParams($stream);
}
return new PlaceholderNode($name, $params, $body, $token->getLine(), $this->getTag());
}
public function decidePlaceholderEnd(TwigToken $token)
{
return $token->test('endplaceholder');
}
protected function loadParams($stream)
{
$params = [];
$end = false;
while (!$end) {
$current = $stream->next();
switch ($current->getType()) {
case TwigToken::NAME_TYPE:
$paramName = $current->getValue();
$stream->expect(TwigToken::OPERATOR_TYPE, '=');
$current = $stream->next();
$params[$paramName] = $current->getValue();
break;
case TwigToken::BLOCK_END_TYPE:
$end = true;
break;
default:
throw new TwigErrorSyntax(
sprintf('Invalid syntax in the placeholder tag. Line %s', $stream->getCurrent()->getLine()),
$stream->getCurrent()->getLine(),
$stream->getSourceContext()
);
break;
}
}
return $params;
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'placeholder';
}
}

View File

@@ -0,0 +1,44 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a put node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class PutNode extends TwigNode
{
public function __construct(TwigNode $body, $name, $endType, $lineno, $tag = 'put')
{
parent::__construct(['body' => $body], ['name' => $name, 'endType' => $endType], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$compiler
->addDebugInfo($this)
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->startBlock(")
->raw("'".$this->getAttribute('name')."'")
->write(");\n")
;
$isOverwrite = strtolower($this->getAttribute('endType')) == 'overwrite';
$compiler->subcompile($this->getNode('body'));
$compiler
->addDebugInfo($this)
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->endBlock(")
->raw($isOverwrite ? 'false' : 'true')
->write(");\n")
;
}
}

View File

@@ -0,0 +1,63 @@
<?php namespace Cms\Twig;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
/**
* Parser for the `{% put %}` Twig tag.
*
* {% put head %}
* <link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet"/>
* {% endput %}
*
* or
*
* {% put head %}
* <link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet"/>
* {% default %}
* {% endput %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class PutTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return Twig\Node\Node A Twig\Node\Node instance
*/
public function parse(TwigToken $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $stream->expect(TwigToken::NAME_TYPE)->getValue();
$stream->expect(TwigToken::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decidePutEnd'], true);
$endType = null;
if ($token = $stream->nextIf(TwigToken::NAME_TYPE)) {
$endType = $token->getValue();
}
$stream->expect(TwigToken::BLOCK_END_TYPE);
return new PutNode($body, $name, $endType, $lineno, $this->getTag());
}
public function decidePutEnd(TwigToken $token)
{
return $token->test('endput');
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'put';
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a "scripts" node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class ScriptsNode extends TwigNode
{
public function __construct($lineno, $tag = 'scripts')
{
parent::__construct([], [], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$compiler
->addDebugInfo($this)
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->assetsFunction('js');\n")
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->displayBlock('scripts');\n")
;
}
}

View File

@@ -0,0 +1,38 @@
<?php namespace Cms\Twig;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
/**
* Parser for the `{% scripts %}` Twig tag.
*
* {% scripts %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class ScriptsTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return Twig\Node\Node A Twig\Node\Node instance
*/
public function parse(TwigToken $token)
{
$stream = $this->parser->getStream();
$stream->expect(TwigToken::BLOCK_END_TYPE);
return new ScriptsNode($token->getLine(), $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'scripts';
}
}

View File

@@ -0,0 +1,79 @@
<?php namespace Cms\Twig;
use Config;
use System\Classes\CombineAssets;
use System\Models\Parameter;
use Twig\Compiler as TwigCompiler;
use Twig\Node\Node as TwigNode;
use Url;
/**
* Represents a "snowboard" node
*
* @package winter\wn-cms-module
* @author Winter CMS
*/
class SnowboardNode extends TwigNode
{
/**
* @var bool Indicates if the base Snowboard framework is already loaded, in case of multiple uses of this tag.
*/
public static $baseLoaded = false;
public function __construct(array $modules, $lineno, $tag = 'snowboard')
{
parent::__construct([], ['modules' => $modules], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$build = Parameter::get('system::core.build', 'winter');
$cacheBust = '?v=' . $build;
$modules = $this->getAttribute('modules');
$compiler
->addDebugInfo($this)
->write("\$_minify = ".CombineAssets::class."::instance()->useMinify;" . PHP_EOL);
$moduleMap = [
'base' => (Config::get('develop.debugSnowboard', false) === true)
? 'snowboard.base.debug'
: 'snowboard.base',
'vendor' => 'snowboard.vendor',
'request' => 'snowboard.request',
'attr' => 'snowboard.data-attr',
'extras' => 'snowboard.extras',
];
$manifestPath = Url::asset('/modules/system/assets/js/build/manifest.js');
$basePath = Url::asset('/modules/system/assets/js/snowboard/build') . '/';
if (!static::$baseLoaded) {
// Add manifest and vendor files
$compiler
->write("echo '<script data-module=\"snowboard-manifest\" src=\"{$manifestPath}{$cacheBust}\"></script>'.PHP_EOL;" . PHP_EOL);
$vendorJs = $moduleMap['vendor'];
$compiler
->write("echo '<script data-module=\"snowboard-vendor\" src=\"{$basePath}{$vendorJs}.js{$cacheBust}\"></script>'.PHP_EOL;" . PHP_EOL);
// Add base script
$baseJs = $moduleMap['base'];
$baseUrl = Url::to('/');
$assetUrl = Url::asset('/');
$compiler
->write("echo '<script data-module=\"snowboard-base\" data-base-url=\"{$baseUrl}\" data-asset-url=\"{$assetUrl}\" src=\"{$basePath}{$baseJs}.js{$cacheBust}\"></script>'.PHP_EOL;" . PHP_EOL);
static::$baseLoaded = true;
}
foreach ($modules as $module) {
$moduleJs = $moduleMap[$module];
$compiler
->write("echo '<script data-module=\"{$module}\" src=\"{$basePath}{$moduleJs}.js{$cacheBust}\"></script>'.PHP_EOL;" . PHP_EOL);
}
}
}

View File

@@ -0,0 +1,60 @@
<?php namespace Cms\Twig;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
/**
* Parser for the `{% snowboard %}` Twig tag.
*
* @package winter\wn-cms-module
* @author Winter CMS
*/
class SnowboardTokenParser extends TwigTokenParser
{
/**
* @inheritDoc
*/
public function parse(TwigToken $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$modules = [];
do {
$token = $stream->next();
if ($token->getType() === TwigToken::NAME_TYPE) {
$modules[] = $token->getValue();
}
} while ($token->getType() !== TwigToken::BLOCK_END_TYPE);
// Filter out invalid types
$modules = array_filter(
array_map(function ($item) {
return strtolower($item);
}, $modules),
function ($item) {
return in_array($item, ['request', 'attr', 'extras', 'all']);
}
);
if (in_array('all', $modules)) {
$modules = [
'request',
'attr',
'extras',
];
}
return new SnowboardNode($modules, $lineno, $this->getTag());
}
/**
* @inheritDoc
*/
public function getTag()
{
return 'snowboard';
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace Cms\Twig;
use Twig\Node\Node as TwigNode;
use Twig\Compiler as TwigCompiler;
/**
* Represents a "styles" node
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class StylesNode extends TwigNode
{
public function __construct($lineno, $tag = 'styles')
{
parent::__construct([], [], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param TwigCompiler $compiler A TwigCompiler instance
*/
public function compile(TwigCompiler $compiler)
{
$compiler
->addDebugInfo($this)
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->assetsFunction('css');\n")
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->displayBlock('styles');\n")
;
}
}

View File

@@ -0,0 +1,38 @@
<?php namespace Cms\Twig;
use Twig\Token as TwigToken;
use Twig\TokenParser\AbstractTokenParser as TwigTokenParser;
/**
* Parser for the `{% styles %}` Twig tag.
*
* {% styles %}
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class StylesTokenParser extends TwigTokenParser
{
/**
* Parses a token and returns a node.
*
* @param TwigToken $token A TwigToken instance
* @return Twig\Node\Node A Twig\Node\Node instance
*/
public function parse(TwigToken $token)
{
$stream = $this->parser->getStream();
$stream->expect(TwigToken::BLOCK_END_TYPE);
return new StylesNode($token->getLine(), $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'styles';
}
}