diff --git a/app/Markdown/StripEnvTrailingNewlines.php b/app/Markdown/StripEnvTrailingNewlines.php index 5a451eafe..e3c1b6e7b 100644 --- a/app/Markdown/StripEnvTrailingNewlines.php +++ b/app/Markdown/StripEnvTrailingNewlines.php @@ -8,6 +8,11 @@ class StripEnvTrailingNewlines implements Preprocessor { + public function supports(?string $grammarName): bool + { + return $grammarName === 'env'; + } + /** * Torchlight's env grammar uses `([^#]*)` for unquoted values, which * swallows the newline Phiki appends to every line. That leaves diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ec9fdeb32..b41a5dc8b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -12,12 +12,14 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; use League\CommonMark\Extension\Attributes\AttributesExtension; +use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode; use League\CommonMark\Extension\DescriptionList\DescriptionListExtension; use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension; use Statamic\Facades\Collection; use Statamic\Facades\Markdown; use Stillat\DocumentationSearch\Events\SearchEntriesCreated; -use Torchlight\Engine\CommonMark\Extension as TorchlightExtension; +use Torchlight\Engine\CommonMark\CodeBlockRenderer; +use Torchlight\Engine\Engine; use Torchlight\Engine\Options as TorchlightOptions; class AppServiceProvider extends ServiceProvider @@ -42,16 +44,14 @@ public function boot(): void if (! app()->runningConsoleCommand('search:update')) { TorchlightOptions::setDefaultOptionsBuilder(fn () => TorchlightOptions::fromArray(config('torchlight.options'))); - $extension = new TorchlightExtension( - config('torchlight.theme'), - true, - ['env' => new StripEnvTrailingNewlines], - ); - $extension - ->renderer() + $engine = new Engine; + $engine->registerPreprocessor(new StripEnvTrailingNewlines, 'env'); + $engine->getEnvironment()->grammar('antlers', resource_path('syntaxes/antlers.json')); + + $renderer = (new CodeBlockRenderer(config('torchlight.theme'), $engine)) ->setDefaultGrammar(config('torchlight.options.defaultLanguage')); - Markdown::addExtension(fn () => $extension); + Markdown::addRenderer(fn () => [FencedCode::class, $renderer, 10]); } Event::listen(SearchEntriesCreated::class, SearchEntriesCreatedListener::class); diff --git a/content/collections/pages/antlers-cheat-sheet.md b/content/collections/pages/antlers-cheat-sheet.md index 75659a999..4a8addfff 100644 --- a/content/collections/pages/antlers-cheat-sheet.md +++ b/content/collections/pages/antlers-cheat-sheet.md @@ -253,6 +253,8 @@ The one-liners: | Include a view | `{{ partial:footer }}` | | Include one that might not exist | `{{ partial:if_exists src="blog/card" }}` | | Pass data to a partial | `{{ partial:blog/card mode="stacked" }}` | +| Render a component | `` | +| Pass a variable to a component | `` | | Render everything pushed onto a stack | `{{ stack:scripts }}` | Some helpful things you may need when building your frontend: @@ -295,7 +297,7 @@ Some helpful things you may need when building your frontend: {{ /section:footer }} ``` -More: [partials](/frontend/antlers#partials), [slots](/frontend/antlers#slots), [stacks](/frontend/antlers#stacks), [section & yield](/frontend/antlers#section--yield). +More: [partials](/frontend/antlers#partials), [partial slots](/frontend/antlers#slots), [components](/frontend/antlers-components), [stacks](/frontend/antlers#stacks), [section & yield](/frontend/antlers#section--yield). ## Escaping and preventing parsing diff --git a/content/collections/pages/antlers-components.md b/content/collections/pages/antlers-components.md new file mode 100644 index 000000000..e9fc22ca1 --- /dev/null +++ b/content/collections/pages/antlers-components.md @@ -0,0 +1,245 @@ +--- +id: 083c717b-545c-45be-a65b-67faf1aa886f +blueprint: page +title: 'Antlers Components' +intro: 'Build reusable components with Antlers, render existing Laravel Blade components, and use HTML-style syntax for Statamic Tags.' +related_entries: + - d37b2af2-f2bf-493a-9345-7087fb5929ce + - 0c54fe7c-c87a-4812-b76e-48f16cf08e0d + - c7816387-ebc4-4204-b5f2-8e7073a4db8b +--- +## Overview + +Components are reusable, self-contained chunks of interface, such as callouts, cards, buttons, layouts, or whatever else you keep copying and pasting around your site. Antlers can render [Laravel Blade components](https://laravel.com/docs/13.x/blade#components), and anonymous components can be written with Antlers itself. + +That gives you two closely related flavors of angle-bracket syntax: + +| Syntax | What it renders | +| --- | --- | +| `` | A Laravel component class or anonymous component view. The view may use Blade or Antlers. | +| `...` | A Statamic [Tag](/tags) using component-style syntax. | +| `{{ collection:blog }}...{{ /collection:blog }}` | The same Statamic Tag using classic Antlers syntax. | + +:::tip +[Partials](/frontend/antlers#partials) inherit the current Antlers scope and are perfect for straightforward includes. Components have their own scope and explicitly receive data through props, the Cascade, and slots. +::: + +## Creating a component + +Anonymous components live in `resources/views/components`. Give the view an `.antlers.html` extension to build it with Antlers: + +```antlers +{{# resources/views/components/callout.antlers.html #}} +@props([ + 'type' => 'info', + 'title' => 'Heads up!' | upper, +]) + + +``` + +Render it from any Antlers template with an `x-` tag. Paired components receive everything between their tags as the default `slot`: + +```antlers + + Save your work before continuing. + +``` + +Components without slot content may self-close: + +```antlers + +``` + +Components in subdirectories use dot notation. For example, `resources/views/components/menu/item.antlers.html` becomes ``. + +:::tip +**[Blade components](https://laravel.com/docs/blade#components) work, too!** Use them from Antlers without rewriting anything. Blade and Antlers components may even nest inside each other like one big, happy template-language family. +::: + +## Props + +The `@props` directive defines the data your component expects. Values with named keys provide defaults, and those defaults are Antlers expressions, not PHP. That means variables, operators, and modifiers like `upper` are all fair game, if that's your style. + +```antlers +@props([ + 'type' => 'info', + 'title' => 'Heads up!' | upper, +]) +``` + +Literal attributes pass strings. Prefix an attribute with `:` to resolve its value from the Antlers scope, or use `:$variable` when the prop and variable share a name: + +```antlers +{{ page_title = 'Back up first' }} +{{ type = 'warning' }} + + +``` + +These are Antlers' [usual parameter rules](/frontend/antlers#tag-parameters), even when the component itself is written in Blade. + +## Attributes + +Attributes not declared as props are collected in the `attributes` bag. Render the bag directly, or call its Laravel methods with Antlers' dot syntax: + +```antlers + +``` + +The `merge` method adds the component's default attributes while preserving those passed by the caller. Class names are combined, so our earlier `class="mt-8"` joins the callout classes instead of booting them out of the club. + +## Slots + +The `slot` variable contains the component's unnamed content. Use the `has_actual_content` modifier when whitespace and HTML comments alone should count as empty: + +```antlers +{{ if slot | has_actual_content }} +
{{ slot }}
+{{ /if }} +``` + +The closely related `is_string` modifier is available when you need to distinguish an ordinary string from a slot object or another value: + +```antlers +{{ if value | is_string }} + {{ value }} +{{ /if }} +``` + +### Named slots + +Use `` to send content to a named slot: + +```antlers + + + Fresh from the blog + + + The latest dispatches from our crew. + +``` + +The component receives the slot as a variable. Any attributes on the slot are available through its own `attributes` bag: + +```antlers +{{# resources/views/components/panel.antlers.html #}} +
+
{{ heading }}
+
{{ slot }}
+
+``` + +## Scope + +A component does not inherit the Antlers variables or Cascade data around it. Variables created inside the component do not leak out, either. Pass values as props when they are part of the component's public API. + +Slot content is the intentional exception. It is evaluated in the caller's scope, so variables available where you invoke the component remain available inside its default and named slots. + +### Cascade data + +Use `@cascade` when a component needs data from Statamic's [Cascade](/data-inheritance). Pass a list to import only the values you need. Values listed without defaults are required; a named key may provide a fallback: + +```antlers +@cascade([ + 'title', + 'eyebrow' => 'Latest', +]) + +

{{ title }}

+

{{ eyebrow }}

+``` + +Omit the arguments to import the entire Cascade: + +```antlers +@cascade +``` + +Pulling in everything is convenient, but selecting values keeps the component's dependencies much easier to spot six months from now. + +### Sharing parent props + +The `@aware` directive lets a nested component consume props explicitly passed to an ancestor component. Here, the menu item picks up the menu's `tone`: + +```antlers +{{# resources/views/components/menu.antlers.html #}} +@props([ + 'tone' => 'light' +]) + + +``` + +```antlers +{{# resources/views/components/menu/item.antlers.html #}} +@aware([ + 'tone' => 'light' +]) + +{{ slot }} +``` + +```antlers + + Docs + +``` + +Like `@props`, the values passed to `@aware` are Antlers expressions. Providing a fallback keeps the nested component useful when it appears outside its usual parent. + +### Escaping directives + +If you need any of these directives to appear as literal text, add another `@`: + +```antlers +@@props(['example']) +@@aware(['example']) +@@cascade +``` + +This renders `@props(['example'])`, `@aware(['example'])`, and `@cascade` without evaluating them. + +## Component-style Statamic Tags + +Statamic Tags may also use HTML-like syntax in Antlers. Prefix the Tag with either `s:` or `statamic:` and otherwise use it as normal: + +```antlers + + {{ title }} + +``` + +This is equivalent to classic Antlers syntax: + +```antlers +{{ collection:pages limit="3" }} + {{ title }} +{{ /collection:pages }} +``` + +Parameters still follow Antlers rules, including dynamic values and shorthand: + +```antlers + + {{ title }} + +``` + +You may use `s-` and `statamic-` prefixes instead if dashes feel more HTML-ish, and Tags without enclosed content may self-close. This syntax still invokes a Statamic Tag; it does not look for a component view in `resources/views/components`. diff --git a/content/collections/pages/antlers.md b/content/collections/pages/antlers.md index 30f6f8b2d..e579efd72 100644 --- a/content/collections/pages/antlers.md +++ b/content/collections/pages/antlers.md @@ -1245,6 +1245,11 @@ Now you can define the context of the named slot using the `slot:name` tag forma {{ /partial:modal }} ``` + +### Components + +Components are reusable, isolated chunks of UI with props, attribute bags, and slots. Antlers can render existing Laravel Blade components, and you can author anonymous components with Antlers itself. Head over to [Antlers Components](/frontend/antlers-components) to learn more. + ### Stacks Antlers allows you to push template code to a "stack" which can be rendered somewhere else in your layout (most commonly) or another view. This can be particularly useful for specifying any JavaScript libraries required by your child views: diff --git a/content/collections/pages/blade.md b/content/collections/pages/blade.md index f968c6b51..603697bc9 100644 --- a/content/collections/pages/blade.md +++ b/content/collections/pages/blade.md @@ -163,7 +163,7 @@ Under the hood, this is syntactic sugar for creating an Antlers partial and does ## Using Antlers Blade components -Despite the name, Antlers Blade Components are a Blade-only feature that allows you to use existing tags inside your Blade templates using a custom tag syntax. For example, you can gather all entries from a "pages" collection using the [collection](/tags/collection) tag like so: +Antlers Blade Components allow you to use existing tags inside your Blade templates with a custom tag syntax. The same component-style syntax also works [inside Antlers templates](/frontend/antlers-components#component-style-statamic-tags). For example, you can gather all entries from a "pages" collection using the [collection](/tags/collection) tag like so: ```blade diff --git a/content/trees/collections/pages.yaml b/content/trees/collections/pages.yaml index dda19b3f3..f0ef6d697 100644 --- a/content/trees/collections/pages.yaml +++ b/content/trees/collections/pages.yaml @@ -174,6 +174,8 @@ tree: entry: d37b2af2-f2bf-493a-9345-7087fb5929ce - entry: 0c54fe7c-c87a-4812-b76e-48f16cf08e0d + - + entry: 083c717b-545c-45be-a65b-67faf1aa886f - entry: c7816387-ebc4-4204-b5f2-8e7073a4db8b - diff --git a/content/trees/navigation/docs.yaml b/content/trees/navigation/docs.yaml index 4447de4f8..7bdd6cf1b 100644 --- a/content/trees/navigation/docs.yaml +++ b/content/trees/navigation/docs.yaml @@ -145,6 +145,9 @@ tree: id: 9d43191e-652b-48e8-8ec5-5cd6266d793d entry: d37b2af2-f2bf-493a-9345-7087fb5929ce title: 'Antlers Templates' + - + id: ff6cfc92-2cb1-49c8-911a-678a58d02e04 + entry: 083c717b-545c-45be-a65b-67faf1aa886f - id: 40ef0b25-0f5e-4523-a527-52a099838d09 entry: c7816387-ebc4-4204-b5f2-8e7073a4db8b diff --git a/resources/syntaxes/antlers.json b/resources/syntaxes/antlers.json new file mode 100644 index 000000000..7375f959b --- /dev/null +++ b/resources/syntaxes/antlers.json @@ -0,0 +1,666 @@ +{ + "comment": "Local override of the Antlers grammar bundled with phiki/phiki for Torchlight.", + "name": "Antlers (Statamic Syntax)", + "fileTypes": [ + "antlers.html", + "antlers.php", + "antlers.xml", + "html", + "htm", + "xhtml" + ], + "scopeName": "text.html.statamic", + "injections": { + "text.html.statamic - (meta.embedded | meta.tag), L:((text.html.statamic meta.tag) - (meta.embedded.block.php | meta.embedded.line.php)), L:(source.js - (meta.embedded.block.php | meta.embedded.line.php)), L:(source.css - (meta.embedded.block.php | meta.embedded.line.php))": { + "patterns": [ + { + "include": "#statamic-comments" + }, + { + "include": "#antlers-tags" + }, + { + "include": "#php-tag" + } + ] + }, + "text.html.statamic - (meta.embedded | meta.tag | comment.block.html | comment.block.statamic), L:(text.html.statamic meta.tag - (comment.block.statamic | meta.embedded.statamic | comment.block.html))": { + "patterns": [ + { + "comment": "This is set to use XHTML standards, but you can change that by changing .strict to .basic for HTML standards", + "include": "text.html.basic" + } + ] + }, + "text.html.statamic": { + "patterns": [ + { + "include": "#antlers-directives" + }, + { + "include": "#statamic-comments" + }, + { + "include": "#antlers-tags" + } + ] + } + }, + "patterns": [ + { + "include": "#antlers-directives" + }, + { + "include": "#php-tag" + }, + { + "include": "#statamic-comments" + }, + { + "include": "#frontMatter" + }, + { + "include": "#antlers-tags" + } + ], + "repository": { + "php-tag": { + "patterns": [ + { + "begin": "<\\?(?i:php|=)?(?![^?]*\\?>)", + "beginCaptures": { + "0": { + "name": "punctuation.section.embedded.begin.statamic" + } + }, + "end": "(\\?)>", + "endCaptures": { + "0": { + "name": "punctuation.section.embedded.end.statamic" + }, + "1": { + "name": "source.php" + } + }, + "name": "meta.embedded.block.statamic", + "contentName": "source.php", + "patterns": [ + { + "include": "source.php" + } + ] + }, + { + "begin": "(?", + "endCaptures": { + "0": { + "name": "punctuation.section.embedded.end.statamic" + }, + "1": { + "name": "source.php" + } + }, + "name": "meta.embedded.line.statamic", + "contentName": "source.php", + "patterns": [ + { + "include": "source.php" + } + ] + } + ] + }, + "frontMatter": { + "begin": "\\A-{3}\\s*$", + "contentName": "meta.embedded.block.frontmatter", + "patterns": [ + { + "include": "source.yaml" + } + ], + "end": "(^|\\G)-{3}|\\.{3}\\s*$" + }, + "statamic-comments": { + "begin": "{{#", + "end": "#}}", + "name": "comment.block.statamic" + }, + "antlers-tags": { + "begin": "(?", + "name": "keyword.operator.key.statamic" + }, + { + "match": "->", + "name": "keyword.operator.class.statamic" + }, + { + "include": "#antlers-attribute-bag" + }, + { + "include": "#statamic-explicit-tags" + }, + { + "include": "#statamic-core-tags" + }, + { + "match": "===|==|!==|!=|<>", + "name": "keyword.operator.comparison.statamic" + }, + { + "match": "\\&=?", + "name": "keyword.operator.string.statamic" + }, + { + "match": "=|\\+=|\\-=|\\*\\*?=|/=|%=|\\|=|\\^=|<<=|>>=", + "name": "keyword.operator.assignment.statamic" + }, + { + "match": "(?|<=|>=|<|>", + "name": "keyword.operator.comparison.statamic" + }, + { + "match": "\\-|\\+|\\*\\*?|/|%", + "name": "keyword.operator.arithmetic.statamic" + }, + { + "begin": "(arr|list|switch)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "support.function.construct.statamic" + }, + "2": { + "name": "punctuation.definition.array.begin.bracket.round.statamic" + } + }, + "end": "\\)|(?=\\?>)", + "endCaptures": { + "0": { + "name": "punctuation.definition.array.end.bracket.round.statamic" + } + }, + "name": "meta.array.statamic", + "patterns": [ + { + "include": "#antlers-expression" + } + ] + }, + { + "begin": "(?