From 6033ca5846375fcd0544100edcd24db489567429 Mon Sep 17 00:00:00 2001 From: vrugtehagel Date: Thu, 11 Dec 2025 13:44:34 +0100 Subject: [PATCH 1/3] Allow direct data for include and layout tags Allows passing data as a variable, rather than an object literal, both for the `{{ include }}` tag and `{{ layout }}` tag. Since paths can include JavaScript themselves, as in, for example, `{{ include "./template" + ext }}`, this requires a bit of care. This solution decides whether the last "word" in the tag is a data argument by looking at what is in front of it; if it is a quote character of any kind, or a word character, the last name token is a data variable. Naturally, there are edge cases. Currently this does not allow for something like `data.foo` or `data[0]`, but presumably can be extended to do so. --- plugins/include.ts | 9 +++++++++ plugins/layout.ts | 29 ++++++++++++++++++++++++----- test/include.test.ts | 29 +++++++++++++++++++++++++++++ test/layout.test.ts | 23 +++++++++++++++++++++++ 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/plugins/include.ts b/plugins/include.ts index 06d3d3c..0e2e770 100644 --- a/plugins/include.ts +++ b/plugins/include.ts @@ -9,6 +9,8 @@ export default function (): Plugin { }; } +const DIRECT_DATA = /["'`\w]\s+([a-z_$][\w$]*)$/i; + function includeTag( env: Environment, token: Token, @@ -37,6 +39,13 @@ function includeTag( data = tagCode.slice(bracketIndex).trim(); } + // Includes data directly (e.g. {{ include "template.vto" data }}) + const directDataMatch = tagCode.match(DIRECT_DATA); + if (directDataMatch) { + data = directDataMatch[1]; + file = tagCode.slice(0, -data.length).trim(); + } + const { dataVarname } = env.options; const tmp = env.getTempVariable(); return `{ diff --git a/plugins/layout.ts b/plugins/layout.ts index 99e1e17..871aaa9 100644 --- a/plugins/layout.ts +++ b/plugins/layout.ts @@ -1,4 +1,5 @@ import { SourceError } from "../core/errors.ts"; +import iterateTopLevel from "../core/js.ts"; import type { Token } from "../core/tokenizer.ts"; import type { Environment, Plugin } from "../core/environment.ts"; @@ -11,6 +12,7 @@ export default function (): Plugin { const LAYOUT_TAG = /^layout\s+([^{]+|`[^`]+`)+(?:\{([^]*)\})?$/; const SLOT_NAME = /^[a-z_]\w*$/i; +const DIRECT_DATA = /["'`\w]\s+([a-z_$][\w$]*)$/i; function layoutTag( env: Environment, @@ -24,12 +26,29 @@ function layoutTag( return; } - const match = code?.match(LAYOUT_TAG); - if (!match) { - throw new SourceError("Invalid layout tag", position); + const tagCode = code.slice(6).trim(); + let file = tagCode; + let data = ""; + + // Includes { data } + if (tagCode.endsWith("}")) { + let bracketIndex = -1; + for (const [index, reason] of iterateTopLevel(tagCode)) { + if (reason == "{") bracketIndex = index; + } + if (bracketIndex == -1) { + throw new SourceError("Invalid layout tag", position); + } + file = tagCode.slice(0, bracketIndex).trim(); + data = tagCode.slice(bracketIndex).trim(); } - const [_, file, data] = match; + // Includes data directly (e.g. {{ layout "template.vto" data }}) + const directDataMatch = tagCode.match(DIRECT_DATA); + if (directDataMatch) { + data = directDataMatch[1]; + file = tagCode.slice(0, -data.length).trim(); + } const compiledFilters = env.compileFilters(tokens, "__slots.content"); const { dataVarname } = env.options; @@ -40,7 +59,7 @@ function layoutTag( return __env.run(${file}, { ...${dataVarname}, ...__slots, - ${data ?? ""} + ${data ? "..." + data : ""} }, __template.path, ${position}); })()).content;`; } diff --git a/test/include.test.ts b/test/include.test.ts index 04b0f70..c4abf59 100644 --- a/test/include.test.ts +++ b/test/include.test.ts @@ -102,6 +102,35 @@ Deno.test("Include tag (with data)", async () => { name: "world", }, }); + + await test({ + template: ` + {{ set data = { name } }} + {{ include "/my-file.vto" data }} + `, + expected: "Hello world", + includes: { + "/my-file.vto": "Hello {{ name }}", + }, + data: { + name: "world", + }, + }); + + await test({ + template: ` + {{ set data = { name } }} + {{ include "/my-file" + ext data }} + `, + expected: "Hello world", + includes: { + "/my-file.vto": "Hello {{ name }}", + }, + data: { + ext: ".vto", + name: "world", + }, + }); }); Deno.test("Include tag (with custom data)", async () => { diff --git a/test/layout.test.ts b/test/layout.test.ts index 276f5b7..3d6cba4 100644 --- a/test/layout.test.ts +++ b/test/layout.test.ts @@ -45,6 +45,29 @@ Deno.test("Layout tag (with extra data)", async () => { "/my-file.vto": "<{{ tag }}>{{ content }}", }, }); + await test({ + template: ` + {{ set data = { tag: "h1" } }} + {{ layout "/my-file.vto" data }}Hello world{{ /layout }} + `, + expected: "

Hello world

", + includes: { + "/my-file.vto": "<{{ tag }}>{{ content }}", + }, + }); + await test({ + template: ` + {{ set data = { tag: "h1" } }} + {{ layout "/my-file" + ext data }}Hello world{{ /layout }} + `, + expected: "

Hello world

", + includes: { + "/my-file.vto": "<{{ tag }}>{{ content }}", + }, + data: { + ext: ".vto", + }, + }); }); Deno.test("Layout tag (with extra data and filters)", async () => { From 46bd12de56bc334cadbe1779df57eca0f9b61612 Mon Sep 17 00:00:00 2001 From: vrugtehagel Date: Thu, 11 Dec 2025 13:52:11 +0100 Subject: [PATCH 2/3] Remove unused variable --- plugins/layout.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/layout.ts b/plugins/layout.ts index 871aaa9..42247c1 100644 --- a/plugins/layout.ts +++ b/plugins/layout.ts @@ -10,7 +10,6 @@ export default function (): Plugin { }; } -const LAYOUT_TAG = /^layout\s+([^{]+|`[^`]+`)+(?:\{([^]*)\})?$/; const SLOT_NAME = /^[a-z_]\w*$/i; const DIRECT_DATA = /["'`\w]\s+([a-z_$][\w$]*)$/i; From a9edaea70b356e7955cd62246ff0476b6683c8dc Mon Sep 17 00:00:00 2001 From: vrugtehagel Date: Fri, 12 Dec 2025 13:13:05 +0100 Subject: [PATCH 3/3] Broaden scope for regular expression This allows not only variable-like tokens as "direct data", but anything not involving spaces or quotes. Unfortunately disallowing quotes is necessary given strings can contain anything, and data passed as object literal often contains strings. For example, `{foo: "bar baz"}` could end up with the regular expression matching the `baz"}` part, which is, of course, problematic. --- plugins/include.ts | 2 +- plugins/layout.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/include.ts b/plugins/include.ts index 0e2e770..ab0cc28 100644 --- a/plugins/include.ts +++ b/plugins/include.ts @@ -9,7 +9,7 @@ export default function (): Plugin { }; } -const DIRECT_DATA = /["'`\w]\s+([a-z_$][\w$]*)$/i; +const DIRECT_DATA = /["'`\w]\s+([a-z_$][^\s'"`]*)$/i; function includeTag( env: Environment, diff --git a/plugins/layout.ts b/plugins/layout.ts index 42247c1..accbf38 100644 --- a/plugins/layout.ts +++ b/plugins/layout.ts @@ -11,7 +11,7 @@ export default function (): Plugin { } const SLOT_NAME = /^[a-z_]\w*$/i; -const DIRECT_DATA = /["'`\w]\s+([a-z_$][\w$]*)$/i; +const DIRECT_DATA = /["'`\w]\s+([a-z_$][^\s'"`]*)$/i; function layoutTag( env: Environment,