diff --git a/plugins/auto_trim.ts b/plugins/auto_trim.ts index 5c68b05..86db139 100644 --- a/plugins/auto_trim.ts +++ b/plugins/auto_trim.ts @@ -5,6 +5,8 @@ export const defaultTags = [ ">", "set", "/set", + "default", + "/default", "if", "/if", "else", diff --git a/plugins/default.ts b/plugins/default.ts new file mode 100644 index 0000000..fac7715 --- /dev/null +++ b/plugins/default.ts @@ -0,0 +1,56 @@ +import { SourceError } from "../core/errors.ts"; +import type { Token } from "../core/tokenizer.ts"; +import type { Environment, Plugin } from "../core/environment.ts"; + +export default function (): Plugin { + return (env: Environment) => { + env.tags.push(defaultTag); + }; +} + +const VARNAME = /^[a-zA-Z_$][\w$]*$/; +const VALID_TAG = /^([a-zA-Z_$][\w$]*)\s*=\s*([^]+)$/; + +function defaultTag( + env: Environment, + token: Token, + _output: string, + tokens: Token[], +): string | undefined { + const [, code, position] = token; + const { dataVarname } = env.options; + + if (!code.startsWith("default ")) { + return; + } + + const expression = code.replace("default", "").trim(); + // Setting a value (e.g. {{ default foo = "bar" }} + if (expression.includes("=")) { + const match = expression.match(VALID_TAG); + if (!match) { + throw new SourceError("Invalid default tag", position); + } + const variable = match[1]; + const value = env.compileFilters(tokens, match[2]); + return ` + if (typeof ${variable} == "undefined" || ${variable} === null) { + var ${variable} = ${dataVarname}["${variable}"] = ${value}; + } + `; + } + + // Capture a value (e.g. {{ default foo }}bar{{ /default }} + if (!VARNAME.test(expression)) { + throw new SourceError("Invalid default tag", position); + } + const subvarName = `${dataVarname}["${expression}"]`; + const compiledFilters = env.compileFilters(tokens, subvarName); + return ` + if (typeof ${expression} == "undefined" || ${expression} === null) { + ${subvarName} = ""; + ${env.compileTokens(tokens, subvarName, "/default").join("")} + var ${expression} = ${subvarName} = ${compiledFilters}; + } + `; +} diff --git a/plugins/mod.ts b/plugins/mod.ts index 5774531..0701937 100644 --- a/plugins/mod.ts +++ b/plugins/mod.ts @@ -4,6 +4,7 @@ import ifTag from "./if.ts"; import forTag from "./for.ts"; import includeTag from "./include.ts"; import setTag from "./set.ts"; +import defaultTag from "./default.ts"; import jsTag from "./js.ts"; import layoutTag from "./layout.ts"; import functionTag from "./function.ts"; @@ -21,6 +22,7 @@ export default function (): Plugin { env.use(jsTag()); env.use(includeTag()); env.use(setTag()); + env.use(defaultTag()); env.use(layoutTag()); env.use(functionTag()); env.use(importTag()); diff --git a/test/default.test.ts b/test/default.test.ts new file mode 100644 index 0000000..3b48b85 --- /dev/null +++ b/test/default.test.ts @@ -0,0 +1,61 @@ +import { test } from "./utils.ts"; + +Deno.test("Default tag", async () => { + await test({ + template: ` + {{ set greeting = "Hello" }} + {{ default message = "Hi" }} + {{ default target = "world" }} + {{ greeting }} {{ target }} + `, + expected: "Hello world", + }); + + await test({ + template: ` + {{ default message -}} + Hi + {{- /default }} + {{ default target -}} + world + {{- /default }} + {{ greeting }} {{ target }} + `, + expected: "Hello world", + data: { + greeting: "Hello", + target: null, + }, + }); +}); + +Deno.test("Default tag in strict mode", async () => { + await test({ + options: { strict: true }, + template: ` + {{ set greeting = "Hello" }} + {{ default message = "Hi" }} + {{ default target = "world" }} + {{ greeting }} {{ target }} + `, + expected: "Hello world", + }); + + await test({ + options: { strict: true }, + template: ` + {{ default message -}} + Hi + {{- /default }} + {{ default target -}} + world + {{- /default }} + {{ greeting }} {{ target }} + `, + expected: "Hello world", + data: { + greeting: "Hello", + target: null, + }, + }); +});