Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/compile-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function compileBody(this: Eta, buff: Array<AstObject>): string {
// we know string exists
returnStr += "__eta.res+='" + str + "';\n";
} else {
const type = currentBlock.t; // "r", "e", or "i"
const type = currentBlock.t; // "r", "e", "i" or custom tag name
let content = currentBlock.val || "";

if (config.debug) returnStr += "__eta.line=" + currentBlock.lineNo + "\n";
Expand Down Expand Up @@ -127,6 +127,8 @@ export function compileBody(this: Eta, buff: Array<AstObject>): string {
} else if (type === "e") {
// execute
returnStr += content + "\n";
} else if (Object.hasOwn(config.customTags, type)) {
returnStr += `__eta.res+=this.config.customTags[${JSON.stringify(type)}](${JSON.stringify(content)},${config.varName});\n`;
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export interface EtaConfig {
/** Holds cache of resolved filepaths. Set to `false` to disable. */
cacheFilepaths: boolean;

/** Object specifying custom tags. Keys are tag prefixes, values are functions which take tag content and return a string. */
customTags: Record<string, (content: string, data: unknown) => string>;

/** Whether to pretty-format error messages (introduces runtime penalties) */
debug: boolean;

Expand Down Expand Up @@ -89,6 +92,7 @@ const defaultConfig: EtaConfig = {
autoTrim: [false, "nl"],
cache: false,
cacheFilepaths: true,
customTags: {},
debug: false,
escapeFunction: XMLEscape,
// default filter function (not used unless enables) just stringifies the input
Expand Down
16 changes: 16 additions & 0 deletions src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ export class Eta {
} else {
this.config = { ...defaultConfig };
}

const reserved = [
this.config.parse.exec,
this.config.parse.interpolate,
this.config.parse.raw,
"-",
"_",
];

for (const prefix of Object.keys(this.config.customTags)) {
if (reserved.includes(prefix)) {
throw new EtaError(
`Custom tag prefix "${prefix}" conflicts with a built-in prefix`,
);
}
}
}

config: EtaConfig;
Expand Down
13 changes: 8 additions & 5 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { ParseErr } from "./err.ts";
import type { Eta } from "./internal.ts";
import { trimWS } from "./utils.ts";

export type TagType = "r" | "e" | "i" | "";

export interface TemplateObject {
t: TagType;
t: string;
val: string;
lineNo?: number;
}
Expand Down Expand Up @@ -40,6 +38,8 @@ export function parse(this: Eta, str: string): Array<AstObject> {
let lastIndex = 0;
const parseOptions = config.parse;

const customTagPrefixes = Object.keys(config.customTags);

if (config.plugins) {
for (let i = 0; i < config.plugins.length; i++) {
const plugin = config.plugins[i];
Expand Down Expand Up @@ -90,6 +90,7 @@ export function parse(this: Eta, str: string): Array<AstObject> {
parseOptions.exec,
parseOptions.interpolate,
parseOptions.raw,
...customTagPrefixes,
].reduce((accumulator, prefix) => {
if (accumulator && prefix) {
return accumulator + "|" + escapeRegExp(prefix);
Expand Down Expand Up @@ -138,14 +139,16 @@ export function parse(this: Eta, str: string): Array<AstObject> {

trimLeftOfNextStr = closeTag[2];

const currentType: TagType =
const currentType: string =
prefix === parseOptions.exec
? "e"
: prefix === parseOptions.raw
? "r"
: prefix === parseOptions.interpolate
? "i"
: "";
: customTagPrefixes.includes(prefix)
? prefix
: "";

currentObj = { t: currentType, val: content };
break;
Expand Down
63 changes: 63 additions & 0 deletions test/render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,69 @@ describe("block", () => {
});
});

describe("customTags", () => {
it("basic custom tag renders output", () => {
const eta = new Eta({
customTags: {
"*": (content, data) =>
(data as Record<string, string>)[content.trim()],
},
});

const res = eta.renderString("Hello <%* name %>!", { name: "World" });
expect(res).toEqual("Hello World!");
});

it("comment tag that outputs nothing", () => {
const eta = new Eta({
customTags: { "#": () => "" },
});

const res = eta.renderString("A<%# this is a comment %>B", {});
expect(res).toEqual("AB");
});

it("multiple custom tags in one template", () => {
const translations: Record<string, Record<string, string>> = {
en: { greeting: "Hello" },
};

const eta = new Eta({
customTags: {
"#": () => "",
"*": (key, data) =>
translations[(data as { lang: string }).lang][key.trim()],
},
});

const res = eta.renderString("<%# comment %><p><%* greeting %></p>", {
lang: "en",
});
expect(res).toEqual("<p>Hello</p>");
});

it("throws on conflicting custom tag prefix", () => {
expect(() => new Eta({ customTags: { "=": () => "" } })).toThrow(
/conflicts with a built-in prefix/,
);
expect(() => new Eta({ customTags: { "-": () => "" } })).toThrow(
/conflicts with a built-in prefix/,
);
});

it("works alongside built-in tags", () => {
const eta = new Eta({
customTags: { "*": (content) => content.trim().toUpperCase() },
});

const res = eta.renderString("<%= it.a %>|<%* hello %>|<%~ it.b %>", {
a: "A",
b: "<B>",
});
expect(res).toEqual("A|HELLO|<B>");
});
});

describe("capture", () => {
const eta = new Eta();

Expand Down
Loading