diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e755a70c8e4..0da3ce0104f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -25,7 +25,7 @@ body: label: Environment description: | Details about your environment. Versions of Statamic, PHP, Laravel, any addons that are installed, etc. - (Go ahead and just paste the output of the `php please support:details` command.) + (Go ahead and just paste the output of the `php please support:details` command.) render: yaml # the format of the command is close to yaml and gets highlighted nicely validations: required: true @@ -40,15 +40,6 @@ body: - Other (please explain) validations: required: true - - type: dropdown - attributes: - label: Antlers Parser - description: If using 3.3+, which Antlers Parser are you using? - options: - - Runtime (default) - - Regex (legacy) - validations: - required: false - type: textarea attributes: label: Additional details diff --git a/config/antlers.php b/config/antlers.php index a0c5087bfba..1b3b65b5266 100644 --- a/config/antlers.php +++ b/config/antlers.php @@ -2,18 +2,6 @@ return [ - /* - |-------------------------------------------------------------------------- - | Version - |-------------------------------------------------------------------------- - | - | The desired Antlers language version to utilize. Supported values are - | "runtime" for the modern parser, or "regex" for the legacy parser. - | - */ - - 'version' => 'runtime', - /* |-------------------------------------------------------------------------- | Guarded Variables diff --git a/src/Fields/Value.php b/src/Fields/Value.php index 2674930a9da..0ddd7b024bf 100644 --- a/src/Fields/Value.php +++ b/src/Fields/Value.php @@ -92,9 +92,7 @@ public function antlersValue(Parser $parser, $variables) } if ($shouldParseAntlers) { - if (config('statamic.antlers.version') === 'runtime') { - $value = (new DocumentTransformer())->correct($value); - } + $value = (new DocumentTransformer())->correct($value); return $parser->parse($value, $variables); } diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index 1bdf8186465..e3e35e1891b 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -800,13 +800,6 @@ public function gravatar($value, $params) */ public function groupBy($value, $params) { - if (config('statamic.antlers.version') != 'runtime') { - // Workaround for https://github.com/statamic/cms/issues/3614 - // At the moment this modifier only works properly when using the param syntax. - $params = implode(':', $params); - $params = explode('|', $params); - } - $groupBy = $params[0]; $groupLabels = []; diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index b01fb5261ad..f7fb9b3ab28 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -187,7 +187,6 @@ protected function addAboutCommandInfo() AboutCommand::add('Statamic', [ 'Version' => fn () => Statamic::version().' '.(Statamic::pro() ? 'PRO' : 'Solo'), - 'Antlers' => config('statamic.antlers.version'), 'Addons' => $addons->count(), 'Stache Watcher' => config('statamic.stache.watcher') ? 'Enabled' : 'Disabled', 'Static Caching' => config('statamic.static_caching.strategy') ?: 'Disabled', diff --git a/src/Providers/ViewServiceProvider.php b/src/Providers/ViewServiceProvider.php index 57252f8dbb5..e9e7b040adb 100644 --- a/src/Providers/ViewServiceProvider.php +++ b/src/Providers/ViewServiceProvider.php @@ -19,7 +19,6 @@ use Statamic\View\Antlers\Language\Runtime\RuntimeParser; use Statamic\View\Antlers\Language\Runtime\Tracing\TraceManager; use Statamic\View\Antlers\Language\Utilities\StringUtilities; -use Statamic\View\Antlers\Parser; use Statamic\View\Blade\AntlersBladePrecompiler; use Statamic\View\Cascade; use Statamic\View\Debugbar\AntlersProfiler\PerformanceCollector; @@ -41,32 +40,14 @@ public function register() return new Cascade($app['request'], Site::current()); }); - $this->registerRuntimeAntlers(); - $this->registerRegexAntlers(); - - $this->app->bind(ParserContract::class, function ($app) { - return config('statamic.antlers.version', 'regex') === 'regex' - ? $app->make('antlers.regex') - : $app->make('antlers.runtime'); - }); + $this->registerAntlers(); $this->app->singleton(Engine::class, function ($app) { return new Engine($app['files'], $app[ParserContract::class]); }); } - private function registerRegexAntlers() - { - $this->app->bind('antlers.regex', function ($app) { - return (new Parser) - ->callback([Engine::class, 'renderTag']) - ->cascade($app[Cascade::class]); - }); - - $this->app->bind(Parser::class, fn ($app) => $app['antlers.regex']); - } - - private function registerRuntimeAntlers() + private function registerAntlers() { GlobalRuntimeState::$environmentId = StringUtilities::uuidv4(); @@ -100,7 +81,7 @@ private function registerRuntimeAntlers() return new PerformanceTracer(); }); - $this->app->bind('antlers.runtime', function ($app) { + $this->app->bind(ParserContract::class, function ($app) { /** @var RuntimeParser $parser */ $parser = $app->make(RuntimeParser::class)->cascade($app[Cascade::class]); $runtimeConfig = new RuntimeConfiguration(); diff --git a/src/View/Antlers/Antlers.php b/src/View/Antlers/Antlers.php index 5b7e62aa094..b0fc4f9c43a 100644 --- a/src/View/Antlers/Antlers.php +++ b/src/View/Antlers/Antlers.php @@ -47,10 +47,6 @@ public function parseLoop($content, $data, $supplement = true, $context = []) public function identifiers(string $content): array { - if (config('statamic.antlers.version') !== 'runtime') { - throw new \Exception('Antlers identifiers can only be retrieved when using the runtime parser.'); - } - return (new IdentifierFinder)->getIdentifiers($content); } } diff --git a/src/View/Antlers/Parser.php b/src/View/Antlers/Parser.php deleted file mode 100644 index 28161e257f4..00000000000 --- a/src/View/Antlers/Parser.php +++ /dev/null @@ -1,1626 +0,0 @@ - [], - ]; - - /** - * Initialize the army of regexes. - */ - public function __construct() - { - // Matches a variable inside curly braces. Spaces not allowed. - $this->variableRegex = "(?!if\s|unless\s)[a-zA-Z0-9_'\"][^<>{}=\s]*"; - - // Matches a full variable expression inside curly braces. - $this->looseVariableRegex = "(?!if\s|unless\s)[a-zA-Z0-9_'\"][^<>{}=]*"; - - // Matches the first part of a {{ tag: followed a variable name and full expression. - $this->callbackNameRegex = '(?!if\s|unless\s)[a-zA-Z0-9_][^<>{}=!?]*'.':'.$this->variableRegex; - - // Matches a tag pair and captures everything inside it. - $this->variableLoopRegex = '/{{\s*('.$this->looseVariableRegex.')\s*}}(.*?){{\s*\/\1\s*}}/ms'; - - // Matches a variable expression inside curly braces. - $this->variableTagRegex = '/{{\s*('.$this->looseVariableRegex.')\s*}}/m'; - - // Matches the callback handle for a matching tag pair, captures the contents, and ignores the parameters. - // We assume it's a callback because the of the matching tag pair, no pipe modifiers, and lack of "?" in the expression. - $this->callbackBlockRegex = '/{{\s*('.$this->variableRegex.')(?!\s*\|)(?:\s([^?]*?))}}(.*?){{\s*\/\1\s*}}/ms'; - - // Matches a recursive children loop. - $this->recursiveRegex = '/{{\s*\*recursive\s*('.$this->variableRegex.')\*\s*}}/ms'; - - // Matches the noparse tag and captures everything inside. - $this->noparseRegex = '/{{\s*noparse\s*}}(.*?){{\s*\/noparse\s*}}/ms'; - - // Matches an ignored tag as indicated by a prefixed @ symbol: @{{ }} - $this->ignoreRegex = '/@{{(?:(?!}}).)*}}/s'; - - // Matches the logic operator tags - $this->conditionalRegex = '/{{\s*(if|unless|elseif|elseunless)\s+((?:\()?(.*?)(?:\))?)\s*}}/ms'; - $this->conditionalElseRegex = '/{{\s*else\s*}}/ms'; - $this->conditionalEndRegex = '/{{\s*(?:endif|\/if|\/unless)\s*}}/ms'; - $this->conditionalExistsRegex = '/(\s+|^)exists\s+('.$this->variableRegex.')(\s+|$)/ms'; - $this->conditionalNotRegex = '/(\s+|^)not(\s+|$)/ms'; - } - - public function allowPhp($allow = true) - { - $this->allowPhp = $allow; - - return $this; - } - - public function callback($callback) - { - $this->callback = $callback; - - return $this; - } - - public function cascade($cascade) - { - $this->cascade = $cascade; - - return $this; - } - - public function parseView($view, $text, $data = []) - { - $existingView = $this->view; - - $this->view = $view; - - $data = array_merge($data, ['view' => $this->cascade->getViewData($view)]); - - try { - $parsed = $this->parse($text, $data); - } catch (\Exception|\Error $e) { - throw $this->viewException($e, $data); - } - - $this->view = $existingView; - - return $parsed; - } - - /** - * Kick off the Antlers parse process. - * - * @param string $text Text to parse - * @param array|object $data Array or object to use - * @return AntlersString - */ - public function parse($text, $data = []) - { - $data = $this->normalizeData($data); - - if (! empty($data) && ! Arr::assoc($data)) { - throw new \InvalidArgumentException('Expecting an associative array'); - } - - // Save the original text coming in so that we can parse it recursively - // later on without this needing to be within a callback - $this->original_text = $text; - - $text = $this->sanitizePhp($text); - - // We want to extract the noparse blocks before comments, - // allowing us to show them for documentation purposes - - $text = $this->extractNoparse($text); - $text = $this->stripComments($text); - - $text = $this->extractTagPairs($text, $data); - - // Parse conditionals first to avoid parsing and execution. - $text = $this->parseConditionPairs($text, $data); - $text = $this->parseTernaries($text, $data); - $text = $this->injectExtractions($text, 'tag_pair'); - $text = $this->parseVariables($text, $data); - $text = $this->injectExtractions($text, 'callback_blocks'); - $text = $this->parseCallbackTags($text, $data); - - // Parse parameters inside tag pairs - if (strpos($text, '{{') !== false) { - $text = $this->parseCallbackTags($text, $data, null); - } - - return new AntlersString($text, $this); - } - - protected function normalizeData($data) - { - if (is_array($data)) { - return $data; - } - - if ($data instanceof Arrayable) { - return $data->toArray(); - } - - throw new \InvalidArgumentException(sprintf( - 'Expecting array or object implementing Arrayable. Encountered [%s]', - ($type = gettype($data)) === 'object' ? get_class($data) : $type - )); - } - - /** - * Strip all comments out of the markup. - * - * @param string $text - * @return string - */ - public function stripComments($text) - { - return $this->preg_replace('/{{#.*?#}}/s', '', $text); - } - - /** - * Recursively parses all of the variables in the given HTML markup. - * - * @param string $html The HTML markup - * @param array|object $data the data - * @return string - */ - public function parseVariables($html, $data) - { - $html = $this->parseLoopVariables($html, $data); - $html = $this->parseStringVariables($html, $data); - $html = $this->parseVariablesWithParameterStyleModifiers($html, $data); - - return $html; - } - - /** - * Look for and parse array variables. - * - * @param string $html The HTML markup - * @param array|object $data the data - * @return string - */ - public function parseLoopVariables($text, $data) - { - /** - * $data_matches[][0][0] is the raw data loop tag - * $data_matches[][0][1] is the offset of raw data loop tag - * $data_matches[][1][0] is the data variable - * $data_matches[][1][1] is the offset of data variable - * $data_matches[][2][0] is the content to be looped over - * $data_matches[][2][1] is the offset of content to be looped over. - */ - if (! $this->preg_match_all($this->variableLoopRegex, $text, $data_matches, PREG_SET_ORDER + PREG_OFFSET_CAPTURE)) { - return $text; - } - - foreach ($data_matches as $match) { - $contents = $match[2][0]; - - $value = $this->getVariable($var = trim($match[1][0]), $data); - - if (! $value || $value instanceof Builder) { - // Must be a callback block. Extract it so it doesn't - // conflict with local scope variables in the next step. - // Also, treat it like a callback if it's a query builder so it can be sent through the query tag. - $text = $this->createExtraction('callback_blocks', $match[0][0], $match[0][0], $text); - - continue; - } - - if ($value instanceof Collection) { - $value = $value->values(); - } - - if ($value instanceof Augmentable || $value instanceof Collection) { - $value = $value->toAugmentedArray(); - } - - if ($value instanceof Arrayable) { - $value = $value->toArray(); - } - - // If it's not an array, the user is trying to loop over something unloopable. - if (! is_array($value)) { - $value = []; - Log::debug("Cannot loop over non-loopable variable: {{ $var }}"); - } - - // Associative arrays (key value pairs like ['foo' => 'bar', 'baz' => 'qux']) shouldn't be looped over, - // instead, they should be parsed one time, as a whole. We'll turn it into a multidimensional array - // so it can be parsed over like the other cases, and then we'll pick out the first one after. - $value = ($associative = Arr::assoc($value)) ? [$value] : $this->addLoopIterationVariables($value); - - $parses = collect($value)->map(function ($iteration) use ($contents, $data) { - return $this->parseLoopInstance($contents, array_merge($data, $iteration)); - }); - - // Again, associative arrays just need the single iteration, so we'll grab - // the first. For the others, we'll concatenate them all into one string. - $loopedText = $associative ? $parses->first() : $parses->implode(''); - - // Replace the contents of the tag pair in the original text with the parsed versions of all the loops. - $text = $this->preg_replace('/'.preg_quote($match[0][0], '/').'/m', addcslashes($loopedText, '\\$'), $text, 1); - } - - return $text; - } - - protected function addLoopIterationVariables($loop) - { - $index = 0; - $total = count($loop); - - foreach ($loop as $key => &$value) { - if ($value instanceof Augmentable) { - $value = $value->toAugmentedArray(); - } - - if ($value instanceof Values) { - $value = $value->toArray(); - } - - // If the value of the current iteration is *not* already an array (ie. we're - // dealing with a super basic list like [one, two, three] then convert it - // to one, where the value is stored in a key named "value". - if (! is_array($value)) { - $value = ['value' => $value, 'name' => $value]; - } - - $value = array_merge($value, [ - 'count' => $index + 1, - 'index' => $index, - 'total_results' => $total, - 'first' => ($index === 0), - 'last' => ($index === $total - 1), - ]); - - $index++; - } - - return $loop; - } - - protected function parseLoopInstance($str, $data) - { - $str = $this->extractTagPairs($str, $data); - $str = $this->parseConditionPairs($str, $data); - $str = $this->parseTernaries($str, $data); - $str = $this->injectExtractions($str, 'tag_pair'); - $str = $this->parseVariables($str, $data); - $str = $this->injectExtractions($str, 'callback_blocks'); - $str = $this->parseCallbackTags($str, $data); - - return $str; - } - - /** - * Look for and parse string variables. - * - * @param string $html The HTML markup - * @param array|object $data the data - * @return string - */ - public function parseStringVariables($text, $data) - { - /** - * $matches[0] are the raw data tags (eg. [{{ string }}, {{ foo }}]) - * $matches[1] are the data variables (eg. [string, foo]). - */ - if (! $this->preg_match_all($this->variableTagRegex, $text, $matches)) { - return $text; - } - - foreach ($matches[1] as $i => $var) { - $tagText = $this->parseStringVariableTag(trim($var), $matches[0][$i], $data); - - // Prevent replacement if the variable value returned null. If we were to return an - // empty string, the tag would would have been replaced by nothingness. This allows - // it to stick around and be picked up in a moment when parsing callback tags. - if ($tagText === '__lex_no_value__') { - continue; - } - - $text = str_replace($matches[0][$i], $tagText, $text); - } - - return $text; - } - - /** - * Parse a single string variable tag. - * - * @param string $var The name of the variable. eg. foo - * @param string $text The tag text. - * eg. - * {{ var }} - * {{ var | modifier }} - * {{ var or anothervar }} - * {{ var | modifier or anothervar }} - * @param array|object $data The data - * @return string|void - */ - protected function parseStringVariableTag($var, $text, $data) - { - // Normalize loose modifiers into tight modifiers. - if (strpos($var, ' | ') !== false) { - $var = str_replace(' | ', '|', $var); - } - - // Null coalescence through "or", "??" or "?:" - if (Str::contains($var, [' or ', ' ?? ', ' ?: '])) { - $isCoalesce = true; - $vars = preg_split('/(\s+or\s+|\s+\?\?\s+|\s+\?\:\s+)/ms', $var, -1, PREG_SPLIT_NO_EMPTY); - } else { - $isCoalesce = false; - $vars = [$var]; - } - - foreach ($vars as $index => $var) { - if ($this->isLiteralString($var)) { - return $this->literalStringWithoutQuotes($var); - } - - $val = $this->getVariable($var, $data, '__lex_no_value__'); - - // Non-coalescing tags could potentially be callback tags (eg. {{ foo }} or - // {{ foo:bar }}) If they have no value, we'll return a special string - // that prevents it from being replaced so they can be re-parsed as - // callback tags. Otherwise, just continue down the chain of vars. - $isLastVar = $index === count($vars) - 1; - if ($isCoalesce && ! $isLastVar && $this->isNullWhenUsedInStrings($val)) { - continue; - } - - if (strpos($var, '|') !== -1) { - $modifiers = array_slice(explode('|', $var), 1); - if (in_array('noparse', $modifiers)) { - return $this->createExtraction('noparse', $text, $val, $text); - } - } - - return $this->getValueAsRenderableString($val, $var); - } - } - - /** - * Look for and parse variables with parameter style modifiers. - * Example: {{ date format="Y-m-d" }}. - * - * @param string $html The HTML markup - * @param array|object $data the data - * @return string - */ - public function parseVariablesWithParameterStyleModifiers($text, $data) - { - $regex = '/{{\s*('.$this->looseVariableRegex.')(\s+.*?)?\s*}}/ms'; - - if ($this->preg_match_all($regex, $text, $data_matches, PREG_SET_ORDER + PREG_OFFSET_CAPTURE)) { - foreach ($data_matches as $match) { - // grab some starting values & init variables - $parameters = []; - $tag = $match[0][0]; - $name = $match[1][0]; - - // is this not the content tag, and is the value known? - [$exists] = $this->getVariableExistenceAndValue($name, $data); - - if ($exists) { - // the value is known. Are there parameters? - if (isset($match[2])) { - // there are, make a backup of our $data - $cb_data = $data; - - // is $data an array? - if (is_array($data)) { - // it is, have we had callback data before? - if (! empty($this->callbackData)) { - // we have, merge it all together - $cb_data = $data + $this->callbackData; - } - - // grab the raw string of parameters - $raw_params = $this->injectExtractions($match[2][0], '__cond_str'); - - // parse them into an array - $parameters = $this->parseParameters($raw_params, $cb_data); - } elseif (is_string($data)) { - $text = str_replace($tag, $data, $text); - } - } - - // Parameter-style modifier time - // Probably should do an extraction here... - [, $replacement] = $this->getVariableExistenceAndValue($name, $data); - - foreach ($parameters as $modifier => $parameters) { - $replacement = $this->runModifier($modifier, $replacement, explode('|', $parameters), $data); - } - - $text = str_replace($tag, $replacement, $text); - } - } - } - - return $text; - } - - /** - * Parses all Callback tags, by sending them through the defined callback. - * - * @param string $text Text to parse - * @param array $data An array of data to use - * @return string - */ - public function parseCallbackTags($text, $data) - { - $text = $this->injectExtractions($text, 'tag_pair'); - - $inCondition = $this->inCondition; - - // if there are no instances of a tag, abort - if (strpos($text, '{') === false) { - return $text; - } - - if ($inCondition) { - $regex = '/{{?\s*('.$this->variableRegex.')(\s+.*?)?\s*}}?/ms'; - } else { - $regex = '/{{\s*('.$this->variableRegex.')(\s+.*?)?\s*(\/)?}}/ms'; - } - - // Make a clean copy of the collective callback data - $cb_data = $data; - - /** - * $match[0][0] is the raw tag - * $match[0][1] is the offset of raw tag - * $match[1][0] is the callback name - * $match[1][1] is the offset of callback name - * $match[2][0] is the parameters - * $match[2][1] is the offset of parameters - * $match[3][0] is the self closure - * $match[3][1] is the offset of closure. - */ - while ($this->preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE)) { - // update the collective data if it's different - if (! empty($this->callbackData)) { - $cb_data = $data + $this->callbackData; - } - - $content = ''; - $parameters = []; - $tag = $match[0][0]; - $start = $match[0][1]; - $name = $match[1][0]; - $selfClosed = array_get($match, 3, false); - $text_subselection = substr($text, $start + strlen($tag)); - - if (isset($match[2])) { - $raw_params = $this->injectExtractions($match[2][0], '__cond_str'); - $parameters = $this->parseParameters($raw_params, $cb_data); - $parameters = $this->parseVariablesInsideParameters($parameters, $data); - } - - if ($this->preg_match('/{{\s*\/'.preg_quote($name, '/').'\s*}}/m', $text_subselection, $match, PREG_OFFSET_CAPTURE) && ! $selfClosed) { - $content = substr($text_subselection, 0, $match[0][1]); - $tag .= $content.$match[0][0]; - - // Is there a nested block under this one existing with the same name? - $nested_regex = '/{{\s*('.preg_quote($name, '/').')(\s.*?)}}(.*?){{\s*\/\1\s*}}/ms'; - if ($this->preg_match($nested_regex, $content.$match[0][0], $nested_matches)) { - $nested_content = $this->preg_replace('/{{\s*\/'.preg_quote($name, '/').'\s*}}/m', '', $nested_matches[0]); - $content = $this->createExtraction('nested_tag_pair', $nested_content, $nested_content, $content); - } - } - - $replacement = null; // oh look, another temporary variable. - - // If there's a matching value in the context, we would have intentionally treated it as - // a callback. If it's a query builder instance, we want to use the Query tag's index - // method to handle the logic. We'll pass the builder into the builder parameter. - [$exists, $value] = $this->getVariableExistenceAndValue($name, $data); - if ($exists) { - $value = $value instanceof Value ? $value->value() : $value; - if ($value instanceof Builder) { - $parameters['builder'] = $value; - $name = 'query'; - } - } - - $replacement = call_user_func_array($this->callback, [$this, $name, $parameters, $content, $data]); - - // Commenting out this line makes no change to parser test coverage. - // TODO: Work out what it's supposed to be doing and write a test. - // $replacement = $this->parseRecursives($replacement, $content); - - // look for tag pairs and (plugin) callbacks - if ($name != 'content' && ! $replacement) { - // is the callback a variable in our data set? - [$exists, $values] = $this->getVariableExistenceAndValue($name, $data); - - if ($exists) { - // is this a tag-pair? - if ($this->isLoopable($values)) { - // yes it is - // there might be parameters that will control how this - // tag-pair's data is filtered/sorted/limited/etc, - // look for those and apply those as needed - - // exact result grabbing ---------------------------------- - foreach ($parameters as $modifier => $parameters) { - $parameters = explode(':', $parameters); - $values = $this->runModifier($modifier, $values, $parameters, $data); - } - } - - if ($this->isLoopable($values) && ! empty($values)) { - if ($values instanceof Value) { - $values = $values->value(); - } - - if ($values instanceof Collection) { - $values = $values->all(); - } - - if (Arr::isAssoc($values)) { - $replacement = $this->parse($content, array_merge($data, $values)); - } else { - $values = $this->addLoopIterationVariables($values); - $replacement = collect($values) - ->map(function ($value) use ($content, $data) { - return (string) $this->parse($content, array_merge($data, $value)); - })->join(''); - } - } - } else { - // nope, this must be a callback - if (is_null($this->callback)) { - // TODO: what does this do? - $text = $this->createExtraction('__variables_not_callbacks', $text, $text, $text); - } elseif (! empty($cb_data[$name])) { - // value not found in the data block, so we check the - // cumulative callback data block for a value and use that - $text = $this->extractTagPairs($text, $cb_data); - $text = $this->parseVariables($text, $cb_data); - $text = $this->injectExtractions($text, 'callback_blocks'); - } - } - } - - // Variables within conditions can be parsed more than once. We can - // skip this block if it's already been run through $this->valueToLiteral - if ($inCondition && (substr($text, 0, 1) !== "'" && substr($text, -1, 1) !== "'")) { - $replacement = $this->valueToLiteral($replacement); - } - - $text = $this->preg_replace('/'.preg_quote($tag, '/').'/m', addcslashes((string) $replacement, '\\$'), $text, 1); - $text = $this->injectExtractions($text, 'nested_tag_pair'); - } - - // parse for recursives, as they may not have been parsed yet - $text = $this->parseRecursives($text, $this->original_text, $data); - - // re-inject any extractions - $text = $this->injectExtractions($text, '__variables_not_callbacks'); - - return $text; - } - - /** - * Parses {variables} with single braces inside parameters - * making sure the parameter's parameters are an array in order to resolve duplicates. - */ - public function parseVariablesInsideParameters($parameters, $data) - { - return collect($parameters)->map(function ($value) use ($data) { - $this->preg_match_all('/(\{\s*'.$this->variableRegex.'\s*\})/', $value, $matches); - - $value = str_replace(['{', '}'], ['{{', '}}'], $value); - $value = $this->parseVariables($value, $data); - - return $this->parseCallbackTags($value, $data); - })->all(); - } - - /** - * Parses all conditionals, then executes the conditionals. - * - * @param string $text Text to parse - * @param mixed $data Data to use when executing conditionals - * @return string - */ - public function parseConditionPairs($text, $data) - { - $this->preg_match_all($this->conditionalRegex, $text, $matches, PREG_SET_ORDER); - - $this->conditionalData = $data; - - /** - * $matches[][0] = Full Match - * $matches[][1] = Either 'if', 'unless', 'elseif', 'elseunless' - * $matches[][2] = Condition. - */ - foreach ($matches as $match) { - $this->inCondition = true; - - $condition = $this->processCondition($match[2], $data); - - $conditional = ''; - - $text = $this->preg_replace('/'.preg_quote($match[0], '/').'/m', addcslashes($conditional, '\\$'), $text, 1); - } - - $text = $this->preg_replace($this->conditionalElseRegex, '', $text); - $text = $this->preg_replace($this->conditionalEndRegex, '', $text); - - $text = $this->parsePhp($text); - $this->inCondition = false; - - return $text; - } - - /** - * Parses simple ternary conditional strings. - * - * @param string $text Text to parse - * @param mixed $data Data to use when executing conditionals - * @return string - */ - public function parseTernaries($text, $data) - { - if ($this->preg_match_all('/{{\s*([^{}]+[^}]\s(\?[^}]*\s\:|\?=).*)\s*}}/msU', $text, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - // Our made up "truth coalescing" syntax. - // eg. {{ true ?= "foo" }} is shorthand for {{ if true }}foo{{ /if }} - if ($match[2] === '?=') { - $bits = explode(' ?= ', $match[1]); - - // Parse the condition side of the statement - $condition = $this->processCondition($bits[0], $data, false); - - // Grab the desired output if true - $if_true = trim($bits[1]); - - // Build a PHP string to evaluate - $conditional = ''.addslashes($this->getVariable($if_true, $data)).''; - - // Do the evaluation - $output = stripslashes($this->parsePhp($conditional)); - - // Slide it on back into the template - $text = str_replace($match[0], $output, $text); - - } else { - // Regular old ternary - - // Split the tag up - $bits = explode('? ', $match[1]); - - // Parse the condition side of the statement - $condition = $this->processCondition(trim($bits[0]), $data, false); - - // Collect the rest of the data - [$if_true, $if_false] = explode(': ', $bits[1]); - - // Build a PHP string to evaluate - $conditional = 'getVariable(trim($if_true), $data, '')).'" : "'.addslashes($this->getVariable(trim($if_false), $data, '')).'"; ?>'; - - // Do the evaluation - $output = stripslashes($this->parsePhp($conditional)); - - // Slide it on back into the template - $text = str_replace($match[0], $output, $text); - } - } - } - - return $text; - } - - /** - * Parses and assembles a condition string. - * - * @param string $condition Text to parse - * @param mixed $data Data to use when executing conditionals - * @return string - */ - public function processCondition($condition, $data, $isTagPair = true) - { - if (strpos($condition, ' | ') !== false) { - $condition = str_replace(' | ', '|', $condition); - } - - // check for and extract callbacks - if ($this->preg_match_all('/\b(?!\{\s*)('.$this->callbackNameRegex.')(?!\s+.*?\s*\})\b/', $condition, $cb_matches)) { - foreach ($cb_matches[0] as $m) { - $condition = $this->createExtraction('__cond_callbacks', $m, "{$m}", $condition); - } - } - - // Extract all literal strings in the conditional to simplify - if ($this->preg_match_all('/(["\']).*?(?createExtraction('__cond_str', $m, $m, $condition); - } - } - - $condition = $this->preg_replace($this->conditionalNotRegex, '$1!$2', $condition); - - if ($this->preg_match_all($this->conditionalExistsRegex, $condition, $existsMatches, PREG_SET_ORDER)) { - foreach ($existsMatches as $m) { - $exists = 'true'; - if ($this->getVariable($m[2], $data, '__doesnt_exist__') === '__doesnt_exist__') { - $exists = 'false'; - } - $condition = $this->createExtraction('__cond_exists', $m[0], $m[1].$exists.$m[3], $condition); - } - } - - // replaced a static-ish call to a callback with an anonymous function so that we could - // also pass in the current callback (for later processing callback tags); also setting - // $ref so that we can use it within the anonymous function - $ref = $this; - $condition = $this->preg_replace_callback('/\b('.$this->variableRegex.'\b]?)/', function ($match) use ($ref) { - return $ref->processConditionVar($match); - }, $condition); - - $condition = $this->parseCallbackTags($condition, $data); - - // Re-extract the strings that have may have been added. - if ($this->preg_match_all('/(["\']).*?(?createExtraction('__cond_str', $m, $m, $condition); - } - } - - // Re-process variables by tricking processConditionVar - $this->inCondition = false; - - // replacements -- the $this->preg_replace_callback below is using word boundaries, which - // will break when one of your original variables gets replaced with a URL path - // (because word boundaries think slashes are boundaries) -- to fix this, we replace - // all instances of a literal string in single quotes with a temporary replacement - $replacements = []; - - // Literally replace literal strings - while ($this->preg_match("/('[^']+'|\"[^\"]+\")/", $condition, $replacement_matches)) { - $replacement_match = $replacement_matches[1]; - $replacement_hash = md5($replacement_match); - - $replacements[$replacement_hash] = $replacement_match; - $condition = str_replace($replacement_match, '__temp_replacement_'.$replacement_hash, $condition); - } - - // next, the original re-processing callback - $correct_regex = (strpos($condition, '(') === 0) ? $this->looseVariableRegex : $this->variableRegex; - - $condition = $this->preg_replace_callback('/\b('.$correct_regex.')\b/', [$this, 'processConditionVar'], $condition); - - // finally, replacing our placeholders with the original values - foreach ($replacements as $replace_key => $replace_value) { - $condition = str_replace('__temp_replacement_'.$replace_key, $replace_value, $condition); - } - - // Ternary statements are evaluated inline and this have no - // tag pair contents to process conditionally. - if ($isTagPair) { - $this->inCondition = true; - } - - // evaluate special comparisons - if (strpos($condition, ' ~ ') !== false) { - $new_condition = $this->preg_replace_callback('/(.*?)\s*~\s*(__cond_str_[a-f0-9]{32})/', function ($cond_matches) { - return '$this->preg_match('.$cond_matches[2].', '.$cond_matches[1].', $temp_matches)'; - }, $condition); - - if ($new_condition !== false) { - $condition = $new_condition; - } - } - - // Re-inject any strings we extracted - $condition = $this->injectExtractions($condition, '__cond_str'); - $condition = $this->injectExtractions($condition, '__cond_exists'); - $condition = $this->injectExtractions($condition, '__cond_callbacks'); - - return $condition; - } - - /** - * Recursively process a callback tag with a passed child array. - * - * @param string $text The replaced text after a callback. - * @param string $orig_text The original text, before a callback is called. - * @return string $text - */ - public function parseRecursives($text, $orig_text, $data) - { - // Is there a {{ *recursive [array_key]* }} tag here, let's loop through it. - if ($this->preg_match($this->recursiveRegex, $text, $match)) { - $tag = $match[0]; - $array_key = $match[1]; - - // check to see if the recursive variable we're looking for is set - // within the current data for this run-through, if it isn't, just - // abort and return the text - if (! Arr::get($data, $array_key)) { - return $text; - } - - $next_tag = null; - $children = Arr::get($data, $array_key); - - // if the array key is scoped, we'll add a scope to the array - if (strpos($array_key, ':') !== false) { - $scope = explode(':', $array_key)[0]; - $children = Arr::addScope($children, $scope); - } - - $child_count = count($children); - $count = 1; - - // Let's make sure it's multi-dimensional. - if ($child_count == count($children, COUNT_RECURSIVE)) { - $children = [$children]; - $child_count = 1; - } - - foreach ($children as $child) { - $has_children = true; - - // If this is a object let's convert it to an array. - is_array($child) or $child = (array) $child; - - // Does this child not contain any children? - // Let's set it as empty then to avoid any errors. - if (! array_key_exists($array_key, $child)) { - $child[$array_key] = []; - $has_children = false; - } - - $replacement = $this->parse($orig_text, array_merge($data, $child)); - - // If this is the first loop we'll use $tag as reference, if not - // we'll use the previous tag ($next_tag) - $current_tag = ($next_tag !== null) ? $next_tag : $tag; - - // If this is the last loop set the next tag to be empty - // otherwise hash it. - $next_tag = ($count == $child_count) ? '' : md5($tag.$replacement); - - $text = str_replace($current_tag, $replacement.$next_tag, $text); - - if ($has_children) { - $text = $this->parseRecursives($text, $orig_text, $data); - } - - $count++; - } - } - - return $text; - } - - /** - * Injects noparse extractions. - * - * This is so that multiple parses can store noparse - * extractions and all noparse can then be injected right - * before data is displayed. - * - * @param string $text Text to inject into - * @return string - */ - public function injectNoparse($text) - { - return $this->injectExtractions($text, 'noparse'); - } - - /** - * This is used as a callback for the conditional parser. It takes a variable - * and returns the value of it, properly formatted. - * - * @param array $match A match from $this->preg_replace_callback - * @return string - */ - public function processConditionVar($match) - { - $var = is_array($match) ? $match[0] : $match; - - if (in_array(strtolower($var), ['true', 'false', 'null', 'or', 'and']) or - strpos($var, '__cond_str') === 0 or - strpos($var, '__cond_exists') === 0 or - - // adds a new temporary replacement to deal with string literals - strpos($var, '__temp_replacement') === 0 or - - is_numeric($var) - ) { - return $var; - } - - $var = $this->injectExtractions($var, '__cond_callbacks'); - - $value = $this->getVariable($var, $this->conditionalData, '__processConditionVar__'); - - // if the resulting value of a variable is a string that contains another variable, - // let's find that variable's value as well - if (is_string($value)) { - while ($this->preg_match($this->variableTagRegex, $value, $matches)) { - $previous_value = $value; - $value = $this->parseVariables($value, $this->conditionalData); - - // nothing changed, break out, prevents any sort of infinite looping - if ($previous_value === $value) { - break; - } - } - } - - if ($value === '__processConditionVar__') { - return $this->inCondition ? $var : 'null'; - } - - return $this->valueToLiteral($value); - } - - /** - * This is used as a callback for the conditional parser. It takes a variable - * and returns the value of it, properly formatted. - * - * @param array $match A match from $this->preg_replace_callback - * @return string - */ - protected function processParamVar($match) - { - return $match[1].$this->processConditionVar($match[2]); - } - - /** - * Takes a value and returns the literal value for it for use in a tag. - * - * @param string $value Value to convert - * @return string - */ - protected function valueToLiteral($value) - { - if ($value instanceof Builder) { - return $value->count(); - } elseif ($value instanceof Collection) { - return $value->isEmpty() ? 'false' : 'true'; - } elseif ($value instanceof ViewErrorBag) { - return $value->getBags() ? 'true' : 'false'; - } elseif (is_array($value)) { - return ! empty($value) ? 'true' : 'false'; - } elseif (is_object($value) and method_exists($value, '__toString')) { - return var_export((string) $value, true); - } elseif (is_object($value)) { - return 'true'; - } else { - return var_export($value, true); - } - } - - public function valueWithNoparse($text) - { - return $this->extractNoparse(str_replace('{{', '@{{', $text)); - } - - /** - * Ignore tags-who-must-not-be-parsed. - * - * @param string $text The text to extract from - * @return string - */ - public function extractNoparse($text) - { - // Ignore @{{ tags }} so we don't have to write JavaScript like animals. - if ($this->preg_match_all($this->ignoreRegex, $text, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - $text = $this->createExtraction('noparse', $match[0], ltrim($match[0], '@'), $text); - } - } - - /** - * $matches[][0] is the raw noparse match - * $matches[][1] is the noparse contents. - */ - if ($this->preg_match_all($this->noparseRegex, $text, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - $text = $this->createExtraction('noparse', $match[0], $match[1], $text); - } - } - - return $text; - } - - /** - * Extracts the looped tags so that we can parse conditionals then re-inject. - * - * @param string $text The text to extract from - * @param array $data Data array to use - * @return string - */ - protected function extractTagPairs($text, $data = []) - { - if ($this->preg_match_all($this->variableLoopRegex, $text, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - $text = $this->createExtraction('tag_pair', $match[0], $match[0], $text); - } - } - - if ($this->preg_match_all($this->callbackBlockRegex, $text, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - $text = $this->createExtraction('callback_blocks', $match[0], $match[0], $text); - } - } - - return $text; - } - - /** - * Extracts text out of the given text and replaces it with a hash which - * can be used to inject the extractions replacement later. - * - * @param string $type Type of extraction - * @param string $extraction The text to extract - * @param string $replacement Text that will replace the extraction when re-injected - * @param string $text Text to extract out of - * @return string - */ - protected function createExtraction($type, $extraction, $replacement, $text) - { - $hash = md5($replacement); - $this->extractions[$type][$hash] = $replacement; - - return str_replace($extraction, "{$type}_{$hash}", $text); - } - - /** - * Injects all of the extractions for a given type. - * - * @param string $text Text to inject into - * @param string $type Type of extraction to inject - * @return string - */ - protected function injectExtractions($text, $type) - { - if (isset($this->extractions[$type])) { - foreach ($this->extractions[$type] as $hash => $replacement) { - if (strpos($text, "{$type}_{$hash}") !== false) { - $text = str_replace("{$type}_{$hash}", $replacement, $text); - unset($this->extractions[$type][$hash]); - } - } - } - - return $text; - } - - /** - * Takes a scope-notated key and finds the value for it in the given - * array or object. - * - * @param string $key Dot-notated key to find - * @param array|object $data Array or object to search - * @param mixed $default Default value to use if not found - * @return mixed - */ - public function getVariable($key, $context, $default = null) - { - [$key, $modifiers] = $this->parseModifiers($key); - - if ($this->isLiteralString($key)) { - $data = trim($key, '"\''); - } else { - [$exists, $data] = $this->getVariableExistenceAndValue($key, $context); - if (! $exists) { - return $default; - } - } - - // execute the modifier chain - if ($modifiers) { - foreach ($modifiers as $modifier) { - [$modifier, $parameters] = $this->unpackModifier($modifier); - $data = $this->runModifier($modifier, $data, $parameters, $context); - } - } - - if ($data instanceof Value) { - $data = $data->antlersValue($this, $context); - } - - return $data; - } - - /** - * Find out whether a given variable exists within a given context. - * - * @param string $key - * @param array $context - * @return array Array of [boolean whether it exists, value of the key] - */ - protected function getVariableExistenceAndValue($key, $context) - { - try { - $key = $this->replaceDynamicArrayKeys($key, $context); - } catch (ArrayKeyNotFoundException $exception) { - return [false, null]; - } - - // If the key exists in the context, great, we're done. - if (Arr::has($context, $key)) { - return [true, Arr::get($context, $key)]; - } - - // If there was no scope glue, there's nothing more we can check. - if (! Str::contains($key, [':', '.'])) { - return [false, null]; - } - - // If it didn't exist and the key contained a scope glue, we'll try again, but this - // time using the first part of the key as the new context. For example, if - // we had been given "foo:bar:baz" as the key, we'll try to get the "foo" - // from the context and get the "bar:baz" from within within its value. - [$first, $rest] = preg_split("/(\:|\.)/", $key, 2); - - if (! Arr::has($context, $first)) { - // If it's not found in the context, we'll try looking for it in the cascade. - if ($cascading = $this->cascade->get($first)) { - return $this->getVariableExistenceAndValue($rest, $cascading); - } - - return [false, null]; - } - - $context = Arr::get($context, $first); - - if ($context instanceof Value) { - $context = $context->value(); - } - - if ($context instanceof Builder) { - $context = $context->get(); - } - - if ($context instanceof Augmentable) { - $context = $context->toAugmentedArray(); - } - - if ($context instanceof Arrayable) { - $context = $context->toArray(); - } - - // It will do this recursively until it's out of colon delimiters or values. - if (is_array($context)) { - return $this->getVariableExistenceAndValue($rest, $context); - } - - return [false, null]; - } - - /** - * Replaces a dynamic array key access with the actual value. - * - * Example: - * - * The context contains a variable 'key' with the value 'foo' and there is an - * array 'old' containing a key 'foo'. The value of the array with the key 'foo' - * can be accessed using the variable 'key' like this inside the template: - * - * {{ old[key] }} or {{ if old[key] }} ... - * - * @param string $key - * @param array $context - * @return string - * - * @throws ArrayKeyNotFoundException - */ - protected function replaceDynamicArrayKeys($key, $context) - { - if (! Str::containsAll($key, ['[', ']'])) { - return $key; - } - - // If the key contains dynamic array keys, let's replace them with their actual value. - return trim($this->preg_replace_callback('/\[([\'"\w\d-]*)\]/', function ($matches) use ($context) { - $key = $matches[1]; - - if ($this->isLiteralString($key)) { - return '.'.trim($key, '"\''); - } - - $value = Arr::get($context, $key); - - if (! (is_string($value) || is_numeric($value))) { - // If the variable does not exist in the context or the value is not a valid key - // the replacement should not return a value to prevent unexpected behaviour. - throw new ArrayKeyNotFoundException(); - } - - return '.'.$value; - }, $key)); - } - - /** - * Splits a string into a modifier and its parameters. - * - * @param string $text Text to evaluate - * @return array - */ - protected function unpackModifier($modifier) - { - $parts = explode(':', $modifier); - $modifier = array_shift($parts); - - return [$modifier, $parts]; - } - - /** - * Checks if a string is wrapped in quotes and should be left alone. - * - * @param string $string String to evaluate - * @return bool - */ - protected function isLiteralString($string) - { - return $this->preg_match('/^(["\']).*\1$/m', $string); - } - - protected function literalStringWithoutQuotes($string) - { - return substr($string, 1, strlen($string) - 2); - } - - /** - * Evaluates the PHP in the given string. - * - * @param string $text Text to evaluate - * @return string - * - * @throws \Statamic\Exceptions\ParsingException - */ - protected function parsePhp($text) - { - ob_start(); - - try { - eval('?>'.$text.'getMessage()} on line {$e->getLine()} of:\n\n{$text}"); - } - - return ob_get_clean(); - } - - /** - * Parses a parameter string into an array. - * - * @param string $parameters The string of parameters - * @param array $data Array of data - * @return array - */ - protected function parseParameters($parameters, $data) - { - $this->conditionalData = $data; - $this->inCondition = true; - - // Extract all literal strings in the conditional to simplify - if ($this->preg_match_all('/(["\']).*?(?createExtraction('__param_str', $m, $m, $parameters); - } - } - - $parameters = $this->preg_replace_callback( - '/(.*?\s*=\s*(?!__))('.$this->variableRegex.')/is', - [$this, 'processParamVar'], - $parameters - ); - - $parameters = $this->preg_replace('/(.*?\s*=\s*(?!\{\s*)(?!__))('.$this->callbackNameRegex.')(?!\s*\})\b/', '$1{$2}', $parameters); - $parameters = $this->parseCallbackTags($parameters, $data); - - // Re-inject extracted strings - $parameters = $this->injectExtractions($parameters, '__param_str'); - $this->inCondition = false; - - if ($this->preg_match_all('/(.*?)\s*=\s*(\'|"|&#?\w+;)(.*?)(? $attr) { - // if there are duplicate parameters, save an array instead of a string - $key = trim($matches[1][$i]); - $value = stripslashes($matches[3][$i]); - - // if it already exists (a parameter with this name already parsed) - if (isset($return[$key])) { - // if its not already an array, turn it into one - if (! is_array($return[$key])) { - $return[$key] = [$return[$key]]; - } - - $return[$key][] = $value; - } else { - // parameter hasnt been parsed yet. just save a string as usual - $return[$key] = $value; - } - } - - return $return; - } - - return []; - } - - protected function parseModifiers($key) - { - $parts = explode('|', $key); - $key = trim(Arr::get($parts, 0)); - $modifiers = array_map('trim', (array) array_slice($parts, 1)); - - return [$key, $modifiers]; - } - - /** - * Manipulate data with the use of Modifiers. - * - * @return mixed - */ - protected function runModifier($modifier, $data, $parameters, $context = []) - { - $data = $data instanceof Value ? $data : new Value($data); - - if ($modifier === 'raw') { - return $data->raw(); - } - - if ($modifier === 'noparse') { - return $data->value(); - } - - $value = $data->antlersValue($this, $context); - - if (Str::startsWith($modifier, ':')) { - $parameters = array_map(function ($param) use ($context) { - return $context[$param] ?? null; - }, $parameters); - $modifier = substr($modifier, 1); - } - - try { - return Modify::value($value)->context($context)->$modifier($parameters)->fetch(); - } catch (ModifierException $e) { - throw_if(config('app.debug'), ($prev = $e->getPrevious()) ? $prev : $e); - Log::notice(sprintf('Error in [%s] modifier: %s', $e->getModifier(), $e->getMessage())); - - return $value; - } - } - - protected function isLoopable($value) - { - if (is_array($value) || $value instanceof Collection) { - return true; - } - - if (! $value instanceof Value) { - return false; - } - - $value = $value->value(); - - return is_array($value) || $value instanceof Collection; - } - - protected function viewException($e, $data) - { - if (! class_exists(ViewException::class)) { - return $e; - } - - // Redirects etc should work instead of actually generating an exception. - if ($e instanceof HttpException || $e instanceof HttpResponseException) { - return $e; - } - - $exceptionClass = ViewException::class; - - if (in_array(ProvidesSolution::class, class_implements($e))) { - $exceptionClass = ViewExceptionWithSolution::class; - } - - $exception = new $exceptionClass($e->getMessage(), 0, 1, $this->view, null, $e); - - if ($exceptionClass === ViewExceptionWithSolution::class) { - $exception->setSolution($e->getSolution()); - } - - $trace = $exception->getPrevious()->getTrace(); - - array_unshift($trace, [ - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - - $traceProperty = new ReflectionProperty('Exception', 'trace'); - $traceProperty->setAccessible(true); - $traceProperty->setValue($exception, $trace); - - $exception->setView($this->view); - $exception->setViewData($this->getViewDataForException($data)); - - return $exception; - } - - protected function getViewDataForException($data) - { - return collect($data) - ->except(['__env', 'app', 'config']) - ->map(function ($value) { - if ($value instanceof Value) { - return new IgnitionViewValue($value); - } - - if (is_array($value)) { - return $this->getViewDataForException($value); - } - - return $value; - }) - ->sortKeys() - ->all(); - } - - protected function getValueAsRenderableString($value, $variable) - { - if (is_array($value)) { - $value = null; - Log::debug("Cannot render an array variable as a string: {{ $variable }}"); - } - - if (is_object($value) && ! method_exists($value, '__toString')) { - $value = null; - Log::debug("Cannot render an object variable as a string: {{ $variable }}"); - } - - return (string) $value; - } - - protected function isNullWhenUsedInStrings($value) - { - if ($value instanceof ArrayableString) { - $value = $value->value(); - } - - return is_null($value) || $value === '__lex_no_value__'; - } - - protected function preg_match($pattern, $subject, &$matches = [], $flags = 0, $offset = 0) - { - $return = preg_match($pattern, $subject, $matches, $flags, $offset); - $this->handleRegexError(); - - return $return; - } - - protected function preg_match_all($pattern, $subject, &$matches = [], $flags = 0, $offset = 0) - { - $return = preg_match_all($pattern, $subject, $matches, $flags, $offset); - $this->handleRegexError(); - - return $return; - } - - protected function preg_replace(...$args) - { - $return = preg_replace(...$args); - $this->handleRegexError(); - - return $return; - } - - protected function preg_replace_callback(...$args) - { - $return = preg_replace_callback(...$args); - $this->handleRegexError(); - - return $return; - } - - protected function handleRegexError() - { - if (! preg_last_error()) { - return; - } - - throw new RegexError(preg_last_error(), $this->view); - } - - protected function sanitizePhp($text) - { - if ($this->allowPhp) { - return $text; - } - - $text = str_replace('error = $error; - $this->view = $view; - - $this->setMessage(); - } - - protected function setMessage() - { - $this->message = array_flip(array_filter(get_defined_constants(true)['pcre'], function ($value) { - return substr($value, -6) === '_ERROR'; - }, ARRAY_FILTER_USE_KEY))[$this->error]; - } - - public function getSolution(): Solution - { - $message = $this->message; - $description = "Error {$this->message} enountered when parsing regular expression."; - - if ($this->message === 'PREG_BACKTRACK_LIMIT_ERROR') { - $message = 'Regular expression backtrack limit reached'; - $description = 'This typically happens when a view is too large. You can try splitting it into partials.'; - if ($this->view) { - $description .= "\\\n\\\nView: `".Str::after($this->view, base_path().'/').'`'; - } - } - - return BaseSolution::create($message) - ->setSolutionDescription($description) - ->setDocumentationLinks([ - 'Read the Antlers guide' => Statamic::docsUrl('antlers'), - 'Read about the Partial tag' => Statamic::docsUrl('tags/partial'), - ]); - } -} diff --git a/src/View/View.php b/src/View/View.php index 161812e1a0f..ad9f40b7c04 100644 --- a/src/View/View.php +++ b/src/View/View.php @@ -114,10 +114,8 @@ public function render(): string $renderedContents = $contents->render(); - if (config('statamic.antlers.version') == 'runtime') { - $renderedContents = LiteralReplacementManager::processReplacements($renderedContents); - $renderedContents = StackReplacementManager::processReplacements($renderedContents); - } + $renderedContents = LiteralReplacementManager::processReplacements($renderedContents); + $renderedContents = StackReplacementManager::processReplacements($renderedContents); return $renderedContents; } diff --git a/tests/Antlers/ContainerTest.php b/tests/Antlers/ContainerTest.php deleted file mode 100644 index 2aba5ffdbb4..00000000000 --- a/tests/Antlers/ContainerTest.php +++ /dev/null @@ -1,81 +0,0 @@ -config->set('statamic.antlers.version', 'regex'); - } - - protected function useRuntimeParser($app) - { - $app->config->set('statamic.antlers.version', 'runtime'); - } - - /** - * @test - * - * @define-env useRegexParser - **/ - public function it_resolves_regex_parser_using_contract() - { - $this->assertRegexParserInstantiatedCorrectly(app(ParserContract::class)); - } - - /** - * @test - * - * @define-env useRuntimeParser - **/ - public function it_resolves_runtime_parser_using_contract() - { - $this->assertRuntimeParserInstantiatedCorrectly(app(ParserContract::class)); - } - - /** - * @test - * - * @define-env useRegexParser - **/ - public function it_resolves_regex_parser_using_class() - { - $this->assertRegexParserInstantiatedCorrectly(app(Parser::class)); - } - - /** - * @test - * - * @define-env useRuntimeParser - **/ - public function it_resolves_runtime_parser_using_class() - { - $this->assertRuntimeParserInstantiatedCorrectly(app(RuntimeParser::class)); - } - - private function assertRegexParserInstantiatedCorrectly($parser) - { - $this->assertInstanceOf(Parser::class, $parser); - - $cascade = (new \ReflectionClass($parser))->getProperty('cascade'); - $cascade->setAccessible(true); - $this->assertEquals(app(Cascade::class), $cascade->getValue($parser)); - - $callback = (new \ReflectionClass($parser))->getProperty('callback'); - $callback->setAccessible(true); - $this->assertEquals([Engine::class, 'renderTag'], $callback->getValue($parser)); - } - - private function assertRuntimeParserInstantiatedCorrectly($parser) - { - $this->assertInstanceOf(RuntimeParser::class, $parser); - } -} diff --git a/tests/Antlers/Runtime/TemplateTest.php b/tests/Antlers/Runtime/TemplateTest.php index 8e922ad4c90..fffe60c6f53 100644 --- a/tests/Antlers/Runtime/TemplateTest.php +++ b/tests/Antlers/Runtime/TemplateTest.php @@ -2,45 +2,2486 @@ namespace Tests\Antlers\Runtime; -use Facades\Tests\Factories\EntryFactory; +use Facades\Statamic\Fields\FieldtypeRepository; +use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Facades\Log; -use Statamic\Facades\Collection; +use Illuminate\Support\MessageBag; +use Illuminate\Support\ViewErrorBag; +use Mockery; +use Statamic\Contracts\Data\Augmentable; +use Statamic\Contracts\Query\Builder; +use Statamic\Data\HasAugmentedData; +use Statamic\Facades\Entry; +use Statamic\Fields\ArrayableString; +use Statamic\Fields\Blueprint; +use Statamic\Fields\Field; +use Statamic\Fields\Fieldtype; +use Statamic\Fields\Value; +use Statamic\Fields\Values; +use Statamic\Tags\Tags; +use Statamic\View\Cascade; +use Tests\Antlers\Fixtures\Addon\Tags\RecursiveChildren; use Tests\Antlers\ParserTestCase; +use Tests\Factories\EntryFactory; use Tests\FakesViews; use Tests\PreventSavingStacheItemsToDisk; -use Tests\View\Antlers\NonArrayableObject; -use Tests\View\Antlers\ParserTests; class TemplateTest extends ParserTestCase { use FakesViews; - use ParserTests; use PreventSavingStacheItemsToDisk; + private $variables; + + public function setUp(): void + { + parent::setUp(); + + $this->variables = [ + 'default_key' => 'two', + 'first_key' => 'three', + 'second_key' => 'deep', + 'good' => true, + 'bad' => false, + 'unknown' => null, + 'string' => 'Hello wilderness', + 'simple' => ['one', 'two', 'three'], + 'complex' => [ + ['string' => 'the first string'], + ['string' => 'the second string'], + ], + 'complex_string' => 'Hello wildernesses', + 'associative' => [ + 'one' => 'hello', + 'two' => 'wilderness', + 'three' => [ + 'deep' => 'Very deep', + ], + ], + 'date' => 'June 19 2012', + 'content' => 'Paragraph', + ]; + } + + /** @test */ + public function string_variable() + { + $template = '{{ string }}'; + + $this->assertEquals('Hello wilderness', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function string_variables_with_tight_braces() + { + $template = '{{string}}'; + + $this->assertEquals('Hello wilderness', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function array_variables() + { + $template = <<<'EOT' +before +{{ simple }} + {{ value }}, {{ count or "0" }}, {{ index or "0" }}, {{ total_results }} + {{ if first }}first{{ elseif last }}last{{ else }}neither{{ /if }} + + +{{ /simple }} +after +EOT; + + $expected = <<<'EOT' +before + + one, 1, 0, 3 + first + + + + two, 2, 1, 3 + neither + + + + three, 3, 2, 3 + last + + + +after +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables)); + + $this->assertEquals('wilderness', $this->renderString('{{ associative[default_key] }}', $this->variables)); + $this->assertEquals('Very deep', $this->renderString('{{ associative[first_key][second_key] }}', $this->variables)); + $this->assertEquals('Very deep', $this->renderString('{{ associative[\'three\'][second_key] }}', $this->variables)); + $this->assertEquals('Very deep', $this->renderString('{{ associative["three"][second_key] }}', $this->variables)); + $this->assertEquals('Very deep', $this->renderString('{{ associative.three[second_key] }}', $this->variables)); + $this->assertEquals('Very deep', $this->renderString('{{ associative:three[second_key] }}', $this->variables)); + } + + /** @test */ + public function complex_array_variable() + { + $template = <<<'EOT' +before +{{ complex }} + {{ string }}, {{ count or "0" }}, {{ index or "0" }}, {{ total_results }} + {{ if first }}first{{ elseif last }}last{{ else }}neither{{ /if }} + + +{{ /complex }} +after +EOT; + + $expected = <<<'EOT' +before + + the first string, 1, 0, 2 + first + + + + the second string, 2, 1, 2 + last + + + +after +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables)); + } + + /** @test */ + public function associative_array_variable() + { + $template = <<<'EOT' +before +{{ associative }} + {{ one }} + {{ two }} + {{ value or "no value" }} + {{ key or "no key" }} + {{ count or "no count" }} + {{ index or "no index" }} + {{ total_results or "no total_results" }} + {{ first or "no first" }} + {{ last or "no last" }} +{{ /associative }} +after +EOT; + + $expected = <<<'EOT' +before + + hello + wilderness + no value + no key + no count + no index + no total_results + no first + no last + +after +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables)); + } + + /** @test */ + public function scope_glue() + { + $template = '{{ associative:one }} {{ associative.two }}'; + + $this->assertEquals('hello wilderness', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function non_existent_variables_should_be_null() + { + $template = '{{ missing }}'; + + $this->assertEquals('', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function accessing_strings_as_arrays_returns_null() + { + $this->assertEquals('bar, ><', $this->renderString('{{ foo }}, >{{ foo:test }}<', ['foo' => 'bar'])); + } + + /** @test */ + public function accessing_string_as_array_which_exists_as_callback_calls_the_callback() + { + (new class extends Tags + { + public static $handle = 'foo'; + + public function test() + { + return 'callback'; + } + })::register(); + + $this->assertEquals('bar, callback', $this->renderString('{{ foo }}, {{ foo:test }}', ['foo' => 'bar'], true)); + } + + /** @test */ + public function non_arrays_cannot_be_looped() + { + Log::shouldReceive('debug')->once() + ->with('Cannot loop over non-loopable variable: {{ string }}', [ + 'line' => 1, 'file' => '', + ]); + + $template = '{{ string }} {{ /string }}'; + + $this->assertEquals('', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function static_strings_with_double_quotes_should_be_left_alone() + { + $template = '{{ "Thundercats are Go!" }}'; + + $this->assertEquals('Thundercats are Go!', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function static_strings_with_single_quotes_should_be_left_alone() + { + $template = "{{ 'Thundercats are Go!' }}"; + + $this->assertEquals('Thundercats are Go!', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function static_strings_with_double_quotes_can_be_modified() + { + $template = '{{ "Thundercats are Go!" | upper }}'; + + $this->assertEquals('THUNDERCATS ARE GO!', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function static_strings_with_single_quotes_can_be_modified() + { + $template = "{{ 'Thundercats are Go!' | upper }}"; + + $this->assertEquals('THUNDERCATS ARE GO!', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function single_braces_should_not_be_parsed() + { + $template = '{string}'; + + $this->assertEquals('{string}', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function modified_non_existent_variables_should_be_null() + { + $template = '{{ missing|upper }}'; + + $this->assertEquals('', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function unclosed_array_variable_pairs_should_be_null() + { + Log::shouldReceive('debug')->once() + ->with('Cannot render an array variable as a string: {{ simple }}', [ + 'line' => 1, 'file' => '', + ]); + + $template = '{{ simple }}'; + + $this->assertEquals('', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function single_condition() + { + $template = '{{ if string == "Hello wilderness" }}yes{{ endif }}'; + + $this->assertEquals('yes', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function multiple_and_conditions() + { + $template = '{{ if string == "Hello wilderness" && content }}yes{{ endif }}'; + + $this->assertEquals('yes', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function multiple_or_conditions() + { + $should_pass = '{{ if string == "failure" || string == "Hello wilderness" }}yes{{ endif }}'; + $should_fail = '{{ if string == "failure" or string == "womp" }}yes{{ endif }}'; + + $this->assertEquals('yes', $this->renderString($should_pass, $this->variables)); + $this->assertEquals('', $this->renderString($should_fail, $this->variables)); + } + + /** @test */ + public function or_existence_conditions() + { + $should_pass = '{{ if string || strudel }}yes{{ endif }}'; + $should_also_pass = '{{ if strudel or string }}yes{{ endif }}'; + $should_fail = '{{ if strudel || wurst }}yes{{ endif }}'; + $should_also_fail = '{{ if strudel or wurst }}yes{{ endif }}'; + + $this->assertEquals('yes', $this->renderString($should_pass, $this->variables)); + $this->assertEquals('yes', $this->renderString($should_also_pass, $this->variables)); + $this->assertEquals('', $this->renderString($should_fail, $this->variables)); + $this->assertEquals('', $this->renderString($should_also_fail, $this->variables)); + } + + /** @test */ + public function conditions_on_overlapping_variable_names() + { + $template = '{{ if complex }}{{ complex limit="1" }}{{ string }}{{ /complex }}{{ /if }}'; + + $this->assertEquals('the first string', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function loop_with_param_inside_condition_matching_variable_name() + { + $template = '{{ if complex_string }}{{ complex_string }}{{ /if }}{{ complex }}{{ /complex }}'; + + $this->assertEquals('Hello wildernesses', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function ternary_condition() + { + $template = '{{ string ? "Pass" : "Fail" }}'; + + $this->assertEquals('Pass', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function ternary_condition_with_dynamic_array() + { + $template = '{{ associative[default_key] ? "Pass" : "Fail" }}'; + + $this->assertEquals('Pass', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function ternary_condition_isnt_too_greedy() + { + $template = '{{ content }} {{ string ? "Pass" : "Fail" }} {{ content }}'; + + $this->assertEquals('Paragraph Pass Paragraph', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function ternary_condition_with_a_variable() + { + $template = '{{ string ? string : "Fail" }}'; + + $this->assertEquals('Hello wilderness', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function ternary_condition_with_modifiers() + { + $template = '{{ string ? string | upper : "Fail" }}'; + + $this->assertEquals('HELLO WILDERNESS', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function ternary_condition_with_modifiers_and_dynamic_array() + { + $template = '{{ string ? associative[default_key] | upper : "Fail" }}'; + + $this->assertEquals('WILDERNESS', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function ternary_condition_with_multiple_lines() + { + $template = <<<'EOT' +{{ string + ? "Pass" + : "Fail" }} +EOT; + + $this->assertEquals('Pass', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function ternary_escapes_quotes_properly() + { + $data = ['condition' => true, 'var' => '"Wow" said the man']; + $template = '{{ condition ? var : "nah" }}'; + + $this->assertEquals('"Wow" said the man', $this->renderString($template, $data)); + } + + /** @test */ + public function ternary_condition_inside_parameter() + { + $this->app['statamic.tags']['test'] = \Tests\Fixtures\Addon\Tags\Test::class; + + $this->assertEquals('yes', $this->renderString( + "{{ test variable='{{ good ? 'yes' : 'fail' }}' }}", + $this->variables, + true + )); + + $this->assertEquals('fail', $this->renderString( + "{{ test variable='{{ bad ? 'yes' : 'fail' }}' }}", + $this->variables, + true + )); + + $this->assertEquals('fail', $this->renderString( + "{{ test variable='{{ unknown ?? 'fail' }}' }}", + $this->variables, + true + )); + + $this->assertEquals('yes', $this->renderString( + "{{ test variable='{{ !unknown ? 'yes' : 'fail' }}' }}", + $this->variables, + true + )); + } + + /** @test */ + public function null_coalescence() + { + // or, ?:, and ?? are all aliases. + // while ?: and ?? have slightly different behaviors in php, they work the same in antlers. + + $this->assertEquals('Hello wilderness', $this->renderString('{{ string or "Pass" }}', $this->variables)); + $this->assertEquals('Hello wilderness', $this->renderString('{{ string ?: "Pass" }}', $this->variables)); + $this->assertEquals('Hello wilderness', $this->renderString('{{ string ?? "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ missing or "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ missing ?: "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ missing ?? "Pass" }}', $this->variables)); + + $this->assertEquals('Pass', $this->renderString('{{ missing or "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ missing ?: "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ missing ?? "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ missing[thing] or "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ missing[thing] ?: "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ missing[thing] ?? "Pass" }}', $this->variables)); + } + + /** @test */ + public function truth_coalescing() + { + $this->assertEquals('Pass', $this->renderString('{{ string ?= "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ associative:one ?= "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ associative[default_key] ?= "Pass" }}', $this->variables)); + $this->assertEquals('', $this->renderString('{{ missing ?= "Pass" }}', $this->variables)); + $this->assertEquals('', $this->renderString('{{ missing:thing ?= "Pass" }}', $this->variables)); + $this->assertEquals('', $this->renderString('{{ missing[thing] ?= "Pass" }}', $this->variables)); + + // Negating with ! + $this->assertEquals('', $this->renderString('{{ !string ?= "Pass" }}', $this->variables)); + $this->assertEquals('', $this->renderString('{{ !associative:one ?= "Pass" }}', $this->variables)); + $this->assertEquals('', $this->renderString('{{ !associative[default_key] ?= "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ !missing ?= "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ !missing:thing ?= "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ !missing[thing] ?= "Pass" }}', $this->variables)); + + // and with spaces + $this->assertEquals('', $this->renderString('{{ ! string ?= "Pass" }}', $this->variables)); + $this->assertEquals('', $this->renderString('{{ ! associative:one ?= "Pass" }}', $this->variables)); + $this->assertEquals('', $this->renderString('{{ ! associative[default_key] ?= "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ ! missing ?= "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ ! missing:thing ?= "Pass" }}', $this->variables)); + $this->assertEquals('Pass', $this->renderString('{{ ! missing[thing] ?= "Pass" }}', $this->variables)); + } + + /** @test */ + public function truth_coalescing_inside_loop() + { + $template = '{{ complex }}{{ first ?= "Pass" }}{{ /complex }}'; + + $this->assertEquals('Pass', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function single_standard_string_modifier_tight() + { + $template = '{{ string|upper }}'; + + $this->assertEquals('HELLO WILDERNESS', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function chained_standard_string_modifiers_tight() + { + $template = '{{ string|upper|lower }}'; + + $this->assertEquals('hello wilderness', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function single_standard_string_modifier_relaxed() + { + $template = '{{ string | upper }}'; + + $this->assertEquals('HELLO WILDERNESS', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function chained_standard_string_modifiers_relaxed() + { + $template = '{{ string | upper | lower }}'; + + $this->assertEquals('hello wilderness', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function chained_standard_string_modifiers_from_dynamic_array_relaxed() + { + $template = '{{ associative[default_key] | upper | lower }}'; + + $this->assertEquals('wilderness', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function single_parameter_string_modifier() + { + $template = "{{ string upper='true' }}"; + + $this->assertEquals('HELLO WILDERNESS', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function single_parameter_string_from_array_modifier() + { + $this->assertEquals( + 'WILDERNESS', + $this->renderString("{{ associative.two upper='true' }}", $this->variables) + ); + + $this->assertEquals( + 'WILDERNESS', + $this->renderString("{{ associative['two'] upper='true' }}", $this->variables) + ); + + $this->assertEquals( + 'WILDERNESS', + $this->renderString("{{ associative[default_key] upper='true' }}", $this->variables) + ); + } + + /** @test */ + public function chained_parameter_string_modifiers() + { + $template = "{{ string upper='true' lower='true' }}"; + + $this->assertEquals('hello wilderness', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function single_standard_array_modifier_tight() + { + $template = '{{ simple|length }}'; + + $this->assertEquals('3', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function single_standard_array_modifier_relaxed() + { + $template = '{{ simple | length }}'; + + $this->assertEquals('3', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function chained_standard_array_modifiers_tight_on_content() + { + $template = '{{ content|markdown|lower }}'; + + $this->assertEquals("

paragraph

\n", $this->renderString($template, $this->variables)); + } + + /** @test */ + public function chained_standard_modifiers_relaxed_on_content() + { + $template = '{{ content | markdown | lower }}'; + + $this->assertEquals("

paragraph

\n", $this->renderString($template, $this->variables)); + } + + /** @test */ + public function chained_parameter_modifiers_on_content() + { + $template = "{{ content markdown='true' lower='true' }}"; + + $this->assertEquals("

paragraph

\n", $this->renderString($template, $this->variables)); + } + + /** @test */ + public function conditions_with_modifiers() + { + $template = "{{ if string|upper == 'HELLO WILDERNESS' }}yes{{ endif }}"; + + $this->assertEquals('yes', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function conditions_with_relaxed_modifiers() + { + $template = "{{ if string | upper == 'HELLO WILDERNESS' }}yes{{ endif }}"; + + $this->assertEquals('yes', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function tags_with_curlies_in_params_gets_parsed() + { + // the variables are inside Test@index + $this->app['statamic.tags']['test'] = \Tests\Fixtures\Addon\Tags\Test::class; + + $template = "{{ test variable='{string}' }}"; + + $this->assertEquals('Hello wilderness', $this->renderString($template, $this->variables, true)); + } + + /** @test */ + public function date_condition_with_chained_relaxed_modifiers_with_spaces_in_arguments() + { + $template = '{{ if (date | modify_date:+3 years | format:Y) == "2015" }}yes{{ endif }}'; + + $this->assertEquals('yes', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function array_modifiers_get_parsed() + { + $template = '{{ simple limit="1" }}{{ value }}{{ /simple }}'; + + $this->assertEquals('one', $this->renderString($template, $this->variables)); + } + + /** @test */ + public function array_modifiers_on_collections_get_parsed() + { + $template = '{{ simple limit="1" }}{{ value }}{{ /simple }}'; + + $this->assertEquals('one', $this->renderString($template, [ + 'simple' => collect(['one', 'two', 'three']), + ])); + } + + /** @test */ + public function recursive_children() + { + // the variables are inside RecursiveChildren@index + $this->app['statamic.tags']['recursive_children'] = RecursiveChildren::class; + + $template = ''; + + $expected = ''; + + $this->assertEquals($expected, $this->renderString($template, ['foo' => 'Bar'], true)); + } + + /** @test */ + public function recursive_children_with_scope() + { + // the variables are inside RecursiveChildren@index + $this->app['statamic.tags']['recursive_children'] = RecursiveChildren::class; + + $template = ''; + + $expected = ''; + + $this->assertEquals($expected, $this->renderString($template, ['foo' => 'Bar'], true)); + } + + /** @test */ + public function empty_values_are_not_overridden_by_previous_iteration() + { + $variables = [ + 'loop' => [ + [ + 'one' => '[1.1]', + 'two' => '[1.2]', + ], + [ + 'one' => '[2.1]', + ], + ], + ]; + + $this->assertEquals( + '[1.1][1.2][2.1]', + $this->renderString('{{ loop }}{{ one }}{{ two }}{{ /loop }}', $variables) + ); + } + + /** @test */ + public function empty_values_are_not_overridden_by_previous_iteration_with_parsing() + { + $this->app['statamic.tags']['test'] = \Tests\Antlers\Fixtures\Addon\Tags\Test::class; + + // Variable name was changed from "loop" to "loopvar" compared to the original test to avoid a + // headache since the base test class is going to be loading all of the core Statamic Tags. + $variables = [ + 'loopvar' => [ + [ + 'one' => '[1.1]', + 'two' => '[1.2]', + ], + [ + 'one' => '[2.1]', + ], + ], + ]; + + $this->assertEquals( + '[1.1][1.2][2.1]', + $this->renderString('{{ loopvar }}{{ one }}{{ test:some_parsing var="two" }}{{ two }}{{ /test:some_parsing }}{{ /loopvar }}', $variables, true) + ); + } + + /** @test */ + public function nested_array_syntax() + { + $variables = [ + 'hello' => [ + 'world' => [ + ['baz' => 'one'], + ['baz' => 'two'], + ], + 'id' => '12345', + ], + ]; + + $this->assertEquals( + '[one][two]', + $this->renderString('{{ hello:world }}[{{ baz }}]{{ /hello:world }}', $variables) + ); + + $this->assertEquals( + '[one][two]', + $this->renderString('{{ hello:world scope="s" }}[{{ s:baz }}]{{ /hello:world }}', $variables) + ); + } + + /** @test */ + public function it_parses_php_when_enabled() + { + $this->assertEquals( + 'Hello wilderness!', + $this->parser($this->variables)->allowPhp()->parse('{{ string }}', $this->variables) + ); + + $this->assertEquals( + 'Hello wilderness<?php echo "!"; ?>', + $this->parser($this->variables)->allowPhp(false)->parse('{{ string }}', $this->variables) + ); + } + + /** @test */ + public function it_doesnt_parse_noparse_tags() + { + $parsed = $this->renderString('{{ noparse }}{{ string }}{{ /noparse }} {{ string }}', $this->variables); + + $this->assertEquals('{{ string }} Hello wilderness', $parsed); + } + + /** @test */ + public function it_doesnt_parse_data_in_noparse_modifiers() + { + $variables = [ + 'string' => 'hello', + 'content' => 'before {{ string }} after', + ]; + + $parsed = $this->renderString('{{ content | noparse }} {{ string }}', $variables); + + $this->assertEquals('before {{ string }} after hello', $parsed); + } + + /** @test */ + public function it_doesnt_parse_data_in_noparse_modifiers_with_null_coalescence() + { + $variables = [ + 'string' => 'hello', + 'content' => 'before {{ string }} after', + ]; + $parsed = $this->renderString('{{ missing or content | noparse }} {{ string }}', $variables); + $this->assertEquals('before {{ string }} after hello', $parsed); + } + + /** @test */ + public function it_doesnt_parse_noparse_tags_inside_callbacks() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function array() + { + return []; + } + + public function loop() + { + return [ + ['string' => 'One'], + ['string' => 'Two'], + ]; + } + })::register(); + + $template = <<<'EOT' +{{ tag:array }}{{ noparse }}{{ string }}{{ /noparse }}{{ /tag:array }} +{{ tag:loop }} + {{ index }} {{ noparse }}{{ string }}{{ /noparse }} {{ string }} +{{ /tag:loop }} +EOT; + + $expected = <<<'EOT' +{{ string }} + + 0 {{ string }} One + + 1 {{ string }} Two + +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables, true)); + } + + /** @test */ + public function it_doesnt_parse_data_in_noparse_modifiers_inside_callbacks() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function array() + { + return [ + 'string' => 'hello', + 'content' => 'beforesingle {{ string }} aftersingle', + ]; + } + + public function loop() + { + return [ + [ + 'string' => 'One', + 'content' => 'beforepair {{ string }} afterpair', + ], + [ + 'string' => 'Two', + 'content' => 'beforepair {{ string }} afterpair', + ], + ]; + } + })::register(); + + $template = <<<'EOT' +{{ tag:array }}{{ content | noparse }}{{ /tag:array }} +{{ tag:loop }} + {{ count }} {{ content | noparse }} {{ string }} +{{ /tag:loop }} +EOT; + + $expected = <<<'EOT' +beforesingle {{ string }} aftersingle + + 1 beforepair {{ string }} afterpair One + + 2 beforepair {{ string }} afterpair Two + +EOT; + + $parsed = $this->renderString($template, [], true); + $this->assertEqualsWithCollapsedNewlines($expected, $parsed); + } + + /** @test */ + public function it_doesnt_parse_tags_prefixed_with_an_at_symbol() + { + $this->assertEquals('foo {{ bar }} baz', $this->renderString('foo @{{ bar }} baz')); + } + + /** @test */ + public function it_doesnt_parse_tags_prefixed_with_an_at_symbol_over_multiple_lines() + { + $template = <<<'EOT' +@{{ foo }} +bar +{{ baz }} +EOT; + + $expected = <<<'EOT' +{{ foo }} +bar +BAZ +EOT; + + $this->assertEquals($expected, $this->renderString($template, ['baz' => 'BAZ'])); + } + + /** @test */ + public function it_doesnt_parse_tags_prefixed_with_an_at_symbol_over_tags_in_multiple_lines() + { + $template = <<<'EOT' +@{{ foo }} {{ qux }} +bar +{{ baz }} +EOT; + + $expected = <<<'EOT' +{{ foo }} QUX +bar +BAZ +EOT; + + $this->assertEquals($expected, $this->renderString($template, ['baz' => 'BAZ', 'qux' => 'QUX'])); + } + + /** @test */ + public function it_doesnt_parse_multiline_tags_prefixed_with_an_at_symbol_over_tags_in_multiple_lines() + { + $template = <<<'EOT' +@{{ foo + bar:baz="qux" +}} {{ qux }} +bar +{{ baz }} +EOT; + + $expected = <<<'EOT' +{{ foo + bar:baz="qux" +}} QUX +bar +BAZ +EOT; + + $this->assertEquals($expected, $this->renderString($template, ['baz' => 'BAZ', 'qux' => 'QUX'])); + } + + /** @test */ + public function it_doesnt_parse_tags_prefixed_with_an_at_symbol_containing_nested_tags() + { + $this->assertEquals('{{ foo bar="{baz}" }}', $this->renderString('@{{ foo bar="{baz}" }}')); + } + + /** @test */ + public function it_accepts_an_arrayable_object() + { + $this->assertEquals( + 'Hello World', + $this->renderString('{{ string }}', new ArrayableObject(['string' => 'Hello World'])) + ); + } + + /** @test */ + public function it_throws_exception_for_non_arrayable_data_object() + { + try { + $this->renderString('{{ string }}', new NonArrayableObject(['string' => 'Hello World'])); + } catch (\InvalidArgumentException $e) { + $this->assertEquals('Expecting array or object implementing Arrayable. Encountered [Tests\Antlers\Runtime\NonArrayableObject]', $e->getMessage()); + + return; + } + + $this->fail('Exception was not thrown.'); + } + + /** @test */ + public function it_throws_exception_for_unsupported_data_value() + { + try { + $this->renderString('{{ string }}', 'string'); + } catch (\InvalidArgumentException $e) { + $this->assertEquals('Expecting array or object implementing Arrayable. Encountered [string]', $e->getMessage()); + + return; + } + + $this->fail('Exception was not thrown.'); + } + + /** @test */ + public function it_gets_augmented_value() + { + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + return 'augmented '.$value; + } + }; + + $value = new Value('expected', 'test', $fieldtype); + + $parsed = $this->renderString('{{ test }}', ['test' => $value]); + + $this->assertEquals('augmented expected', $parsed); + } + + /** @test */ + public function it_expands_augmented_value_when_used_as_an_array() + { + $fieldtype = new class extends Fieldtype + { + public function augment($values) + { + return collect($values)->map(function ($value) { + return strtoupper($value); + })->all(); + } + }; + + $value = new Value([ + 'one' => 'hello', + 'two' => 'world', + ], 'test', $fieldtype); + + $parsed = $this->renderString('{{ test }}{{ one }} {{ two }}{{ /test }}', ['test' => $value]); + + $this->assertEquals('HELLO WORLD', $parsed); + } + + /** @test */ + public function it_gets_nested_values_from_augmentable_objects() + { + $value = new AugmentableObject(['foo' => 'bar']); + + $parsed = $this->renderString('{{ test:foo }}', ['test' => $value]); + + $this->assertEquals('bar', $parsed); + } + + /** @test */ + public function it_loops_over_value_object() + { + $fieldtype = new class extends Fieldtype + { + public function augment($values) + { + return collect($values)->map(function ($value) { + return collect($value)->map(function ($v) { + return strtoupper($v); + }); + })->toArray(); + } + }; + + $value = new Value([ + ['one' => 'uno', 'two' => 'dos'], + ['one' => 'une', 'two' => 'deux'], + ], 'test', $fieldtype); + + $parsed = $this->renderString('{{ test }}{{ one }} {{ two }} {{ /test }}', ['test' => $value]); + + $this->assertEquals('UNO DOS UNE DEUX ', $parsed); + } + + /** @test */ + public function it_gets_nested_values_from_value_objects() + { + $value = new Value(['foo' => 'bar'], 'test'); + + $parsed = $this->renderString('{{ test:foo }}', ['test' => $value]); + + $this->assertEquals('bar', $parsed); + } + + /** @test */ + public function it_gets_nested_values_from_nested_value_objects() + { + $value = new Value(['foo' => 'bar'], 'test'); + + $parsed = $this->renderString('{{ nested:test:foo }}', [ + 'nested' => [ + 'test' => $value, + ], + ]); + + $this->assertEquals('bar', $parsed); + } + + /** @test */ + public function it_gets_nested_values_from_within_nested_value_objects() + { + $value = new Value([ + 'foo' => ['nested' => 'bar'], + ], 'test'); + + $parsed = $this->renderString('{{ nested:test:foo:nested }}', [ + 'nested' => [ + 'test' => $value, + ], + ]); + + $this->assertEquals('bar', $parsed); + } + + /** @test */ + public function it_parses_value_objects_values_when_configured_to_do_so() + { + $fieldtypeOne = new class extends Fieldtype + { + public function augment($value) + { + return 'augmented '.$value; + } + + public function config(?string $key = null, $fallback = null) + { + return true; + } + + // fake what's being returned from the field config + }; + $fieldtypeTwo = new class extends Fieldtype + { + public function augment($value) + { + return 'augmented '.$value; + } + + public function config(?string $key = null, $fallback = null) + { + return false; + } + + // fake what's being returned from the field config + }; + + $parseable = new Value('before {{ string }} after', 'parseable', $fieldtypeOne); + $nonParseable = new Value('before {{ string }} after', 'non_parseable', $fieldtypeTwo); + + $template = <<<'EOT' +{{ parseable }} +{{ non_parseable }} +EOT; + + $expected = <<<'EOT' +augmented before hello after +augmented before {{ string }} after +EOT; + + $variables = [ + 'parseable' => $parseable, + 'non_parseable' => $nonParseable, + 'string' => 'hello', + ]; + + $this->assertEquals($expected, (string) $this->renderString($template, $variables)); + + $this->assertEquals( + 'shmaugmented before hello after', + (string) $this->renderString('{{ parseable | replace:aug:shmaug }}', $variables) + ); + + $this->assertEquals( + 'shmaugmented before {{ string }} after', + (string) $this->renderString('{{ non_parseable | replace:aug:shmaug }}', $variables) + ); + + $this->assertEquals( + 'shmaugmented before hello after', + (string) $this->renderString('{{ parseable replace="aug|shmaug" }}', $variables) + ); + + $this->assertEquals( + 'shmaugmented before {{ string }} after', + (string) $this->renderString('{{ non_parseable replace="aug|shmaug" }}', $variables) + ); + } + + /** @test */ + public function it_casts_objects_to_string_when_using_single_tags() + { + $object = new class + { + public function __toString() + { + return 'string'; + } + }; + + $this->assertEquals( + 'string', + $this->renderString('{{ object }}', compact('object')) + ); + } + + /** @test */ + public function it_doesnt_output_anything_if_object_cannot_be_cast_to_a_string() + { + Log::shouldReceive('debug')->once() + ->with('Cannot render an object variable as a string: {{ object }}', [ + 'line' => 1, 'file' => '', + ]); + + $object = new class + { + }; + + $this->assertEquals('', $this->renderString('{{ object }}', compact('object'))); + } + + /** @test */ + public function it_casts_arrayable_objects_to_arrays_when_using_tag_pairs() + { + $arrayableObject = new ArrayableObject([ + 'one' => 'foo', + 'two' => 'bar', + ]); + + $this->assertEquals( + 'foo bar', + $this->renderString('{{ object }}{{ one }} {{ two }}{{ /object }}', [ + 'object' => $arrayableObject, + ]) + ); + } + + /** @test */ + public function it_cannot_cast_non_arrayable_objects_to_arrays_when_using_tag_pairs() + { + Log::shouldReceive('debug')->once() + ->with('Cannot loop over non-loopable variable: {{ object }}', [ + 'line' => 1, 'file' => '', + ]); + + $nonArrayableObject = new NonArrayableObject([ + 'one' => 'foo', + 'two' => 'bar', + ]); + + $this->assertEquals( + '', + $this->renderString('{{ object }}{{ one }} {{ two }}{{ /object }}', [ + 'object' => $nonArrayableObject, + ]) + ); + } + + /** @test */ + public function callback_tags_that_return_unparsed_simple_arrays_get_parsed() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + return ['one' => 'a', 'two' => 'b']; + } + })::register(); + + $template = <<<'EOT' +{{ tag }} + {{ one }} {{ two }} +{{ /tag }} +EOT; + + $expected = <<<'EOT' + + a b + +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, [], true)); + } + + /** @test */ + public function callback_tags_that_return_unparsed_simple_arrays_get_parsed_with_scope() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + return ['one' => 'a', 'two' => 'b']; + } + })::register(); + + $template = <<<'EOT' +{{ tag scope="foo" }} + {{ foo:one }} {{ foo:two }} +{{ /tag }} +EOT; + + $expected = <<<'EOT' + + a b + +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, [], true)); + } + + /** @test */ + public function callback_tags_that_return_unparsed_multidimensional_arrays_get_parsed() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + return [ + ['one' => 'a', 'two' => 'b'], + ['one' => 'c', 'two' => 'd'], + ]; + } + })::register(); + + $template = <<<'EOT' +{{ string }} +{{ tag }} + {{ count }} {{ if first }}first{{ else }}not-first{{ /if }} {{ if last }}last{{ else }}not-last{{ /if }} {{ one }} {{ two }} {{ string }} +{{ /tag }} +EOT; + + $expected = <<<'EOT' +Hello wilderness + + 1 first not-last a b Hello wilderness + + 2 not-first last c d Hello wilderness + +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, ['string' => 'Hello wilderness'], true)); + } + + /** @test */ + public function callback_tags_that_return_empty_arrays_get_parsed_with_no_results() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + return []; + } + })::register(); + + $template = <<<'EOT' +{{ tag }} + {{ if no_results }}no results{{ else }}there are results{{ /if }} +{{ /tag }} +EOT; + + $expected = <<<'EOT' + + no results + +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables, true)); + } + + /** @test */ + public function callback_tags_that_return_collections_get_parsed() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + return collect([ + ['one' => 'a', 'two' => 'b'], + ['one' => 'c', 'two' => 'd'], + ]); + } + })::register(); + + $template = <<<'EOT' +{{ string }} +{{ tag }} + {{ count }} {{ if first }}first{{ else }}not-first{{ /if }} {{ if last }}last{{ else }}not-last{{ /if }} {{ one }} {{ two }} {{ string }} +{{ /tag }} +EOT; + + $expected = <<<'EOT' +Hello wilderness + + 1 first not-last a b Hello wilderness + + 2 not-first last c d Hello wilderness + +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables, true)); + } + + /** @test */ + public function callback_tags_that_return_query_builders_get_parsed() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->andReturn(collect([ + ['one' => 'a', 'two' => 'b'], + ['one' => 'c', 'two' => 'd'], + ])); + + return $builder; + } + })::register(); + + $template = <<<'EOT' +{{ string }} +{{ tag }} + {{ count }} {{ if first }}first{{ else }}not-first{{ /if }} {{ if last }}last{{ else }}not-last{{ /if }} {{ one }} {{ two }} {{ string }} +{{ /tag }} +EOT; + + $expected = <<<'EOT' +Hello wilderness + + 1 first not-last a b Hello wilderness + + 2 not-first last c d Hello wilderness + +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables, true)); + } + + /** @test */ + public function callback_tags_that_return_value_objects_gets_parsed() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + return 'augmented '.$value; + } + }; + + return new Value('the value', null, $fieldtype); + } + })::register(); + + $this->assertEquals('augmented the value', $this->renderString('{{ tag }}', [], true)); + } + + /** @test */ + public function callback_tags_that_return_value_objects_with_antlers_gets_parsed() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + return 'augmented '.$value; + } + }; + + $fieldtype->setField(new Field('test', ['antlers' => true])); + + return new Value('the value with {{ var }} in it', null, $fieldtype); + } + })::register(); + + $this->assertEquals( + 'augmented the value with howdy in it', + (string) $this->renderString('{{ tag }}', ['var' => 'howdy'], true) + ); + } + + /** @test */ + public function callback_tags_that_return_value_objects_with_antlers_disabled_does_not_get_parsed() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + return 'augmented '.$value; + } + }; + + $fieldtype->setField(new Field('test', ['antlers' => false])); + + return new Value('the value with {{ var }} in it', null, $fieldtype); + } + })::register(); + + $this->assertEquals( + 'augmented the value with {{ var }} in it', + (string) $this->renderString('{{ tag }}', ['var' => 'howdy'], true) + ); + } + + /** @test */ + public function value_objects_with_antlers_gets_parsed() + { + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + return 'augmented '.$value; + } + }; + + $fieldtype->setField(new Field('test', ['antlers' => true])); + + $value = new Value('the value with {{ var }} in it', null, $fieldtype); + + $this->assertEquals( + 'augmented the value with howdy in it', + (string) $this->renderString('{{ test }}', [ + 'test' => $value, + 'var' => 'howdy', + ], true) + ); + } + + /** @test */ + public function value_objects_with_antlers_disabled_do_not_get_parsed() + { + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + return 'augmented '.$value; + } + }; + + $fieldtype->setField(new Field('test', ['antlers' => false])); + + $value = new Value('the value with {{ var }} in it', null, $fieldtype); + + $this->assertEquals( + 'augmented the value with {{ var }} in it', + (string) $this->renderString('{{ test }}', [ + 'test' => $value, + 'var' => 'howdy', + ]) + ); + } + + /** @test */ + public function it_automatically_augments_augmentable_objects_when_using_tag_pairs() + { + $augmentable = new AugmentableObject([ + 'one' => 'foo', + 'two' => 'bar', + ]); + + $this->assertEquals( + 'FOO! bar', + $this->renderString('{{ object }}{{ one }} {{ two }}{{ /object }}', [ + 'object' => $augmentable, + ]) + ); + } + + /** @test */ + public function it_automatically_augments_augmentable_objects_when_returned_from_a_callback_tag() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + return new AugmentableObject([ + 'one' => 'foo', + 'two' => 'bar', + ]); + } + })::register(); + + $this->assertEquals( + 'FOO! bar', + $this->renderString('{{ tag }}{{ one }} {{ two }}{{ /tag }}', [], true) + ); + } + + /** @test */ + public function it_automatically_augments_collections_when_using_tag_pairs() + { + $augmentable = collect([ + new AugmentableObject(['one' => 'foo', 'two' => 'bar']), + new AugmentableObject(['one' => 'baz', 'two' => 'qux']), + ]); + + $this->assertEquals( + 'FOO! bar BAZ! qux ', + $this->renderString('{{ object }}{{ one }} {{ two }} {{ /object }}', [ + 'object' => $augmentable, + ]) + ); + } + + /** @test */ + public function callback_tag_pair_variables_get_context_merged_in_but_nulls_remain_null() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + return [ + 'drink' => 'juice', + 'activity' => null, + ]; + } + })::register(); + + $context = [ + 'drink' => 'whisky', + 'food' => 'burger', + 'activity' => 'singing', + ]; + + $template = <<<'EOT' +{{ drink }} {{ food }} {{ activity }} +{{ tag }}{{ drink }} {{ food }} -{{ activity }}-{{ /tag }} +EOT; + + $expected = <<<'EOT' +whisky burger singing +juice burger -- +EOT; + $this->assertEquals($expected, $this->renderString($template, $context, true)); + } + + /** @test */ + public function variable_tag_pair_get_context_merged_in_except_for_nulls() + { + $context = [ + 'drink' => 'whisky', + 'food' => 'burger', + 'activity' => 'singing', + 'array' => [ + 'drink' => 'juice', + 'activity' => null, + ], + ]; + + $template = <<<'EOT' +{{ drink }} {{ food }} {{ activity }} +{{ array }}{{ drink }} {{ food }} -{{ activity }}-{{ /array }} +EOT; + + $expected = <<<'EOT' +whisky burger singing +juice burger -- +EOT; + $this->assertEquals($expected, $this->renderString($template, $context)); + } + + /** @test */ + public function scope_modifier_can_add_scopes() + { + $context = [ + 'drink' => 'whisky', + 'food' => 'burger', + 'array' => [ + ['drink' => 'juice'], + ['drink' => 'smoothie'], + ], + ]; + + $template = <<<'EOT' +{{ food }} {{ drink }} +{{ array scope="s" }} +-{{ s:food }}- {{ s:drink }} {{ food }} {{ drink }} +{{ /array }} +EOT; + + $expected = <<<'EOT' +burger whisky + +-- juice burger juice + +-- smoothie burger smoothie + +EOT; + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $context, true)); + } + + /** @test */ + public function it_can_reach_into_the_cascade() + { + $cascade = $this->mock(Cascade::class, function ($m) { + $m->shouldReceive('get')->with('page')->andReturn(['drink' => 'juice']); + $m->shouldReceive('get')->with('global')->andReturn(['drink' => 'water']); + $m->shouldReceive('get')->with('menu')->andReturn(['drink' => 'vodka']); + $m->shouldNotReceive('get')->with('nested'); + $m->shouldNotReceive('get')->with('augmented'); + }); + + $fieldtype = new class extends Fieldtype + { + }; + $augmented = new Value(['drink' => 'la croix'], 'augmented', $fieldtype); + + $context = [ + 'drink' => 'whisky', + 'augmented' => $augmented, + 'nested' => [ + 'drink' => 'coke', + 'augmented' => $augmented, + ], + ]; + + $template = <<<'EOT' +var: {{ drink }} +page: {{ page:drink }} +global: {{ global:drink }} +menu: {{ menu:drink }} +nested: {{ nested:drink }} +augmented: {{ augmented:drink }} +nested augmented: {{ nested:augmented:drink }} +EOT; + + $expected = <<<'EOT' +var: whisky +page: juice +global: water +menu: vodka +nested: coke +augmented: la croix +nested augmented: la croix +EOT; + + $results = (string) $this->parser()->cascade($cascade)->parse($template, $context); + $this->assertEquals($expected, $results); + } + + /** @test */ + public function it_can_create_scopes() + { + $context = [ + 'drink' => 'whisky', + 'food' => 'burger', + 'activity' => 'singing', + 'array' => [ + 'drink' => 'juice', + 'activity' => null, + ], + ]; + + $template = <<<'EOT' +{{ scope:test }} +drink: {{ drink }} +food: {{ food }} +activity: {{ activity }} + +{{ array }} + array:drink: {{ drink }} + array:food: {{ food }} + array:activity: -{{ activity }}- + array:test:drink: {{ test:drink }} + array:test:food: {{ test:food }} + array:test:activity: {{ test:activity }} +{{ /array }} +{{ /scope:test }} +EOT; + + $expected = <<<'EOT' +drink: whisky +food: burger +activity: singing + + + array:drink: juice + array:food: burger + array:activity: -- + array:test:drink: whisky + array:test:food: burger + array:test:activity: singing +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, trim($this->renderString($template, $context, true))); + } + + /** @test */ + public function it_does_not_accept_sequences() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expecting an associative array'); + $this->renderString('', ['foo', 'bar']); + } + + /** @test */ + public function it_does_not_accept_multidimensional_array() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expecting an associative array'); + $this->renderString('', [ + ['foo' => 'bar'], + ['foo' => 'baz'], + ]); + } + + /** @test */ + public function it_aliases_array_tag_pairs_using_the_as_modifier() + { + $template = <<<'EOT' +{{ array as="stuff" }} +before +{{ stuff }} +{{ foo }} +{{ /stuff }} +after +{{ /array }} +EOT; + + $expected = <<<'EOT' + +before + +bar + +baz + +qux + +after + +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, [ + 'array' => [ + ['foo' => 'bar'], + ['foo' => 'baz'], + ['foo' => 'qux'], + ], + ], true)); + } + + /** @test */ + public function it_aliases_callback_tag_pair_loop_using_the_as_param() + { + (new class extends Tags + { + public static $handle = 'tag'; + + public function index() + { + return [ + ['foo' => 'bar'], + ['foo' => 'baz'], + ['foo' => 'qux'], + ]; + } + })::register(); + + $template = <<<'EOT' +{{ tag as="stuff" }} +before +{{ stuff }} +{{ foo }} +{{ /stuff }} +after +{{ /tag }} +EOT; + + $expected = <<<'EOT' + +before + +bar + +baz + +qux + +after + +EOT; + + $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, [], true)); + } + + /** @test */ + public function it_counts_query_builder_results_in_conditions() + { + (new EntryFactory())->collection('blog')->create(); + + $template = '{{ if entries }}yup{{ else }}nope{{ /if }}'; + + $this->assertEquals('yup', $this->renderString($template, ['entries' => Entry::query()])); + $this->assertEquals('yup', $this->renderString($template, ['entries' => Entry::query()->where('collection', 'blog')])); + $this->assertEquals('nope', $this->renderString($template, ['entries' => Entry::query()->where('collection', 'dunno')])); + } + + /** @test */ + public function it_applies_modifier_on_different_array_syntax() + { + $vars = [ + 'key' => 'entries', + 'source' => [ + 'entries' => [ + ['id' => 0], + ['id' => 1], + ['id' => 2], + ['id' => 3], + ], + ], + ]; + + $this->assertEquals( + '[0][1][2][3]', + $this->renderString('{{ source.entries }}[{{ id }}]{{ /source.entries }}', $vars) + ); + + $this->assertEquals( + '[0][1][2][3]', + $this->renderString('{{ source[key] }}[{{ id }}]{{ /source[key] }}', $vars) + ); + + $this->assertEquals( + '[0][1][2][3]', + $this->renderString('{{ source.entries sort="id" }}[{{ id }}]{{ /source.entries }}', $vars) + ); + + $this->assertEquals( + '[0][1][2][3]', + $this->renderString('{{ source[key] sort="id" }}[{{ id }}]{{ /source[key] }}', $vars) + ); + + $this->assertEquals( + '[3][2][1][0]', + $this->renderString('{{ source[key] sort="id|desc" }}[{{ id }}]{{ /source[key] }}', $vars) + ); + } + + /** @test */ + public function modifiers_on_tag_pairs_receive_the_augmented_value() + { + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + $value[1]['type'] = 'yup'; + + return $value; + } + }; + + $value = new Value([ + ['type' => 'yup', 'text' => '1'], + ['type' => 'nope', 'text' => '2'], + ['type' => 'yup', 'text' => '3'], + ], 'test', $fieldtype); + + // unaugmented, the second item would be filtered out. + // augmenting changes the second item to a yup, so it should be included. + $this->assertEquals('123', $this->renderString('{{ test where="type:yup" }}{{ text }}{{ /test }}', [ + 'test' => $value, + 'hello' => 'there', + ])); + } + + /** @test */ + public function it_outputs_the_value_when_a_ArrayableString_object_is_used_as_string() + { + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + return new ArrayableString('world', ['label' => 'World']); + } + }; + + $value = new Value('world', 'hello', $fieldtype); + + $this->assertEquals('world', $this->renderString('{{ hello }}', [ + 'hello' => $value, + ])); + } + + /** @test */ + public function it_can_treat_a_ArrayableString_object_as_an_array() + { + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + return new ArrayableString('world', ['label' => 'World']); + } + }; + + $value = new Value('world', 'hello', $fieldtype); + + $this->assertEquals( + 'world, World', + $this->renderString('{{ hello }}{{ value }}, {{ label }}{{ /hello }}', [ + 'hello' => $value, + ]) + ); + } + /** @test */ - public function non_arrays_cannot_be_looped() + public function it_can_access_ArrayableString_properties_by_colon_notation() { - Log::shouldReceive('debug')->once() - ->with('Cannot loop over non-loopable variable: {{ string }}', [ - 'line' => 1, 'file' => '', - ]); + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + return new ArrayableString('world', ['label' => 'World']); + } + }; - $template = '{{ string }} {{ /string }}'; + $value = new Value('world', 'hello', $fieldtype); - $this->assertEquals('', $this->renderString($template, $this->variables)); + $vars = ['hello' => $value]; + + $this->assertEquals('world', $this->renderString('{{ hello:value }}', $vars)); + $this->assertEquals('World', $this->renderString('{{ hello:label }}', $vars)); } /** @test */ - public function unclosed_array_variable_pairs_should_be_null() + public function it_can_use_ArrayableString_objects_in_conditions() { - Log::shouldReceive('debug')->once() - ->with('Cannot render an array variable as a string: {{ simple }}', [ - 'line' => 1, 'file' => '', - ]); + $fieldtype = new class extends Fieldtype + { + public function augment($value) + { + $label = is_null($value) ? null : strtoupper($value); - $template = '{{ simple }}'; + return new ArrayableString($value, ['label' => $label]); + } + }; - $this->assertEquals('', $this->renderString($template, $this->variables)); + $vars = [ + 'string' => new Value('foo', 'string', $fieldtype), + 'nully' => new Value(null, 'nully', $fieldtype), + ]; + + $this->assertEquals('true', $this->renderString('{{ if string }}true{{ else }}false{{ /if }}', $vars)); + $this->assertEquals('false', $this->renderString('{{ if nully }}true{{ else }}false{{ /if }}', $vars)); + + $this->assertEquals('true', $this->renderString('{{ if string == "foo" }}true{{ else }}false{{ /if }}', $vars)); + $this->assertEquals('false', $this->renderString('{{ if nully == "foo" }}true{{ else }}false{{ /if }}', $vars)); + $this->assertEquals('false', $this->renderString('{{ if string == "bar" }}true{{ else }}false{{ /if }}', $vars)); + $this->assertEquals('false', $this->renderString('{{ if nully == "bar" }}true{{ else }}false{{ /if }}', $vars)); + + $this->assertEquals('true', $this->renderString('{{ string ? "true" : "false" }}', $vars)); + $this->assertEquals('false', $this->renderString('{{ nully ? "true" : "false" }}', $vars)); + + $this->assertEquals('true', $this->renderString('{{ string == "foo" ? "true" : "false" }}', $vars)); + $this->assertEquals('false', $this->renderString('{{ string == "bar" ? "true" : "false" }}', $vars)); + + $this->assertEquals('foo', $this->renderString('{{ string or "fallback" }}', $vars)); + $this->assertEquals('FOO', $this->renderString('{{ string:label or "fallback" }}', $vars)); + $this->assertEquals('fallback', $this->renderString('{{ nully or "fallback" }}', $vars)); + $this->assertEquals('fallback', $this->renderString('{{ nully:label or "fallback" }}', $vars)); + + $this->assertEquals('foo', $this->renderString('{{ string ?? "fallback" }}', $vars)); + $this->assertEquals('FOO', $this->renderString('{{ string:label ?? "fallback" }}', $vars)); + $this->assertEquals('fallback', $this->renderString('{{ nully ?? "fallback" }}', $vars)); + $this->assertEquals('fallback', $this->renderString('{{ nully:label ?? "fallback" }}', $vars)); + + $this->assertEquals('fallback', $this->renderString('{{ string ?= "fallback" }}', $vars)); + $this->assertEquals('fallback', $this->renderString('{{ string:label ?= "fallback" }}', $vars)); + $this->assertEquals('', $this->renderString('{{ nully ?= "fallback" }}', $vars)); + $this->assertEquals('', $this->renderString('{{ nully:label ?= "fallback" }}', $vars)); + } + + /** @test */ + public function it_can_remove_escaping_characters_from_tenary_output() + { + $vars = [ + 'seo_title' => "Let's work together", + 'title' => 'Contact', + + 'local_office_link' => '', + 'head_office_link' => 'https://statamic.com', + ]; + + $this->assertEquals("Let's work together", $this->renderString('{{ seo_title ? seo_title : title }}', $vars)); + $this->assertEquals('Contact', $this->renderString('{{ title ? title : seo_title }}', $vars)); + + $this->assertEquals('https://statamic.com', $this->renderString('{{ local_office_link ? local_office_link : head_office_link }}', $vars)); + $this->assertEquals('https://statamic.com', $this->renderString('{{ head_office_link ? head_office_link : local_office_link }}', $vars)); + } + + /** @test */ + public function it_can_remove_escaping_characters_from_tenary_output_with_truth_coalescence() + { + $vars = [ + 'truthy' => true, + 'string' => "Let's work together", + 'link' => 'https://statamic.com', + ]; + + $this->assertEquals("Let's work together", $this->renderString('{{ truthy ?= string }}', $vars)); + $this->assertEquals('https://statamic.com', $this->renderString('{{ truthy ?= link }}', $vars)); + } + + /** @test */ + public function empty_collections_are_considered_empty_in_conditions() + { + $template = '{{ if stuff }}yes{{ else }}no{{ /if }}'; + $this->assertEquals('no', $this->renderString($template, ['stuff' => collect()])); + $this->assertEquals('yes', $this->renderString($template, ['stuff' => collect(['one'])])); + } + + /** @test */ + public function empty_view_error_bags_are_considered_empty_in_conditions() + { + $template = '{{ if errors}}yes{{ else }}no{{ /if }}'; + $viewErrorBag = new ViewErrorBag; + + $this->assertEquals('no', $this->renderString($template, ['errors' => $viewErrorBag])); + $this->assertEquals('yes', $this->renderString($template, ['errors' => $viewErrorBag->put('default', new MessageBag)])); + $this->assertEquals('yes', $this->renderString($template, ['errors' => $viewErrorBag->put('form.contact', new MessageBag)])); + } + + /** @test */ + public function objects_are_considered_truthy() + { + $this->assertEquals('yes', $this->renderString('{{ if object }}yes{{ else }}no{{ /if }}', [ + 'object' => new \stdClass(), + ])); + } + + /** @test */ + public function parameter_style_modifier_with_colon_prefix_will_get_the_values_from_context() + { + $this->assertEquals('Tes Te', $this->renderString('{{ word :backspace="one" }} {{ word :backspace="two" }}', [ + 'word' => 'Test', + 'one' => 1, + 'two' => 2, + ])); + } + + /** @test */ + public function variables_starting_with_if_arent_treated_as_if_statements() + { + $this->assertEquals('test', $this->renderString('{{ iframe }}', ['iframe' => 'test'])); + $this->assertEquals('test', $this->renderString('{{ unlesses }}', ['unlesses' => 'test'])); + $this->assertEquals('test', $this->renderString('{{ elseifs }}', ['elseifs' => 'test'])); + $this->assertEquals('test', $this->renderString('{{ elseunlessses }}', ['elseunlessses' => 'test'])); + } + + /** @test */ + public function when_a_loop_is_a_value_object_with_an_empty_array_it_get_parsed_as_one() + { + $template = <<<'EOT' +before +{{ simple }} + {{ foo }} +{{ /simple }} +after +EOT; + + $expected = <<<'EOT' +before + +after +EOT; + $this->assertEquals($expected, $this->renderString($template, [ + 'simple' => new Value([], null, new class extends \Statamic\Fieldtypes\Replicator + { + }), + ])); + } + + /** @test */ + public function it_automatically_augments_augmentable_objects_when_looping_with_modifier() + { + $loop = [ + new AugmentableObject(['one' => 'foo', 'two' => 'bar']), + new AugmentableObject(['one' => 'baz', 'two' => 'qux']), + ]; + + $this->assertEquals( + '', + (string) $this->renderString('{{ augmentables limit="1" }}<{{ one }}><{{ two }}>{{ /augmentables }}', ['augmentables' => $loop]) + ); + } + + /** @test */ + public function it_uses_tags_with_single_part_in_conditions() + { + (new class extends Tags + { + public static $handle = 'truthy'; + + public function index() + { + return true; + } + })::register(); + + (new class extends Tags + { + public static $handle = 'falsey'; + + public function index() + { + return false; + } + })::register(); + + $this->assertEquals('yes', $this->renderString('{{ if {truthy} }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('yes', $this->renderString('{{ if {truthy} == true }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('no', $this->renderString('{{ if {truthy} == false }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('no', $this->renderString('{{ if {falsey} }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('no', $this->renderString('{{ if {falsey} == true }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('yes', $this->renderString('{{ if {falsey} == false }}yes{{ else }}no{{ /if }}', [], true)); + } + + /** @test */ + public function it_uses_tags_with_multiple_parts_in_conditions() + { + (new class extends Tags + { + public static $handle = 'truthy'; + + public function test() + { + return true; + } + })::register(); + + (new class extends Tags + { + public static $handle = 'falsey'; + + public function test() + { + return false; + } + })::register(); + + $this->assertEquals('yes', $this->renderString('{{ if {truthy:test} }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('yes', $this->renderString('{{ if {truthy:test} == true }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('no', $this->renderString('{{ if {truthy:test} == false }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('no', $this->renderString('{{ if {falsey:test} }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('no', $this->renderString('{{ if {falsey:test} == true }}yes{{ else }}no{{ /if }}', [], true)); + $this->assertEquals('yes', $this->renderString('{{ if {falsey:test} == false }}yes{{ else }}no{{ /if }}', [], true)); + } + + /** @test */ + public function it_does_stuff_in_issue_2537() + { + $template = '{{ if noindex || segment_1 == "mobile" || get:page > 0 }}yes{{ else }}no{{ /if }}'; + + $this->assertEquals('yes', $this->renderString($template, ['noindex' => true])); + } + + /** @test */ + public function it_does_stuff_in_issue_2456() + { + $template = '{{ if publication_venue:publication_venue_types:slug !== "journal" and publication_venue:first_year }}yes{{ else }}no{{ /if }}'; + + $this->assertEquals('yes', $this->renderString($template, [ + 'publication_venue' => [ + 'first_year' => true, + 'publication_venue_types' => [ + 'slug' => 'notjournal', + ], + ], + ])); + } + + /** @test */ + public function it_compares_to_a_string_that_looks_like_array_access() + { + $template = '{{ if test == "price:desc" }}yes{{ else }}no{{ /if }}'; + + $this->assertEquals('yes', $this->renderString($template, [ + 'test' => 'price:desc', + ])); + } + + /** + * @test + * + * @see https://github.com/statamic/cms/issues/3374 + **/ + /** @test */ + public function it_parses_single_and_tag_pairs_with_modifiers() + { + $data = ['items' => ['one', 'two', 'three']]; + + $this->assertEquals('3', $this->renderString('{{ items limit="2" }}<{{ value }}>{{ /items }}{{ items | count }}', $data)); + $this->assertEquals('3', $this->renderString('{{ items | count }}{{ items limit="2" }}<{{ value }}>{{ /items }}', $data)); + } + + /** @test */ + public function it_passes_along_query_builder_values_to_the_query_tag() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->once()->andReturn(collect([ + ['title' => 'Foo'], + ['title' => 'Bar'], + ])); + + $this->assertEquals('', $this->renderString('{{ my_query }}<{{ title }}>{{ /my_query }}', [ + 'my_query' => $builder, + ])); + } + + /** @test */ + public function it_passes_along_query_builder_augmented_values_to_the_query_tag() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->once()->andReturn(collect([ + ['title' => 'Foo'], + ['title' => 'Bar'], + ])); + + $this->assertEquals('', $this->renderString('{{ my_query }}<{{ title }}>{{ /my_query }}', [ + 'my_query' => new Value($builder), + ])); + } + + /** @test */ + public function it_can_reach_into_query_builders() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->times(2)->andReturn(collect([ + ['title' => 'Foo'], + ['title' => 'Bar'], + ])); + + $this->assertEquals('', $this->renderString('<{{ my_query:1:title }}><{{ my_query:0:title }}>', [ + 'my_query' => $builder, + ])); + } + + /** @test */ + public function it_can_reach_into_query_builders_through_values() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->times(2)->andReturn(collect([ + ['title' => 'Foo'], + ['title' => 'Bar'], + ])); + + $this->assertEquals('', $this->renderString('<{{ my_query:1:title }}><{{ my_query:0:title }}>', [ + 'my_query' => new Value($builder), + ])); + } + + /** @test */ + public function it_can_get_nested_query_builders() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->andReturn(collect([ + ['title' => 'Foo'], + ['title' => 'Bar'], + ])); + + $this->assertEquals('', $this->renderString('{{ nested:my_query }}<{{ title }}>{{ /nested:my_query }}', [ + 'nested' => [ + 'my_query' => $builder, + ], + ])); + } + + /** @test */ + public function it_loops_over_values_instances() + { + $this->assertEquals('', $this->renderString('{{ grid }}<{{ foo }}><{{ bar }}>{{ /grid }}', [ + 'grid' => [ + new Values(['foo' => 'alfa', 'bar' => 'bravo']), + new Values(['foo' => 'charlie', 'bar' => 'delta']), + ], + ])); + } + + /** + * @test + * + * @dataProvider objectInConditionProvider + */ + public function it_uses_entries_as_conditions($object) + { + $this->assertEquals('yes', $this->renderString('{{ if the_field }}yes{{ else }}no{{ /if }}', [ + 'the_field' => $object, + ])); + } + + public function objectInConditionProvider() + { + return [ + 'with __toString' => [new class() + { + public function __toString() + { + return 'foo'; + } + }, ], + 'with __call' => [new class() + { + public function __call($method, $args) + { + return 'foo'; + } + }, ], + 'without __call or __toString' => [new class() + { + // + }, ], + ]; + } + + private function assertEqualsWithCollapsedNewlines($expected, $actual) + { + $expected = trim($expected); + $actual = trim($actual); + + // replace all newlines with a single newline + $expected = preg_replace('/\n+/', "\n", $expected); + $actual = preg_replace('/\n+/', "\n", $actual); + + $this->assertEquals($expected, $actual); } public function test_rendering_a_non_array_variable_reports_current_file() @@ -72,42 +2513,6 @@ public function test_rendering_a_non_array_variable_reports_current_file() $this->get('/home')->assertOk(); } - /** @test */ - public function it_doesnt_output_anything_if_object_cannot_be_cast_to_a_string() - { - Log::shouldReceive('debug')->once() - ->with('Cannot render an object variable as a string: {{ object }}', [ - 'line' => 1, 'file' => '', - ]); - - $object = new class - { - }; - - $this->assertEquals('', $this->renderString('{{ object }}', compact('object'))); - } - - /** @test */ - public function it_cannot_cast_non_arrayable_objects_to_arrays_when_using_tag_pairs() - { - Log::shouldReceive('debug')->once() - ->with('Cannot loop over non-loopable variable: {{ object }}', [ - 'line' => 1, 'file' => '', - ]); - - $nonArrayableObject = new NonArrayableObject([ - 'one' => 'foo', - 'two' => 'bar', - ]); - - $this->assertEquals( - '', - $this->renderString('{{ object }}{{ one }} {{ two }}{{ /object }}', [ - 'object' => $nonArrayableObject, - ]) - ); - } - public function test_it_passes_data_to_php_when_enabled() { $this->assertEquals('hello', (string) $this->parser()->allowPhp(true)->parse('{{ associative }}{{ /associative }}', $this->variables)); @@ -118,45 +2523,47 @@ public function test_it_returns_escaped_content() $input = 'Hey, look at that @{{ noun }}!'; $this->assertSame('Hey, look at that {{ noun }}!', $this->renderString($input, [])); } +} - /** @test */ - public function it_applies_modifier_on_different_array_syntax() +class NonArrayableObject +{ + protected $data; + + public function __construct($data) { - $vars = [ - 'key' => 'entries', - 'source' => [ - 'entries' => [ - ['id' => 0], - ['id' => 1], - ['id' => 2], - ['id' => 3], - ], - ], - ]; + $this->data = $data; + } +} - $this->assertEquals( - '[0][1][2][3]', - $this->renderString('{{ source.entries }}[{{ id }}]{{ /source.entries }}', $vars) - ); +class ArrayableObject extends NonArrayableObject implements Arrayable +{ + public function toArray() + { + return $this->data; + } +} - $this->assertEquals( - '[0][1][2][3]', - $this->renderString('{{ source[key] }}[{{ id }}]{{ /source[key] }}', $vars) - ); +class AugmentableObject extends ArrayableObject implements Augmentable +{ + use HasAugmentedData; - $this->assertEquals( - '[0][1][2][3]', - $this->renderString('{{ source.entries sort="id" }}[{{ id }}]{{ /source.entries }}', $vars) - ); + public function augmentedArrayData() + { + return $this->data; + } - $this->assertEquals( - '[0][1][2][3]', - $this->renderString('{{ source[key] sort="id" }}[{{ id }}]{{ /source[key] }}', $vars) - ); + public function blueprint() + { + FieldtypeRepository::shouldReceive('find')->andReturn(new class extends Fieldtype + { + public function augment($data) + { + return strtoupper($data).'!'; + } + }); - $this->assertEquals( - '[3][2][1][0]', - $this->renderString('{{ source[key] sort="id|desc" }}[{{ id }}]{{ /source[key] }}', $vars) - ); + return (new Blueprint)->setContents(['fields' => [ + ['handle' => 'one', 'field' => ['type' => 'test']], + ]]); } } diff --git a/tests/View/Antlers/EngineTests.php b/tests/View/Antlers/EngineTests.php deleted file mode 100644 index 59830fb0608..00000000000 --- a/tests/View/Antlers/EngineTests.php +++ /dev/null @@ -1,80 +0,0 @@ -engine = new Engine( - $this->files = Mockery::mock(Filesystem::class), - app(Parser::class) - ); - } - - /** @test */ - public function parses_a_basic_template() - { - $this->files - ->shouldReceive('get') - ->with('/path/to/foo.antlers.html') - ->andReturn('Hello {{ foo }}'); - - $this->assertEquals( - 'Hello World', - $this->engine->get('/path/to/foo.antlers.html', ['foo' => 'World']) - ); - } - - /** @test */ - public function parses_a_template_with_noparse_tags() - { - $this->files - ->shouldReceive('get') - ->with('/path/to/foo.antlers.html') - ->andReturn('Hello {{ foo }} {{ noparse }}{{ bar }}{{ /noparse }} {{ bar }}'); - - $this->assertEquals( - 'Hello World {{ bar }} Bar', - $this->engine->get('/path/to/foo.antlers.html', ['foo' => 'World', 'bar' => 'Bar']) - ); - } - - /** @test */ - public function php_is_not_executed_if_the_filename_is_html() - { - $this->files - ->shouldReceive('get') - ->with('/path/to/foo.antlers.html') - ->andReturn('Hello '); - - $this->assertEquals( - 'Hello <?php echo "World"; ?>', - $this->engine->get('/path/to/foo.antlers.html', ['foo' => 'World']) - ); - } - - /** @test */ - public function php_is_executed_if_the_filename_is_php() - { - $this->files - ->shouldReceive('get') - ->with('/path/to/foo.antlers.php') - ->andReturn('Hello '); - - $this->assertEquals( - 'Hello World', - $this->engine->get('/path/to/foo.antlers.php', ['foo' => 'World']) - ); - } -} diff --git a/tests/View/Antlers/ParserTest.php b/tests/View/Antlers/ParserTest.php deleted file mode 100644 index df73b68f0b4..00000000000 --- a/tests/View/Antlers/ParserTest.php +++ /dev/null @@ -1,27 +0,0 @@ -set('statamic.antlers.version', 'regex'); - } - - private function renderString($template, $data = []) - { - return (string) $this->parser()->parse($template, $data); - } - - private function parser() - { - return Antlers::parser(); - } -} diff --git a/tests/View/Antlers/ParserTests.php b/tests/View/Antlers/ParserTests.php deleted file mode 100644 index ccd7a6c897f..00000000000 --- a/tests/View/Antlers/ParserTests.php +++ /dev/null @@ -1,2518 +0,0 @@ -variables = [ - 'default_key' => 'two', - 'first_key' => 'three', - 'second_key' => 'deep', - 'good' => true, - 'bad' => false, - 'unknown' => null, - 'string' => 'Hello wilderness', - 'simple' => ['one', 'two', 'three'], - 'complex' => [ - ['string' => 'the first string'], - ['string' => 'the second string'], - ], - 'complex_string' => 'Hello wildernesses', - 'associative' => [ - 'one' => 'hello', - 'two' => 'wilderness', - 'three' => [ - 'deep' => 'Very deep', - ], - ], - 'date' => 'June 19 2012', - 'content' => 'Paragraph', - ]; - } - - /** @test */ - public function string_variable() - { - $template = '{{ string }}'; - - $this->assertEquals('Hello wilderness', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function string_variables_with_tight_braces() - { - $template = '{{string}}'; - - $this->assertEquals('Hello wilderness', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function array_variables() - { - $template = <<<'EOT' -before -{{ simple }} - {{ value }}, {{ count or "0" }}, {{ index or "0" }}, {{ total_results }} - {{ if first }}first{{ elseif last }}last{{ else }}neither{{ /if }} - - -{{ /simple }} -after -EOT; - - $expected = <<<'EOT' -before - - one, 1, 0, 3 - first - - - - two, 2, 1, 3 - neither - - - - three, 3, 2, 3 - last - - - -after -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables)); - - $this->assertEquals('wilderness', $this->renderString('{{ associative[default_key] }}', $this->variables)); - $this->assertEquals('Very deep', $this->renderString('{{ associative[first_key][second_key] }}', $this->variables)); - $this->assertEquals('Very deep', $this->renderString('{{ associative[\'three\'][second_key] }}', $this->variables)); - $this->assertEquals('Very deep', $this->renderString('{{ associative["three"][second_key] }}', $this->variables)); - $this->assertEquals('Very deep', $this->renderString('{{ associative.three[second_key] }}', $this->variables)); - $this->assertEquals('Very deep', $this->renderString('{{ associative:three[second_key] }}', $this->variables)); - } - - /** @test */ - public function complex_array_variable() - { - $template = <<<'EOT' -before -{{ complex }} - {{ string }}, {{ count or "0" }}, {{ index or "0" }}, {{ total_results }} - {{ if first }}first{{ elseif last }}last{{ else }}neither{{ /if }} - - -{{ /complex }} -after -EOT; - - $expected = <<<'EOT' -before - - the first string, 1, 0, 2 - first - - - - the second string, 2, 1, 2 - last - - - -after -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables)); - } - - /** @test */ - public function associative_array_variable() - { - $template = <<<'EOT' -before -{{ associative }} - {{ one }} - {{ two }} - {{ value or "no value" }} - {{ key or "no key" }} - {{ count or "no count" }} - {{ index or "no index" }} - {{ total_results or "no total_results" }} - {{ first or "no first" }} - {{ last or "no last" }} -{{ /associative }} -after -EOT; - - $expected = <<<'EOT' -before - - hello - wilderness - no value - no key - no count - no index - no total_results - no first - no last - -after -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables)); - } - - /** @test */ - public function scope_glue() - { - $template = '{{ associative:one }} {{ associative.two }}'; - - $this->assertEquals('hello wilderness', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function non_existent_variables_should_be_null() - { - $template = '{{ missing }}'; - - $this->assertEquals('', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function accessing_strings_as_arrays_returns_null() - { - $this->assertEquals('bar, ><', $this->renderString('{{ foo }}, >{{ foo:test }}<', ['foo' => 'bar'])); - } - - /** @test */ - public function accessing_string_as_array_which_exists_as_callback_calls_the_callback() - { - (new class extends Tags - { - public static $handle = 'foo'; - - public function test() - { - return 'callback'; - } - })::register(); - - $this->assertEquals('bar, callback', $this->renderString('{{ foo }}, {{ foo:test }}', ['foo' => 'bar'], true)); - } - - /** @test */ - public function non_arrays_cannot_be_looped() - { - Log::shouldReceive('debug')->once() - ->with('Cannot loop over non-loopable variable: {{ string }}'); - - $template = '{{ string }} {{ /string }}'; - - $this->assertEquals('', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function static_strings_with_double_quotes_should_be_left_alone() - { - $template = '{{ "Thundercats are Go!" }}'; - - $this->assertEquals('Thundercats are Go!', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function static_strings_with_single_quotes_should_be_left_alone() - { - $template = "{{ 'Thundercats are Go!' }}"; - - $this->assertEquals('Thundercats are Go!', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function static_strings_with_double_quotes_can_be_modified() - { - $template = '{{ "Thundercats are Go!" | upper }}'; - - $this->assertEquals('THUNDERCATS ARE GO!', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function static_strings_with_single_quotes_can_be_modified() - { - $template = "{{ 'Thundercats are Go!' | upper }}"; - - $this->assertEquals('THUNDERCATS ARE GO!', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function single_braces_should_not_be_parsed() - { - $template = '{string}'; - - $this->assertEquals('{string}', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function modified_non_existent_variables_should_be_null() - { - $template = '{{ missing|upper }}'; - - $this->assertEquals('', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function unclosed_array_variable_pairs_should_be_null() - { - Log::shouldReceive('debug')->once() - ->with('Cannot render an array variable as a string: {{ simple }}'); - - $template = '{{ simple }}'; - - $this->assertEquals('', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function single_condition() - { - $template = '{{ if string == "Hello wilderness" }}yes{{ endif }}'; - - $this->assertEquals('yes', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function multiple_and_conditions() - { - $template = '{{ if string == "Hello wilderness" && content }}yes{{ endif }}'; - - $this->assertEquals('yes', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function multiple_or_conditions() - { - $should_pass = '{{ if string == "failure" || string == "Hello wilderness" }}yes{{ endif }}'; - $should_fail = '{{ if string == "failure" or string == "womp" }}yes{{ endif }}'; - - $this->assertEquals('yes', $this->renderString($should_pass, $this->variables)); - $this->assertEquals('', $this->renderString($should_fail, $this->variables)); - } - - /** @test */ - public function or_existence_conditions() - { - $should_pass = '{{ if string || strudel }}yes{{ endif }}'; - $should_also_pass = '{{ if strudel or string }}yes{{ endif }}'; - $should_fail = '{{ if strudel || wurst }}yes{{ endif }}'; - $should_also_fail = '{{ if strudel or wurst }}yes{{ endif }}'; - - $this->assertEquals('yes', $this->renderString($should_pass, $this->variables)); - $this->assertEquals('yes', $this->renderString($should_also_pass, $this->variables)); - $this->assertEquals('', $this->renderString($should_fail, $this->variables)); - $this->assertEquals('', $this->renderString($should_also_fail, $this->variables)); - } - - /** @test */ - public function conditions_on_overlapping_variable_names() - { - $template = '{{ if complex }}{{ complex limit="1" }}{{ string }}{{ /complex }}{{ /if }}'; - - $this->assertEquals('the first string', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function loop_with_param_inside_condition_matching_variable_name() - { - $template = '{{ if complex_string }}{{ complex_string }}{{ /if }}{{ complex }}{{ /complex }}'; - - $this->assertEquals('Hello wildernesses', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function ternary_condition() - { - $template = '{{ string ? "Pass" : "Fail" }}'; - - $this->assertEquals('Pass', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function ternary_condition_with_dynamic_array() - { - $template = '{{ associative[default_key] ? "Pass" : "Fail" }}'; - - $this->assertEquals('Pass', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function ternary_condition_isnt_too_greedy() - { - $template = '{{ content }} {{ string ? "Pass" : "Fail" }} {{ content }}'; - - $this->assertEquals('Paragraph Pass Paragraph', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function ternary_condition_with_a_variable() - { - $template = '{{ string ? string : "Fail" }}'; - - $this->assertEquals('Hello wilderness', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function ternary_condition_with_modifiers() - { - $template = '{{ string ? string | upper : "Fail" }}'; - - $this->assertEquals('HELLO WILDERNESS', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function ternary_condition_with_modifiers_and_dynamic_array() - { - $template = '{{ string ? associative[default_key] | upper : "Fail" }}'; - - $this->assertEquals('WILDERNESS', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function ternary_condition_with_multiple_lines() - { - $template = <<<'EOT' -{{ string - ? "Pass" - : "Fail" }} -EOT; - - $this->assertEquals('Pass', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function ternary_escapes_quotes_properly() - { - $data = ['condition' => true, 'var' => '"Wow" said the man']; - $template = '{{ condition ? var : "nah" }}'; - - $this->assertEquals('"Wow" said the man', $this->renderString($template, $data)); - } - - /** @test */ - public function ternary_condition_inside_parameter() - { - $this->app['statamic.tags']['test'] = \Tests\Fixtures\Addon\Tags\Test::class; - - $this->assertEquals('yes', $this->renderString( - "{{ test variable='{{ good ? 'yes' : 'fail' }}' }}", - $this->variables, - true - )); - - $this->assertEquals('fail', $this->renderString( - "{{ test variable='{{ bad ? 'yes' : 'fail' }}' }}", - $this->variables, - true - )); - - $this->assertEquals('fail', $this->renderString( - "{{ test variable='{{ unknown ?? 'fail' }}' }}", - $this->variables, - true - )); - - $this->assertEquals('yes', $this->renderString( - "{{ test variable='{{ !unknown ? 'yes' : 'fail' }}' }}", - $this->variables, - true - )); - } - - /** @test */ - public function null_coalescence() - { - // or, ?:, and ?? are all aliases. - // while ?: and ?? have slightly different behaviors in php, they work the same in antlers. - - $this->assertEquals('Hello wilderness', $this->renderString('{{ string or "Pass" }}', $this->variables)); - $this->assertEquals('Hello wilderness', $this->renderString('{{ string ?: "Pass" }}', $this->variables)); - $this->assertEquals('Hello wilderness', $this->renderString('{{ string ?? "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ missing or "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ missing ?: "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ missing ?? "Pass" }}', $this->variables)); - - $this->assertEquals('Pass', $this->renderString('{{ missing or "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ missing ?: "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ missing ?? "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ missing[thing] or "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ missing[thing] ?: "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ missing[thing] ?? "Pass" }}', $this->variables)); - } - - /** @test */ - public function truth_coalescing() - { - $this->assertEquals('Pass', $this->renderString('{{ string ?= "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ associative:one ?= "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ associative[default_key] ?= "Pass" }}', $this->variables)); - $this->assertEquals('', $this->renderString('{{ missing ?= "Pass" }}', $this->variables)); - $this->assertEquals('', $this->renderString('{{ missing:thing ?= "Pass" }}', $this->variables)); - $this->assertEquals('', $this->renderString('{{ missing[thing] ?= "Pass" }}', $this->variables)); - - // Negating with ! - $this->assertEquals('', $this->renderString('{{ !string ?= "Pass" }}', $this->variables)); - $this->assertEquals('', $this->renderString('{{ !associative:one ?= "Pass" }}', $this->variables)); - $this->assertEquals('', $this->renderString('{{ !associative[default_key] ?= "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ !missing ?= "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ !missing:thing ?= "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ !missing[thing] ?= "Pass" }}', $this->variables)); - - // and with spaces - $this->assertEquals('', $this->renderString('{{ ! string ?= "Pass" }}', $this->variables)); - $this->assertEquals('', $this->renderString('{{ ! associative:one ?= "Pass" }}', $this->variables)); - $this->assertEquals('', $this->renderString('{{ ! associative[default_key] ?= "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ ! missing ?= "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ ! missing:thing ?= "Pass" }}', $this->variables)); - $this->assertEquals('Pass', $this->renderString('{{ ! missing[thing] ?= "Pass" }}', $this->variables)); - } - - /** @test */ - public function truth_coalescing_inside_loop() - { - $template = '{{ complex }}{{ first ?= "Pass" }}{{ /complex }}'; - - $this->assertEquals('Pass', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function single_standard_string_modifier_tight() - { - $template = '{{ string|upper }}'; - - $this->assertEquals('HELLO WILDERNESS', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function chained_standard_string_modifiers_tight() - { - $template = '{{ string|upper|lower }}'; - - $this->assertEquals('hello wilderness', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function single_standard_string_modifier_relaxed() - { - $template = '{{ string | upper }}'; - - $this->assertEquals('HELLO WILDERNESS', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function chained_standard_string_modifiers_relaxed() - { - $template = '{{ string | upper | lower }}'; - - $this->assertEquals('hello wilderness', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function chained_standard_string_modifiers_from_dynamic_array_relaxed() - { - $template = '{{ associative[default_key] | upper | lower }}'; - - $this->assertEquals('wilderness', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function single_parameter_string_modifier() - { - $template = "{{ string upper='true' }}"; - - $this->assertEquals('HELLO WILDERNESS', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function single_parameter_string_from_array_modifier() - { - $this->assertEquals( - 'WILDERNESS', - $this->renderString("{{ associative.two upper='true' }}", $this->variables) - ); - - $this->assertEquals( - 'WILDERNESS', - $this->renderString("{{ associative['two'] upper='true' }}", $this->variables) - ); - - $this->assertEquals( - 'WILDERNESS', - $this->renderString("{{ associative[default_key] upper='true' }}", $this->variables) - ); - } - - /** @test */ - public function chained_parameter_string_modifiers() - { - $template = "{{ string upper='true' lower='true' }}"; - - $this->assertEquals('hello wilderness', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function single_standard_array_modifier_tight() - { - $template = '{{ simple|length }}'; - - $this->assertEquals('3', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function single_standard_array_modifier_relaxed() - { - $template = '{{ simple | length }}'; - - $this->assertEquals('3', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function chained_standard_array_modifiers_tight_on_content() - { - $template = '{{ content|markdown|lower }}'; - - $this->assertEquals("

paragraph

\n", $this->renderString($template, $this->variables)); - } - - /** @test */ - public function chained_standard_modifiers_relaxed_on_content() - { - $template = '{{ content | markdown | lower }}'; - - $this->assertEquals("

paragraph

\n", $this->renderString($template, $this->variables)); - } - - /** @test */ - public function chained_parameter_modifiers_on_content() - { - $template = "{{ content markdown='true' lower='true' }}"; - - $this->assertEquals("

paragraph

\n", $this->renderString($template, $this->variables)); - } - - /** @test */ - public function conditions_with_modifiers() - { - $template = "{{ if string|upper == 'HELLO WILDERNESS' }}yes{{ endif }}"; - - $this->assertEquals('yes', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function conditions_with_relaxed_modifiers() - { - $template = "{{ if string | upper == 'HELLO WILDERNESS' }}yes{{ endif }}"; - - $this->assertEquals('yes', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function tags_with_curlies_in_params_gets_parsed() - { - // the variables are inside Test@index - $this->app['statamic.tags']['test'] = \Tests\Fixtures\Addon\Tags\Test::class; - - $template = "{{ test variable='{string}' }}"; - - $this->assertEquals('Hello wilderness', $this->renderString($template, $this->variables, true)); - } - - /** @test */ - public function date_condition_with_chained_relaxed_modifiers_with_spaces_in_arguments() - { - $template = '{{ if (date | modify_date:+3 years | format:Y) == "2015" }}yes{{ endif }}'; - - $this->assertEquals('yes', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function array_modifiers_get_parsed() - { - $template = '{{ simple limit="1" }}{{ value }}{{ /simple }}'; - - $this->assertEquals('one', $this->renderString($template, $this->variables)); - } - - /** @test */ - public function array_modifiers_on_collections_get_parsed() - { - $template = '{{ simple limit="1" }}{{ value }}{{ /simple }}'; - - $this->assertEquals('one', $this->renderString($template, [ - 'simple' => collect(['one', 'two', 'three']), - ])); - } - - /** @test */ - public function recursive_children() - { - // the variables are inside RecursiveChildren@index - $this->app['statamic.tags']['recursive_children'] = RecursiveChildren::class; - - $template = '
    {{ recursive_children }}
  • {{ title }}.{{ foo }}{{ if children }}
      {{ *recursive children* }}
    {{ /if }}
  • {{ /recursive_children }}
'; - - $expected = '
  • One.Bar
    • Two.Bar
    • Three.Bar
      • Four.Baz
'; - - $this->assertEquals($expected, $this->renderString($template, ['foo' => 'Bar'], true)); - } - - /** @test */ - public function recursive_children_with_scope() - { - // the variables are inside RecursiveChildren@index - $this->app['statamic.tags']['recursive_children'] = RecursiveChildren::class; - - $template = '
    {{ recursive_children scope="item" }}
  • {{ item:title }}.{{ item:foo }}.{{ foo }}{{ if item:children }}
      {{ *recursive item:children* }}
    {{ /if }}
  • {{ /recursive_children }}
'; - - $expected = '
  • One..Bar
    • Two..Bar
    • Three..Bar
      • Four.Baz.Baz
'; - - $this->assertEquals($expected, $this->renderString($template, ['foo' => 'Bar'], true)); - } - - /** @test */ - public function empty_values_are_not_overridden_by_previous_iteration() - { - $variables = [ - 'loop' => [ - [ - 'one' => '[1.1]', - 'two' => '[1.2]', - ], - [ - 'one' => '[2.1]', - ], - ], - ]; - - $this->assertEquals( - '[1.1][1.2][2.1]', - $this->renderString('{{ loop }}{{ one }}{{ two }}{{ /loop }}', $variables) - ); - } - - /** @test */ - public function empty_values_are_not_overridden_by_previous_iteration_with_parsing() - { - $this->app['statamic.tags']['test'] = \Tests\Antlers\Fixtures\Addon\Tags\Test::class; - - // Variable name was changed from "loop" to "loopvar" compared to the original test to avoid a - // headache since the base test class is going to be loading all of the core Statamic Tags. - $variables = [ - 'loopvar' => [ - [ - 'one' => '[1.1]', - 'two' => '[1.2]', - ], - [ - 'one' => '[2.1]', - ], - ], - ]; - - $this->assertEquals( - '[1.1][1.2][2.1]', - $this->renderString('{{ loopvar }}{{ one }}{{ test:some_parsing var="two" }}{{ two }}{{ /test:some_parsing }}{{ /loopvar }}', $variables, true) - ); - } - - /** @test */ - public function nested_array_syntax() - { - $variables = [ - 'hello' => [ - 'world' => [ - ['baz' => 'one'], - ['baz' => 'two'], - ], - 'id' => '12345', - ], - ]; - - $this->assertEquals( - '[one][two]', - $this->renderString('{{ hello:world }}[{{ baz }}]{{ /hello:world }}', $variables) - ); - - $this->assertEquals( - '[one][two]', - $this->renderString('{{ hello:world scope="s" }}[{{ s:baz }}]{{ /hello:world }}', $variables) - ); - } - - /** @test */ - public function it_parses_php_when_enabled() - { - $this->assertEquals( - 'Hello wilderness!', - $this->parser($this->variables)->allowPhp()->parse('{{ string }}', $this->variables) - ); - - $this->assertEquals( - 'Hello wilderness<?php echo "!"; ?>', - $this->parser($this->variables)->allowPhp(false)->parse('{{ string }}', $this->variables) - ); - } - - /** @test */ - public function it_doesnt_parse_noparse_tags() - { - $parsed = $this->renderString('{{ noparse }}{{ string }}{{ /noparse }} {{ string }}', $this->variables); - - $this->assertEquals('{{ string }} Hello wilderness', $parsed); - } - - /** @test */ - public function it_doesnt_parse_data_in_noparse_modifiers() - { - $variables = [ - 'string' => 'hello', - 'content' => 'before {{ string }} after', - ]; - - $parsed = $this->renderString('{{ content | noparse }} {{ string }}', $variables); - - $this->assertEquals('before {{ string }} after hello', $parsed); - } - - /** @test */ - public function it_doesnt_parse_data_in_noparse_modifiers_with_null_coalescence() - { - $variables = [ - 'string' => 'hello', - 'content' => 'before {{ string }} after', - ]; - $parsed = $this->renderString('{{ missing or content | noparse }} {{ string }}', $variables); - $this->assertEquals('before {{ string }} after hello', $parsed); - } - - /** @test */ - public function it_doesnt_parse_noparse_tags_inside_callbacks() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function array() - { - return []; - } - - public function loop() - { - return [ - ['string' => 'One'], - ['string' => 'Two'], - ]; - } - })::register(); - - $template = <<<'EOT' -{{ tag:array }}{{ noparse }}{{ string }}{{ /noparse }}{{ /tag:array }} -{{ tag:loop }} - {{ index }} {{ noparse }}{{ string }}{{ /noparse }} {{ string }} -{{ /tag:loop }} -EOT; - - $expected = <<<'EOT' -{{ string }} - - 0 {{ string }} One - - 1 {{ string }} Two - -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables, true)); - } - - /** @test */ - public function it_doesnt_parse_data_in_noparse_modifiers_inside_callbacks() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function array() - { - return [ - 'string' => 'hello', - 'content' => 'beforesingle {{ string }} aftersingle', - ]; - } - - public function loop() - { - return [ - [ - 'string' => 'One', - 'content' => 'beforepair {{ string }} afterpair', - ], - [ - 'string' => 'Two', - 'content' => 'beforepair {{ string }} afterpair', - ], - ]; - } - })::register(); - - $template = <<<'EOT' -{{ tag:array }}{{ content | noparse }}{{ /tag:array }} -{{ tag:loop }} - {{ count }} {{ content | noparse }} {{ string }} -{{ /tag:loop }} -EOT; - - $expected = <<<'EOT' -beforesingle {{ string }} aftersingle - - 1 beforepair {{ string }} afterpair One - - 2 beforepair {{ string }} afterpair Two - -EOT; - - $parsed = $this->renderString($template, [], true); - $this->assertEqualsWithCollapsedNewlines($expected, $parsed); - } - - /** @test */ - public function it_doesnt_parse_tags_prefixed_with_an_at_symbol() - { - $this->assertEquals('foo {{ bar }} baz', $this->renderString('foo @{{ bar }} baz')); - } - - /** @test */ - public function it_doesnt_parse_tags_prefixed_with_an_at_symbol_over_multiple_lines() - { - $template = <<<'EOT' -@{{ foo }} -bar -{{ baz }} -EOT; - - $expected = <<<'EOT' -{{ foo }} -bar -BAZ -EOT; - - $this->assertEquals($expected, $this->renderString($template, ['baz' => 'BAZ'])); - } - - /** @test */ - public function it_doesnt_parse_tags_prefixed_with_an_at_symbol_over_tags_in_multiple_lines() - { - $template = <<<'EOT' -@{{ foo }} {{ qux }} -bar -{{ baz }} -EOT; - - $expected = <<<'EOT' -{{ foo }} QUX -bar -BAZ -EOT; - - $this->assertEquals($expected, $this->renderString($template, ['baz' => 'BAZ', 'qux' => 'QUX'])); - } - - /** @test */ - public function it_doesnt_parse_multiline_tags_prefixed_with_an_at_symbol_over_tags_in_multiple_lines() - { - $template = <<<'EOT' -@{{ foo - bar:baz="qux" -}} {{ qux }} -bar -{{ baz }} -EOT; - - $expected = <<<'EOT' -{{ foo - bar:baz="qux" -}} QUX -bar -BAZ -EOT; - - $this->assertEquals($expected, $this->renderString($template, ['baz' => 'BAZ', 'qux' => 'QUX'])); - } - - /** @test */ - public function it_doesnt_parse_tags_prefixed_with_an_at_symbol_containing_nested_tags() - { - $this->assertEquals('{{ foo bar="{baz}" }}', $this->renderString('@{{ foo bar="{baz}" }}')); - } - - /** @test */ - public function it_accepts_an_arrayable_object() - { - $this->assertEquals( - 'Hello World', - $this->renderString('{{ string }}', new ArrayableObject(['string' => 'Hello World'])) - ); - } - - /** @test */ - public function it_throws_exception_for_non_arrayable_data_object() - { - try { - $this->renderString('{{ string }}', new NonArrayableObject(['string' => 'Hello World'])); - } catch (\InvalidArgumentException $e) { - $this->assertEquals('Expecting array or object implementing Arrayable. Encountered [Tests\View\Antlers\NonArrayableObject]', $e->getMessage()); - - return; - } - - $this->fail('Exception was not thrown.'); - } - - /** @test */ - public function it_throws_exception_for_unsupported_data_value() - { - try { - $this->renderString('{{ string }}', 'string'); - } catch (\InvalidArgumentException $e) { - $this->assertEquals('Expecting array or object implementing Arrayable. Encountered [string]', $e->getMessage()); - - return; - } - - $this->fail('Exception was not thrown.'); - } - - /** @test */ - public function it_gets_augmented_value() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - return 'augmented '.$value; - } - }; - - $value = new Value('expected', 'test', $fieldtype); - - $parsed = $this->renderString('{{ test }}', ['test' => $value]); - - $this->assertEquals('augmented expected', $parsed); - } - - /** @test */ - public function it_expands_augmented_value_when_used_as_an_array() - { - $fieldtype = new class extends Fieldtype - { - public function augment($values) - { - return collect($values)->map(function ($value) { - return strtoupper($value); - })->all(); - } - }; - - $value = new Value([ - 'one' => 'hello', - 'two' => 'world', - ], 'test', $fieldtype); - - $parsed = $this->renderString('{{ test }}{{ one }} {{ two }}{{ /test }}', ['test' => $value]); - - $this->assertEquals('HELLO WORLD', $parsed); - } - - /** @test */ - public function it_gets_nested_values_from_augmentable_objects() - { - $value = new AugmentableObject(['foo' => 'bar']); - - $parsed = $this->renderString('{{ test:foo }}', ['test' => $value]); - - $this->assertEquals('bar', $parsed); - } - - /** @test */ - public function it_loops_over_value_object() - { - $fieldtype = new class extends Fieldtype - { - public function augment($values) - { - return collect($values)->map(function ($value) { - return collect($value)->map(function ($v) { - return strtoupper($v); - }); - })->toArray(); - } - }; - - $value = new Value([ - ['one' => 'uno', 'two' => 'dos'], - ['one' => 'une', 'two' => 'deux'], - ], 'test', $fieldtype); - - $parsed = $this->renderString('{{ test }}{{ one }} {{ two }} {{ /test }}', ['test' => $value]); - - $this->assertEquals('UNO DOS UNE DEUX ', $parsed); - } - - /** @test */ - public function it_gets_nested_values_from_value_objects() - { - $value = new Value(['foo' => 'bar'], 'test'); - - $parsed = $this->renderString('{{ test:foo }}', ['test' => $value]); - - $this->assertEquals('bar', $parsed); - } - - /** @test */ - public function it_gets_nested_values_from_nested_value_objects() - { - $value = new Value(['foo' => 'bar'], 'test'); - - $parsed = $this->renderString('{{ nested:test:foo }}', [ - 'nested' => [ - 'test' => $value, - ], - ]); - - $this->assertEquals('bar', $parsed); - } - - /** @test */ - public function it_gets_nested_values_from_within_nested_value_objects() - { - $value = new Value([ - 'foo' => ['nested' => 'bar'], - ], 'test'); - - $parsed = $this->renderString('{{ nested:test:foo:nested }}', [ - 'nested' => [ - 'test' => $value, - ], - ]); - - $this->assertEquals('bar', $parsed); - } - - /** @test */ - public function it_parses_value_objects_values_when_configured_to_do_so() - { - $fieldtypeOne = new class extends Fieldtype - { - public function augment($value) - { - return 'augmented '.$value; - } - - public function config(?string $key = null, $fallback = null) - { - return true; - } - - // fake what's being returned from the field config - }; - $fieldtypeTwo = new class extends Fieldtype - { - public function augment($value) - { - return 'augmented '.$value; - } - - public function config(?string $key = null, $fallback = null) - { - return false; - } - - // fake what's being returned from the field config - }; - - $parseable = new Value('before {{ string }} after', 'parseable', $fieldtypeOne); - $nonParseable = new Value('before {{ string }} after', 'non_parseable', $fieldtypeTwo); - - $template = <<<'EOT' -{{ parseable }} -{{ non_parseable }} -EOT; - - $expected = <<<'EOT' -augmented before hello after -augmented before {{ string }} after -EOT; - - $variables = [ - 'parseable' => $parseable, - 'non_parseable' => $nonParseable, - 'string' => 'hello', - ]; - - $this->assertEquals($expected, (string) $this->renderString($template, $variables)); - - $this->assertEquals( - 'shmaugmented before hello after', - (string) $this->renderString('{{ parseable | replace:aug:shmaug }}', $variables) - ); - - $this->assertEquals( - 'shmaugmented before {{ string }} after', - (string) $this->renderString('{{ non_parseable | replace:aug:shmaug }}', $variables) - ); - - $this->assertEquals( - 'shmaugmented before hello after', - (string) $this->renderString('{{ parseable replace="aug|shmaug" }}', $variables) - ); - - $this->assertEquals( - 'shmaugmented before {{ string }} after', - (string) $this->renderString('{{ non_parseable replace="aug|shmaug" }}', $variables) - ); - } - - /** @test */ - public function it_casts_objects_to_string_when_using_single_tags() - { - $object = new class - { - public function __toString() - { - return 'string'; - } - }; - - $this->assertEquals( - 'string', - $this->renderString('{{ object }}', compact('object')) - ); - } - - /** @test */ - public function it_doesnt_output_anything_if_object_cannot_be_cast_to_a_string() - { - Log::shouldReceive('debug')->once() - ->with('Cannot render an object variable as a string: {{ object }}'); - - $object = new class - { - }; - - $this->assertEquals('', $this->renderString('{{ object }}', compact('object'))); - } - - /** @test */ - public function it_casts_arrayable_objects_to_arrays_when_using_tag_pairs() - { - $arrayableObject = new ArrayableObject([ - 'one' => 'foo', - 'two' => 'bar', - ]); - - $this->assertEquals( - 'foo bar', - $this->renderString('{{ object }}{{ one }} {{ two }}{{ /object }}', [ - 'object' => $arrayableObject, - ]) - ); - } - - /** @test */ - public function it_cannot_cast_non_arrayable_objects_to_arrays_when_using_tag_pairs() - { - Log::shouldReceive('debug')->once() - ->with('Cannot loop over non-loopable variable: {{ object }}'); - - $nonArrayableObject = new NonArrayableObject([ - 'one' => 'foo', - 'two' => 'bar', - ]); - - $this->assertEquals( - '', - $this->renderString('{{ object }}{{ one }} {{ two }}{{ /object }}', [ - 'object' => $nonArrayableObject, - ]) - ); - } - - /** @test */ - public function callback_tags_that_return_unparsed_simple_arrays_get_parsed() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - return ['one' => 'a', 'two' => 'b']; - } - })::register(); - - $template = <<<'EOT' -{{ tag }} - {{ one }} {{ two }} -{{ /tag }} -EOT; - - $expected = <<<'EOT' - - a b - -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, [], true)); - } - - /** @test */ - public function callback_tags_that_return_unparsed_simple_arrays_get_parsed_with_scope() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - return ['one' => 'a', 'two' => 'b']; - } - })::register(); - - $template = <<<'EOT' -{{ tag scope="foo" }} - {{ foo:one }} {{ foo:two }} -{{ /tag }} -EOT; - - $expected = <<<'EOT' - - a b - -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, [], true)); - } - - /** @test */ - public function callback_tags_that_return_unparsed_multidimensional_arrays_get_parsed() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - return [ - ['one' => 'a', 'two' => 'b'], - ['one' => 'c', 'two' => 'd'], - ]; - } - })::register(); - - $template = <<<'EOT' -{{ string }} -{{ tag }} - {{ count }} {{ if first }}first{{ else }}not-first{{ /if }} {{ if last }}last{{ else }}not-last{{ /if }} {{ one }} {{ two }} {{ string }} -{{ /tag }} -EOT; - - $expected = <<<'EOT' -Hello wilderness - - 1 first not-last a b Hello wilderness - - 2 not-first last c d Hello wilderness - -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, ['string' => 'Hello wilderness'], true)); - } - - /** @test */ - public function callback_tags_that_return_empty_arrays_get_parsed_with_no_results() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - return []; - } - })::register(); - - $template = <<<'EOT' -{{ tag }} - {{ if no_results }}no results{{ else }}there are results{{ /if }} -{{ /tag }} -EOT; - - $expected = <<<'EOT' - - no results - -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables, true)); - } - - /** @test */ - public function callback_tags_that_return_collections_get_parsed() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - return collect([ - ['one' => 'a', 'two' => 'b'], - ['one' => 'c', 'two' => 'd'], - ]); - } - })::register(); - - $template = <<<'EOT' -{{ string }} -{{ tag }} - {{ count }} {{ if first }}first{{ else }}not-first{{ /if }} {{ if last }}last{{ else }}not-last{{ /if }} {{ one }} {{ two }} {{ string }} -{{ /tag }} -EOT; - - $expected = <<<'EOT' -Hello wilderness - - 1 first not-last a b Hello wilderness - - 2 not-first last c d Hello wilderness - -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables, true)); - } - - /** @test */ - public function callback_tags_that_return_query_builders_get_parsed() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - $builder = Mockery::mock(Builder::class); - $builder->shouldReceive('get')->andReturn(collect([ - ['one' => 'a', 'two' => 'b'], - ['one' => 'c', 'two' => 'd'], - ])); - - return $builder; - } - })::register(); - - $template = <<<'EOT' -{{ string }} -{{ tag }} - {{ count }} {{ if first }}first{{ else }}not-first{{ /if }} {{ if last }}last{{ else }}not-last{{ /if }} {{ one }} {{ two }} {{ string }} -{{ /tag }} -EOT; - - $expected = <<<'EOT' -Hello wilderness - - 1 first not-last a b Hello wilderness - - 2 not-first last c d Hello wilderness - -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $this->variables, true)); - } - - /** @test */ - public function callback_tags_that_return_value_objects_gets_parsed() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - return 'augmented '.$value; - } - }; - - return new Value('the value', null, $fieldtype); - } - })::register(); - - $this->assertEquals('augmented the value', $this->renderString('{{ tag }}', [], true)); - } - - /** @test */ - public function callback_tags_that_return_value_objects_with_antlers_gets_parsed() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - return 'augmented '.$value; - } - }; - - $fieldtype->setField(new Field('test', ['antlers' => true])); - - return new Value('the value with {{ var }} in it', null, $fieldtype); - } - })::register(); - - $this->assertEquals( - 'augmented the value with howdy in it', - (string) $this->renderString('{{ tag }}', ['var' => 'howdy'], true) - ); - } - - /** @test */ - public function callback_tags_that_return_value_objects_with_antlers_disabled_does_not_get_parsed() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - return 'augmented '.$value; - } - }; - - $fieldtype->setField(new Field('test', ['antlers' => false])); - - return new Value('the value with {{ var }} in it', null, $fieldtype); - } - })::register(); - - $this->assertEquals( - 'augmented the value with {{ var }} in it', - (string) $this->renderString('{{ tag }}', ['var' => 'howdy'], true) - ); - } - - /** @test */ - public function value_objects_with_antlers_gets_parsed() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - return 'augmented '.$value; - } - }; - - $fieldtype->setField(new Field('test', ['antlers' => true])); - - $value = new Value('the value with {{ var }} in it', null, $fieldtype); - - $this->assertEquals( - 'augmented the value with howdy in it', - (string) $this->renderString('{{ test }}', [ - 'test' => $value, - 'var' => 'howdy', - ], true) - ); - } - - /** @test */ - public function value_objects_with_antlers_disabled_do_not_get_parsed() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - return 'augmented '.$value; - } - }; - - $fieldtype->setField(new Field('test', ['antlers' => false])); - - $value = new Value('the value with {{ var }} in it', null, $fieldtype); - - $this->assertEquals( - 'augmented the value with {{ var }} in it', - (string) $this->renderString('{{ test }}', [ - 'test' => $value, - 'var' => 'howdy', - ]) - ); - } - - /** @test */ - public function it_automatically_augments_augmentable_objects_when_using_tag_pairs() - { - $augmentable = new AugmentableObject([ - 'one' => 'foo', - 'two' => 'bar', - ]); - - $this->assertEquals( - 'FOO! bar', - $this->renderString('{{ object }}{{ one }} {{ two }}{{ /object }}', [ - 'object' => $augmentable, - ]) - ); - } - - /** @test */ - public function it_automatically_augments_augmentable_objects_when_returned_from_a_callback_tag() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - return new AugmentableObject([ - 'one' => 'foo', - 'two' => 'bar', - ]); - } - })::register(); - - $this->assertEquals( - 'FOO! bar', - $this->renderString('{{ tag }}{{ one }} {{ two }}{{ /tag }}', [], true) - ); - } - - /** @test */ - public function it_automatically_augments_collections_when_using_tag_pairs() - { - $augmentable = collect([ - new AugmentableObject(['one' => 'foo', 'two' => 'bar']), - new AugmentableObject(['one' => 'baz', 'two' => 'qux']), - ]); - - $this->assertEquals( - 'FOO! bar BAZ! qux ', - $this->renderString('{{ object }}{{ one }} {{ two }} {{ /object }}', [ - 'object' => $augmentable, - ]) - ); - } - - /** @test */ - public function callback_tag_pair_variables_get_context_merged_in_but_nulls_remain_null() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - return [ - 'drink' => 'juice', - 'activity' => null, - ]; - } - })::register(); - - $context = [ - 'drink' => 'whisky', - 'food' => 'burger', - 'activity' => 'singing', - ]; - - $template = <<<'EOT' -{{ drink }} {{ food }} {{ activity }} -{{ tag }}{{ drink }} {{ food }} -{{ activity }}-{{ /tag }} -EOT; - - $expected = <<<'EOT' -whisky burger singing -juice burger -- -EOT; - $this->assertEquals($expected, $this->renderString($template, $context, true)); - } - - /** @test */ - public function variable_tag_pair_get_context_merged_in_except_for_nulls() - { - $context = [ - 'drink' => 'whisky', - 'food' => 'burger', - 'activity' => 'singing', - 'array' => [ - 'drink' => 'juice', - 'activity' => null, - ], - ]; - - $template = <<<'EOT' -{{ drink }} {{ food }} {{ activity }} -{{ array }}{{ drink }} {{ food }} -{{ activity }}-{{ /array }} -EOT; - - $expected = <<<'EOT' -whisky burger singing -juice burger -- -EOT; - $this->assertEquals($expected, $this->renderString($template, $context)); - } - - /** @test */ - public function scope_modifier_can_add_scopes() - { - $context = [ - 'drink' => 'whisky', - 'food' => 'burger', - 'array' => [ - ['drink' => 'juice'], - ['drink' => 'smoothie'], - ], - ]; - - $template = <<<'EOT' -{{ food }} {{ drink }} -{{ array scope="s" }} --{{ s:food }}- {{ s:drink }} {{ food }} {{ drink }} -{{ /array }} -EOT; - - $expected = <<<'EOT' -burger whisky - --- juice burger juice - --- smoothie burger smoothie - -EOT; - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, $context, true)); - } - - /** @test */ - public function it_can_reach_into_the_cascade() - { - $cascade = $this->mock(Cascade::class, function ($m) { - $m->shouldReceive('get')->with('page')->andReturn(['drink' => 'juice']); - $m->shouldReceive('get')->with('global')->andReturn(['drink' => 'water']); - $m->shouldReceive('get')->with('menu')->andReturn(['drink' => 'vodka']); - $m->shouldNotReceive('get')->with('nested'); - $m->shouldNotReceive('get')->with('augmented'); - }); - - $fieldtype = new class extends Fieldtype - { - }; - $augmented = new Value(['drink' => 'la croix'], 'augmented', $fieldtype); - - $context = [ - 'drink' => 'whisky', - 'augmented' => $augmented, - 'nested' => [ - 'drink' => 'coke', - 'augmented' => $augmented, - ], - ]; - - $template = <<<'EOT' -var: {{ drink }} -page: {{ page:drink }} -global: {{ global:drink }} -menu: {{ menu:drink }} -nested: {{ nested:drink }} -augmented: {{ augmented:drink }} -nested augmented: {{ nested:augmented:drink }} -EOT; - - $expected = <<<'EOT' -var: whisky -page: juice -global: water -menu: vodka -nested: coke -augmented: la croix -nested augmented: la croix -EOT; - - $results = (string) $this->parser()->cascade($cascade)->parse($template, $context); - $this->assertEquals($expected, $results); - } - - /** @test */ - public function it_can_create_scopes() - { - $context = [ - 'drink' => 'whisky', - 'food' => 'burger', - 'activity' => 'singing', - 'array' => [ - 'drink' => 'juice', - 'activity' => null, - ], - ]; - - $template = <<<'EOT' -{{ scope:test }} -drink: {{ drink }} -food: {{ food }} -activity: {{ activity }} - -{{ array }} - array:drink: {{ drink }} - array:food: {{ food }} - array:activity: -{{ activity }}- - array:test:drink: {{ test:drink }} - array:test:food: {{ test:food }} - array:test:activity: {{ test:activity }} -{{ /array }} -{{ /scope:test }} -EOT; - - $expected = <<<'EOT' -drink: whisky -food: burger -activity: singing - - - array:drink: juice - array:food: burger - array:activity: -- - array:test:drink: whisky - array:test:food: burger - array:test:activity: singing -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, trim($this->renderString($template, $context, true))); - } - - /** @test */ - public function it_does_not_accept_sequences() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Expecting an associative array'); - $this->renderString('', ['foo', 'bar']); - } - - /** @test */ - public function it_does_not_accept_multidimensional_array() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Expecting an associative array'); - $this->renderString('', [ - ['foo' => 'bar'], - ['foo' => 'baz'], - ]); - } - - /** @test */ - public function it_aliases_array_tag_pairs_using_the_as_modifier() - { - $template = <<<'EOT' -{{ array as="stuff" }} -before -{{ stuff }} -{{ foo }} -{{ /stuff }} -after -{{ /array }} -EOT; - - $expected = <<<'EOT' - -before - -bar - -baz - -qux - -after - -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, [ - 'array' => [ - ['foo' => 'bar'], - ['foo' => 'baz'], - ['foo' => 'qux'], - ], - ], true)); - } - - /** @test */ - public function it_aliases_callback_tag_pair_loop_using_the_as_param() - { - (new class extends Tags - { - public static $handle = 'tag'; - - public function index() - { - return [ - ['foo' => 'bar'], - ['foo' => 'baz'], - ['foo' => 'qux'], - ]; - } - })::register(); - - $template = <<<'EOT' -{{ tag as="stuff" }} -before -{{ stuff }} -{{ foo }} -{{ /stuff }} -after -{{ /tag }} -EOT; - - $expected = <<<'EOT' - -before - -bar - -baz - -qux - -after - -EOT; - - $this->assertEqualsWithCollapsedNewlines($expected, $this->renderString($template, [], true)); - } - - /** @test */ - public function it_counts_query_builder_results_in_conditions() - { - (new EntryFactory())->collection('blog')->create(); - - $template = '{{ if entries }}yup{{ else }}nope{{ /if }}'; - - $this->assertEquals('yup', $this->renderString($template, ['entries' => Entry::query()])); - $this->assertEquals('yup', $this->renderString($template, ['entries' => Entry::query()->where('collection', 'blog')])); - $this->assertEquals('nope', $this->renderString($template, ['entries' => Entry::query()->where('collection', 'dunno')])); - } - - /** @test */ - public function it_applies_modifier_on_different_array_syntax() - { - $vars = [ - 'key' => 'entries', - 'source' => [ - 'entries' => [ - ['id' => 0], - ['id' => 1], - ['id' => 2], - ['id' => 3], - ], - ], - ]; - - $this->assertEquals( - '[0][1][2][3]', - $this->renderString('{{ source.entries }}[{{ id }}]{{ /source.entries }}', $vars) - ); - - $this->assertEquals( - '[0][1][2][3]', - $this->renderString('{{ source[key] }}[{{ id }}]{{ /source[key] }}', $vars) - ); - - $this->assertEquals( - '[0][1][2][3]', - $this->renderString('{{ source.entries sort="id" }}[{{ id }}]{{ /source.entries }}', $vars) - ); - - $this->assertEquals( - '[0][1][2][3]', - $this->renderString('{{ source[key] sort="id" }}[{{ id }}]{{ /source[key] }}', $vars) - ); - - $this->assertEquals( - '[3][2][1][0]', - $this->renderString('{{ source[key] sort="id:desc" }}[{{ id }}]{{ /source[key] }}', $vars) - ); - } - - /** @test */ - public function modifiers_on_tag_pairs_receive_the_augmented_value() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - $value[1]['type'] = 'yup'; - - return $value; - } - }; - - $value = new Value([ - ['type' => 'yup', 'text' => '1'], - ['type' => 'nope', 'text' => '2'], - ['type' => 'yup', 'text' => '3'], - ], 'test', $fieldtype); - - // unaugmented, the second item would be filtered out. - // augmenting changes the second item to a yup, so it should be included. - $this->assertEquals('123', $this->renderString('{{ test where="type:yup" }}{{ text }}{{ /test }}', [ - 'test' => $value, - 'hello' => 'there', - ])); - } - - /** @test */ - public function it_outputs_the_value_when_a_ArrayableString_object_is_used_as_string() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - return new ArrayableString('world', ['label' => 'World']); - } - }; - - $value = new Value('world', 'hello', $fieldtype); - - $this->assertEquals('world', $this->renderString('{{ hello }}', [ - 'hello' => $value, - ])); - } - - /** @test */ - public function it_can_treat_a_ArrayableString_object_as_an_array() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - return new ArrayableString('world', ['label' => 'World']); - } - }; - - $value = new Value('world', 'hello', $fieldtype); - - $this->assertEquals( - 'world, World', - $this->renderString('{{ hello }}{{ value }}, {{ label }}{{ /hello }}', [ - 'hello' => $value, - ]) - ); - } - - /** @test */ - public function it_can_access_ArrayableString_properties_by_colon_notation() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - return new ArrayableString('world', ['label' => 'World']); - } - }; - - $value = new Value('world', 'hello', $fieldtype); - - $vars = ['hello' => $value]; - - $this->assertEquals('world', $this->renderString('{{ hello:value }}', $vars)); - $this->assertEquals('World', $this->renderString('{{ hello:label }}', $vars)); - } - - /** @test */ - public function it_can_use_ArrayableString_objects_in_conditions() - { - $fieldtype = new class extends Fieldtype - { - public function augment($value) - { - $label = is_null($value) ? null : strtoupper($value); - - return new ArrayableString($value, ['label' => $label]); - } - }; - - $vars = [ - 'string' => new Value('foo', 'string', $fieldtype), - 'nully' => new Value(null, 'nully', $fieldtype), - ]; - - $this->assertEquals('true', $this->renderString('{{ if string }}true{{ else }}false{{ /if }}', $vars)); - $this->assertEquals('false', $this->renderString('{{ if nully }}true{{ else }}false{{ /if }}', $vars)); - - $this->assertEquals('true', $this->renderString('{{ if string == "foo" }}true{{ else }}false{{ /if }}', $vars)); - $this->assertEquals('false', $this->renderString('{{ if nully == "foo" }}true{{ else }}false{{ /if }}', $vars)); - $this->assertEquals('false', $this->renderString('{{ if string == "bar" }}true{{ else }}false{{ /if }}', $vars)); - $this->assertEquals('false', $this->renderString('{{ if nully == "bar" }}true{{ else }}false{{ /if }}', $vars)); - - $this->assertEquals('true', $this->renderString('{{ string ? "true" : "false" }}', $vars)); - $this->assertEquals('false', $this->renderString('{{ nully ? "true" : "false" }}', $vars)); - - $this->assertEquals('true', $this->renderString('{{ string == "foo" ? "true" : "false" }}', $vars)); - $this->assertEquals('false', $this->renderString('{{ string == "bar" ? "true" : "false" }}', $vars)); - - $this->assertEquals('foo', $this->renderString('{{ string or "fallback" }}', $vars)); - $this->assertEquals('FOO', $this->renderString('{{ string:label or "fallback" }}', $vars)); - $this->assertEquals('fallback', $this->renderString('{{ nully or "fallback" }}', $vars)); - $this->assertEquals('fallback', $this->renderString('{{ nully:label or "fallback" }}', $vars)); - - $this->assertEquals('foo', $this->renderString('{{ string ?? "fallback" }}', $vars)); - $this->assertEquals('FOO', $this->renderString('{{ string:label ?? "fallback" }}', $vars)); - $this->assertEquals('fallback', $this->renderString('{{ nully ?? "fallback" }}', $vars)); - $this->assertEquals('fallback', $this->renderString('{{ nully:label ?? "fallback" }}', $vars)); - - $this->assertEquals('fallback', $this->renderString('{{ string ?= "fallback" }}', $vars)); - $this->assertEquals('fallback', $this->renderString('{{ string:label ?= "fallback" }}', $vars)); - $this->assertEquals('', $this->renderString('{{ nully ?= "fallback" }}', $vars)); - $this->assertEquals('', $this->renderString('{{ nully:label ?= "fallback" }}', $vars)); - } - - /** @test */ - public function it_can_remove_escaping_characters_from_tenary_output() - { - $vars = [ - 'seo_title' => "Let's work together", - 'title' => 'Contact', - - 'local_office_link' => '', - 'head_office_link' => 'https://statamic.com', - ]; - - $this->assertEquals("Let's work together", $this->renderString('{{ seo_title ? seo_title : title }}', $vars)); - $this->assertEquals('Contact', $this->renderString('{{ title ? title : seo_title }}', $vars)); - - $this->assertEquals('https://statamic.com', $this->renderString('{{ local_office_link ? local_office_link : head_office_link }}', $vars)); - $this->assertEquals('https://statamic.com', $this->renderString('{{ head_office_link ? head_office_link : local_office_link }}', $vars)); - } - - /** @test */ - public function it_can_remove_escaping_characters_from_tenary_output_with_truth_coalescence() - { - $vars = [ - 'truthy' => true, - 'string' => "Let's work together", - 'link' => 'https://statamic.com', - ]; - - $this->assertEquals("Let's work together", $this->renderString('{{ truthy ?= string }}', $vars)); - $this->assertEquals('https://statamic.com', $this->renderString('{{ truthy ?= link }}', $vars)); - } - - /** @test */ - public function empty_collections_are_considered_empty_in_conditions() - { - $template = '{{ if stuff }}yes{{ else }}no{{ /if }}'; - $this->assertEquals('no', $this->renderString($template, ['stuff' => collect()])); - $this->assertEquals('yes', $this->renderString($template, ['stuff' => collect(['one'])])); - } - - /** @test */ - public function empty_view_error_bags_are_considered_empty_in_conditions() - { - $template = '{{ if errors}}yes{{ else }}no{{ /if }}'; - $viewErrorBag = new ViewErrorBag; - - $this->assertEquals('no', $this->renderString($template, ['errors' => $viewErrorBag])); - $this->assertEquals('yes', $this->renderString($template, ['errors' => $viewErrorBag->put('default', new MessageBag)])); - $this->assertEquals('yes', $this->renderString($template, ['errors' => $viewErrorBag->put('form.contact', new MessageBag)])); - } - - /** @test */ - public function objects_are_considered_truthy() - { - $this->assertEquals('yes', $this->renderString('{{ if object }}yes{{ else }}no{{ /if }}', [ - 'object' => new \stdClass(), - ])); - } - - /** @test */ - public function parameter_style_modifier_with_colon_prefix_will_get_the_values_from_context() - { - $this->assertEquals('Tes Te', $this->renderString('{{ word :backspace="one" }} {{ word :backspace="two" }}', [ - 'word' => 'Test', - 'one' => 1, - 'two' => 2, - ])); - } - - /** @test */ - public function variables_starting_with_if_arent_treated_as_if_statements() - { - $this->assertEquals('test', $this->renderString('{{ iframe }}', ['iframe' => 'test'])); - $this->assertEquals('test', $this->renderString('{{ unlesses }}', ['unlesses' => 'test'])); - $this->assertEquals('test', $this->renderString('{{ elseifs }}', ['elseifs' => 'test'])); - $this->assertEquals('test', $this->renderString('{{ elseunlessses }}', ['elseunlessses' => 'test'])); - } - - /** @test */ - public function when_a_loop_is_a_value_object_with_an_empty_array_it_get_parsed_as_one() - { - $template = <<<'EOT' -before -{{ simple }} - {{ foo }} -{{ /simple }} -after -EOT; - - $expected = <<<'EOT' -before - -after -EOT; - $this->assertEquals($expected, $this->renderString($template, [ - 'simple' => new Value([], null, new class extends \Statamic\Fieldtypes\Replicator - { - }), - ])); - } - - /** @test */ - public function it_automatically_augments_augmentable_objects_when_looping_with_modifier() - { - $loop = [ - new AugmentableObject(['one' => 'foo', 'two' => 'bar']), - new AugmentableObject(['one' => 'baz', 'two' => 'qux']), - ]; - - $this->assertEquals( - '', - (string) $this->renderString('{{ augmentables limit="1" }}<{{ one }}><{{ two }}>{{ /augmentables }}', ['augmentables' => $loop]) - ); - } - - /** @test */ - public function it_uses_tags_with_single_part_in_conditions() - { - (new class extends Tags - { - public static $handle = 'truthy'; - - public function index() - { - return true; - } - })::register(); - - (new class extends Tags - { - public static $handle = 'falsey'; - - public function index() - { - return false; - } - })::register(); - - $this->assertEquals('yes', $this->renderString('{{ if {truthy} }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('yes', $this->renderString('{{ if {truthy} == true }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('no', $this->renderString('{{ if {truthy} == false }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('no', $this->renderString('{{ if {falsey} }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('no', $this->renderString('{{ if {falsey} == true }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('yes', $this->renderString('{{ if {falsey} == false }}yes{{ else }}no{{ /if }}', [], true)); - } - - /** @test */ - public function it_uses_tags_with_multiple_parts_in_conditions() - { - (new class extends Tags - { - public static $handle = 'truthy'; - - public function test() - { - return true; - } - })::register(); - - (new class extends Tags - { - public static $handle = 'falsey'; - - public function test() - { - return false; - } - })::register(); - - $this->assertEquals('yes', $this->renderString('{{ if {truthy:test} }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('yes', $this->renderString('{{ if {truthy:test} == true }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('no', $this->renderString('{{ if {truthy:test} == false }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('no', $this->renderString('{{ if {falsey:test} }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('no', $this->renderString('{{ if {falsey:test} == true }}yes{{ else }}no{{ /if }}', [], true)); - $this->assertEquals('yes', $this->renderString('{{ if {falsey:test} == false }}yes{{ else }}no{{ /if }}', [], true)); - } - - /** @test */ - public function it_does_stuff_in_issue_2537() - { - $template = '{{ if noindex || segment_1 == "mobile" || get:page > 0 }}yes{{ else }}no{{ /if }}'; - - $this->assertEquals('yes', $this->renderString($template, ['noindex' => true])); - } - - /** @test */ - public function it_does_stuff_in_issue_2456() - { - $template = '{{ if publication_venue:publication_venue_types:slug !== "journal" and publication_venue:first_year }}yes{{ else }}no{{ /if }}'; - - $this->assertEquals('yes', $this->renderString($template, [ - 'publication_venue' => [ - 'first_year' => true, - 'publication_venue_types' => [ - 'slug' => 'notjournal', - ], - ], - ])); - } - - /** @test */ - public function it_compares_to_a_string_that_looks_like_array_access() - { - $template = '{{ if test == "price:desc" }}yes{{ else }}no{{ /if }}'; - - $this->assertEquals('yes', $this->renderString($template, [ - 'test' => 'price:desc', - ])); - } - - /** - * @test - * - * @see https://github.com/statamic/cms/issues/3374 - **/ - /** @test */ - public function it_parses_single_and_tag_pairs_with_modifiers() - { - $data = ['items' => ['one', 'two', 'three']]; - - $this->assertEquals('3', $this->renderString('{{ items limit="2" }}<{{ value }}>{{ /items }}{{ items | count }}', $data)); - $this->assertEquals('3', $this->renderString('{{ items | count }}{{ items limit="2" }}<{{ value }}>{{ /items }}', $data)); - } - - /** @test */ - public function it_passes_along_query_builder_values_to_the_query_tag() - { - $builder = Mockery::mock(Builder::class); - $builder->shouldReceive('get')->once()->andReturn(collect([ - ['title' => 'Foo'], - ['title' => 'Bar'], - ])); - - $this->assertEquals('', $this->renderString('{{ my_query }}<{{ title }}>{{ /my_query }}', [ - 'my_query' => $builder, - ])); - } - - /** @test */ - public function it_passes_along_query_builder_augmented_values_to_the_query_tag() - { - $builder = Mockery::mock(Builder::class); - $builder->shouldReceive('get')->once()->andReturn(collect([ - ['title' => 'Foo'], - ['title' => 'Bar'], - ])); - - $this->assertEquals('', $this->renderString('{{ my_query }}<{{ title }}>{{ /my_query }}', [ - 'my_query' => new Value($builder), - ])); - } - - /** @test */ - public function it_can_reach_into_query_builders() - { - $builder = Mockery::mock(Builder::class); - $builder->shouldReceive('get')->times(2)->andReturn(collect([ - ['title' => 'Foo'], - ['title' => 'Bar'], - ])); - - $this->assertEquals('', $this->renderString('<{{ my_query:1:title }}><{{ my_query:0:title }}>', [ - 'my_query' => $builder, - ])); - } - - /** @test */ - public function it_can_reach_into_query_builders_through_values() - { - $builder = Mockery::mock(Builder::class); - $builder->shouldReceive('get')->times(2)->andReturn(collect([ - ['title' => 'Foo'], - ['title' => 'Bar'], - ])); - - $this->assertEquals('', $this->renderString('<{{ my_query:1:title }}><{{ my_query:0:title }}>', [ - 'my_query' => new Value($builder), - ])); - } - - /** @test */ - public function it_can_get_nested_query_builders() - { - $builder = Mockery::mock(Builder::class); - $builder->shouldReceive('get')->andReturn(collect([ - ['title' => 'Foo'], - ['title' => 'Bar'], - ])); - - $this->assertEquals('', $this->renderString('{{ nested:my_query }}<{{ title }}>{{ /nested:my_query }}', [ - 'nested' => [ - 'my_query' => $builder, - ], - ])); - } - - /** @test */ - public function it_loops_over_values_instances() - { - $this->assertEquals('', $this->renderString('{{ grid }}<{{ foo }}><{{ bar }}>{{ /grid }}', [ - 'grid' => [ - new Values(['foo' => 'alfa', 'bar' => 'bravo']), - new Values(['foo' => 'charlie', 'bar' => 'delta']), - ], - ])); - } - - /** - * @test - * - * @dataProvider objectInConditionProvider - */ - public function it_uses_entries_as_conditions($object) - { - $this->assertEquals('yes', $this->renderString('{{ if the_field }}yes{{ else }}no{{ /if }}', [ - 'the_field' => $object, - ])); - } - - public function objectInConditionProvider() - { - return [ - 'with __toString' => [new class() - { - public function __toString() - { - return 'foo'; - } - }, ], - 'with __call' => [new class() - { - public function __call($method, $args) - { - return 'foo'; - } - }, ], - 'without __call or __toString' => [new class() - { - // - }, ], - ]; - } - - private function assertEqualsWithCollapsedNewlines($expected, $actual) - { - $expected = trim($expected); - $actual = trim($actual); - - // replace all newlines with a single newline - $expected = preg_replace('/\n+/', "\n", $expected); - $actual = preg_replace('/\n+/', "\n", $actual); - - $this->assertEquals($expected, $actual); - } -} - -class NonArrayableObject -{ - protected $data; - - public function __construct($data) - { - $this->data = $data; - } -} - -class ArrayableObject extends NonArrayableObject implements Arrayable -{ - public function toArray() - { - return $this->data; - } -} - -class AugmentableObject extends ArrayableObject implements Augmentable -{ - use HasAugmentedData; - - public function augmentedArrayData() - { - return $this->data; - } - - public function blueprint() - { - FieldtypeRepository::shouldReceive('find')->andReturn(new class extends Fieldtype - { - public function augment($data) - { - return strtoupper($data).'!'; - } - }); - - return (new Blueprint)->setContents(['fields' => [ - ['handle' => 'one', 'field' => ['type' => 'test']], - ]]); - } -} diff --git a/tests/View/Antlers/RegexParserEngineTest.php b/tests/View/Antlers/RegexParserEngineTest.php deleted file mode 100644 index 1ab2663ec81..00000000000 --- a/tests/View/Antlers/RegexParserEngineTest.php +++ /dev/null @@ -1,39 +0,0 @@ -config->set('statamic.antlers.version', 'regex'); - } - - /** @test */ - public function it_can_prevent_injecting_noparse_extractions() - { - $this->files - ->shouldReceive('get') - ->with('/path/to/foo.antlers.html') - ->andReturn('Hello {{ foo }} {{ noparse }}{{ bar }}{{ /noparse }} {{ bar }}'); - - $this->assertEquals( - 'Hello World noparse_8e726b27d1e6ef37447e1aa0aaa30932 Bar', - $this->engine - ->withoutExtractions() - ->get('/path/to/foo.antlers.html', ['foo' => 'World', 'bar' => 'Bar']) - ); - - // Proof that the prevention of injecting extractions only happens once. - $this->assertEquals( - 'Hello World {{ bar }} Bar', - $this->engine->get('/path/to/foo.antlers.html', ['foo' => 'World', 'bar' => 'Bar']) - ); - } -} diff --git a/tests/View/Antlers/RuntimeParserEngineTest.php b/tests/View/Antlers/RuntimeParserEngineTest.php index 4c1e41ec762..e12f0027f31 100644 --- a/tests/View/Antlers/RuntimeParserEngineTest.php +++ b/tests/View/Antlers/RuntimeParserEngineTest.php @@ -2,16 +2,80 @@ namespace Tests\View\Antlers; +use Illuminate\Filesystem\Filesystem; +use Mockery; +use Statamic\Contracts\View\Antlers\Parser; +use Statamic\View\Antlers\Engine; use Tests\TestCase; class RuntimeParserEngineTest extends TestCase { - use EngineTests; + private $engine; + private $files; - protected function resolveApplicationConfiguration($app) + public function setUp(): void { - parent::resolveApplicationConfiguration($app); + parent::setUp(); - $app->config->set('statamic.antlers.version', 'runtime'); + $this->engine = new Engine( + $this->files = Mockery::mock(Filesystem::class), + app(Parser::class) + ); + } + + /** @test */ + public function parses_a_basic_template() + { + $this->files + ->shouldReceive('get') + ->with('/path/to/foo.antlers.html') + ->andReturn('Hello {{ foo }}'); + + $this->assertEquals( + 'Hello World', + $this->engine->get('/path/to/foo.antlers.html', ['foo' => 'World']) + ); + } + + /** @test */ + public function parses_a_template_with_noparse_tags() + { + $this->files + ->shouldReceive('get') + ->with('/path/to/foo.antlers.html') + ->andReturn('Hello {{ foo }} {{ noparse }}{{ bar }}{{ /noparse }} {{ bar }}'); + + $this->assertEquals( + 'Hello World {{ bar }} Bar', + $this->engine->get('/path/to/foo.antlers.html', ['foo' => 'World', 'bar' => 'Bar']) + ); + } + + /** @test */ + public function php_is_not_executed_if_the_filename_is_html() + { + $this->files + ->shouldReceive('get') + ->with('/path/to/foo.antlers.html') + ->andReturn('Hello '); + + $this->assertEquals( + 'Hello <?php echo "World"; ?>', + $this->engine->get('/path/to/foo.antlers.html', ['foo' => 'World']) + ); + } + + /** @test */ + public function php_is_executed_if_the_filename_is_php() + { + $this->files + ->shouldReceive('get') + ->with('/path/to/foo.antlers.php') + ->andReturn('Hello '); + + $this->assertEquals( + 'Hello World', + $this->engine->get('/path/to/foo.antlers.php', ['foo' => 'World']) + ); } }