vellora 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.js +105 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +6 -0
- package/dist/input.js +79 -0
- package/dist/input.js.map +1 -0
- package/dist/mock-bridge.d.ts +15 -0
- package/dist/mock-bridge.js +0 -0
- package/dist/mock-bridge.js.map +1 -0
- package/dist/native-bridge.d.ts +6 -0
- package/dist/native-bridge.js +43 -0
- package/dist/native-bridge.js.map +1 -0
- package/dist/orchestrate.d.ts +17 -0
- package/dist/orchestrate.js +148 -0
- package/dist/orchestrate.js.map +1 -0
- package/dist/render.d.ts +30 -0
- package/dist/render.js +82 -0
- package/dist/render.js.map +1 -0
- package/dist/source-position.d.ts +10 -0
- package/dist/source-position.js +21 -0
- package/dist/source-position.js.map +1 -0
- package/dist/template/helpers.d.ts +7 -0
- package/dist/template/helpers.js +150 -0
- package/dist/template/helpers.js.map +1 -0
- package/dist/template/index.d.ts +11 -0
- package/dist/template/index.js +11 -0
- package/dist/template/index.js.map +1 -0
- package/dist/template/interpreter.d.ts +4 -0
- package/dist/template/interpreter.js +208 -0
- package/dist/template/interpreter.js.map +1 -0
- package/dist/template/parser.d.ts +22 -0
- package/dist/template/parser.js +99 -0
- package/dist/template/parser.js.map +1 -0
- package/dist/template/tokenizer.d.ts +22 -0
- package/dist/template/tokenizer.js +74 -0
- package/dist/template/tokenizer.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public render functions: `renderPdf` and `renderPdfToStream`.
|
|
3
|
+
*
|
|
4
|
+
* Both wire input handling → templating → strict orchestration → native bridge. Input is always
|
|
5
|
+
* content (string | Uint8Array | Readable), never a path, and a `Readable` is buffered in full
|
|
6
|
+
* before templating. Currently the native bridge is the deterministic mock; the native bridge
|
|
7
|
+
* swaps in the real `@vellora/native` with no change to these signatures.
|
|
8
|
+
*/
|
|
9
|
+
import type { Writable } from "node:stream";
|
|
10
|
+
import { VelloraInputError } from "./errors.js";
|
|
11
|
+
import type { HtmlInput, NativeBridge, RenderData, RenderOptions } from "./types.js";
|
|
12
|
+
/** Test/wiring seam: swap the default native bridge. Returns the previous bridge. */
|
|
13
|
+
export declare function setNativeBridge(bridge: NativeBridge): NativeBridge;
|
|
14
|
+
/**
|
|
15
|
+
* Render document HTML to a complete PDF.
|
|
16
|
+
*
|
|
17
|
+
* @param html document **content** (string | Uint8Array | Readable), never a file path.
|
|
18
|
+
* @param data optional templating data.
|
|
19
|
+
* @param opts optional render options (`strict` defaults to `true`).
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderPdf(html: HtmlInput, data?: RenderData, opts?: RenderOptions): Promise<Uint8Array>;
|
|
22
|
+
/**
|
|
23
|
+
* Render document HTML and write the complete PDF to `writable`, then end it.
|
|
24
|
+
*
|
|
25
|
+
* Input is fully buffered; the complete PDF is produced via the native render path and then written.
|
|
26
|
+
* Resolves only after the complete PDF is written. A `writable` `error` rejects and aborts.
|
|
27
|
+
* (Page-by-page progressive emission awaits a future native streaming surface.)
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderPdfToStream(html: HtmlInput, writable: Writable, data?: RenderData, opts?: RenderOptions): Promise<void>;
|
|
30
|
+
export { VelloraInputError };
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { VelloraError, VelloraInputError } from "./errors.js";
|
|
2
|
+
import { normalizeInput } from "./input.js";
|
|
3
|
+
import { NativeAddonBridge } from "./native-bridge.js";
|
|
4
|
+
import { orchestrate } from "./orchestrate.js";
|
|
5
|
+
import { renderTemplate } from "./template/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Internal: the active native bridge. The production default is the real `@vellora/native` addon
|
|
8
|
+
* (lazy — the `.node` loads only on first render). Unit tests reset this to the deterministic mock
|
|
9
|
+
* (see `test/_setup-bridge.ts`); a per-call override is passed via `_bridge`.
|
|
10
|
+
*/
|
|
11
|
+
let defaultBridge;
|
|
12
|
+
function getDefaultBridge() {
|
|
13
|
+
if (!defaultBridge) {
|
|
14
|
+
defaultBridge = new NativeAddonBridge();
|
|
15
|
+
}
|
|
16
|
+
return defaultBridge;
|
|
17
|
+
}
|
|
18
|
+
/** Test/wiring seam: swap the default native bridge. Returns the previous bridge. */
|
|
19
|
+
export function setNativeBridge(bridge) {
|
|
20
|
+
const previous = getDefaultBridge();
|
|
21
|
+
defaultBridge = bridge;
|
|
22
|
+
return previous;
|
|
23
|
+
}
|
|
24
|
+
/** Run the shared pipeline: normalize → template → orchestrate, resolving to PDF bytes. */
|
|
25
|
+
async function pipeline(html, data, opts) {
|
|
26
|
+
const content = await normalizeInput(html);
|
|
27
|
+
const templated = renderTemplate(content, data);
|
|
28
|
+
const bridge = opts._bridge ?? getDefaultBridge();
|
|
29
|
+
return orchestrate(templated, opts, bridge);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Render document HTML to a complete PDF.
|
|
33
|
+
*
|
|
34
|
+
* @param html document **content** (string | Uint8Array | Readable), never a file path.
|
|
35
|
+
* @param data optional templating data.
|
|
36
|
+
* @param opts optional render options (`strict` defaults to `true`).
|
|
37
|
+
*/
|
|
38
|
+
export function renderPdf(html, data, opts = {}) {
|
|
39
|
+
return pipeline(html, data, opts);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Render document HTML and write the complete PDF to `writable`, then end it.
|
|
43
|
+
*
|
|
44
|
+
* Input is fully buffered; the complete PDF is produced via the native render path and then written.
|
|
45
|
+
* Resolves only after the complete PDF is written. A `writable` `error` rejects and aborts.
|
|
46
|
+
* (Page-by-page progressive emission awaits a future native streaming surface.)
|
|
47
|
+
*/
|
|
48
|
+
export async function renderPdfToStream(html, writable, data, opts = {}) {
|
|
49
|
+
const pdf = await pipeline(html, data, opts);
|
|
50
|
+
await writeAndEnd(writable, pdf);
|
|
51
|
+
}
|
|
52
|
+
/** Write a buffer to a `Writable` and end it; reject on a destination error. */
|
|
53
|
+
function writeAndEnd(writable, bytes) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
let settled = false;
|
|
56
|
+
const fail = (cause) => {
|
|
57
|
+
if (settled) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
settled = true;
|
|
61
|
+
reject(cause instanceof Error ? cause : new VelloraError(String(cause)));
|
|
62
|
+
};
|
|
63
|
+
// Stay subscribed for the lifetime of the write so a post-callback `error` event (Node emits one
|
|
64
|
+
// when a write callback reports an error) is absorbed rather than left uncaught.
|
|
65
|
+
writable.on("error", fail);
|
|
66
|
+
writable.write(bytes, (writeErr) => {
|
|
67
|
+
if (writeErr) {
|
|
68
|
+
fail(writeErr);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
writable.end(() => {
|
|
72
|
+
if (!settled) {
|
|
73
|
+
settled = true;
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Re-export so the public barrel can avoid importing the input module directly.
|
|
81
|
+
export { VelloraInputError };
|
|
82
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared offset→{line,col} mapping. One implementation for the mock bridge's violation locator and
|
|
3
|
+
* the tokenizer's non-incremental callers, so the two can't drift. The tokenizer's main scan tracks
|
|
4
|
+
* line/col incrementally (single O(N) pass); this helper serves the occasional one-off lookup.
|
|
5
|
+
*/
|
|
6
|
+
/** Compute the 1-based line/column of an absolute `offset` within `source`. */
|
|
7
|
+
export declare function offsetToLineCol(source: string, offset: number): {
|
|
8
|
+
line: number;
|
|
9
|
+
col: number;
|
|
10
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared offset→{line,col} mapping. One implementation for the mock bridge's violation locator and
|
|
3
|
+
* the tokenizer's non-incremental callers, so the two can't drift. The tokenizer's main scan tracks
|
|
4
|
+
* line/col incrementally (single O(N) pass); this helper serves the occasional one-off lookup.
|
|
5
|
+
*/
|
|
6
|
+
/** Compute the 1-based line/column of an absolute `offset` within `source`. */
|
|
7
|
+
export function offsetToLineCol(source, offset) {
|
|
8
|
+
let line = 1;
|
|
9
|
+
let col = 1;
|
|
10
|
+
for (let i = 0; i < offset; i++) {
|
|
11
|
+
if (source[i] === "\n") {
|
|
12
|
+
line++;
|
|
13
|
+
col = 1;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
col++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { line, col };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=source-position.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-position.js","sourceRoot":"","sources":["../src/source-position.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,MAAc;IAC5D,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACvB,IAAI,EAAE,CAAC;YACP,GAAG,GAAG,CAAC,CAAC;QACV,CAAC;aAAM,CAAC;YACN,GAAG,EAAE,CAAC;QACR,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A built-in helper: receives the resolved value + literal args, returns a string. */
|
|
2
|
+
export type Helper = (value: unknown, args: unknown[], location: {
|
|
3
|
+
line: number;
|
|
4
|
+
col: number;
|
|
5
|
+
}) => string;
|
|
6
|
+
/** The built-in helper registry. Unknown helper names reject with a `VelloraTemplateError`. */
|
|
7
|
+
export declare const HELPERS: Record<string, Helper>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic format helpers: `currency`, `number`, `date`.
|
|
3
|
+
*
|
|
4
|
+
* Determinism is the hard constraint (ARCHITECTURE.md: same template + data ⇒ byte-stable PDF):
|
|
5
|
+
* helpers NEVER read the host's ambient `Intl` locale or `TZ`. `currency`/`number` use
|
|
6
|
+
* `Intl.NumberFormat` with an explicitly pinned locale; `date` parses the instant as UTC and formats
|
|
7
|
+
* via an explicit format string in UTC. Output is escaped by the interpolation layer like any value.
|
|
8
|
+
*/
|
|
9
|
+
import { VelloraTemplateError } from "../errors.js";
|
|
10
|
+
/** Locale pinned per currency so output is machine-independent. Extend as currencies are added. */
|
|
11
|
+
const CURRENCY_LOCALE = {
|
|
12
|
+
BRL: "pt-BR",
|
|
13
|
+
USD: "en-US",
|
|
14
|
+
EUR: "de-DE",
|
|
15
|
+
GBP: "en-GB",
|
|
16
|
+
};
|
|
17
|
+
function toNumber(value) {
|
|
18
|
+
if (typeof value === "number") {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
22
|
+
const n = Number(value);
|
|
23
|
+
if (!Number.isNaN(n)) {
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return Number.NaN;
|
|
28
|
+
}
|
|
29
|
+
/** `currency(code)` — e.g. `currency("BRL")` ⇒ `R$ 1.234,50` (NBSP separator, per Intl pt-BR/BRL). */
|
|
30
|
+
const currency = (value, args, location) => {
|
|
31
|
+
const code = args[0];
|
|
32
|
+
if (typeof code !== "string") {
|
|
33
|
+
throw new VelloraTemplateError('currency() requires a currency code string, e.g. currency("BRL").', location);
|
|
34
|
+
}
|
|
35
|
+
if (!/^[A-Za-z]{3}$/.test(code)) {
|
|
36
|
+
throw new VelloraTemplateError(`currency() received an invalid currency code: ${JSON.stringify(code)}. Expected a 3-letter ISO 4217 code, e.g. currency("BRL").`, location);
|
|
37
|
+
}
|
|
38
|
+
const n = toNumber(value);
|
|
39
|
+
if (Number.isNaN(n)) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
const locale = CURRENCY_LOCALE[code] ?? "en-US";
|
|
43
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency: code }).format(n);
|
|
44
|
+
};
|
|
45
|
+
/** `number(fractionDigits?)` — fixed fraction digits via en-US so the decimal point is `.`. */
|
|
46
|
+
const number = (value, args, location) => {
|
|
47
|
+
const digitsArg = args[0];
|
|
48
|
+
let digits;
|
|
49
|
+
if (digitsArg !== undefined) {
|
|
50
|
+
// `Intl.NumberFormat` requires `fractionDigits` to be an integer in [0, 20]; out-of-range or
|
|
51
|
+
// non-integer values throw a raw `RangeError`. Validate up front for a located message.
|
|
52
|
+
if (typeof digitsArg !== "number" ||
|
|
53
|
+
!Number.isInteger(digitsArg) ||
|
|
54
|
+
digitsArg < 0 ||
|
|
55
|
+
digitsArg > 20) {
|
|
56
|
+
throw new VelloraTemplateError(`number() received an invalid fraction-digit count: ${JSON.stringify(digitsArg)}. Expected an integer in [0, 20].`, location);
|
|
57
|
+
}
|
|
58
|
+
digits = digitsArg;
|
|
59
|
+
}
|
|
60
|
+
const n = toNumber(value);
|
|
61
|
+
if (Number.isNaN(n)) {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
return new Intl.NumberFormat("en-US", {
|
|
65
|
+
minimumFractionDigits: digits,
|
|
66
|
+
maximumFractionDigits: digits,
|
|
67
|
+
useGrouping: false,
|
|
68
|
+
}).format(n);
|
|
69
|
+
};
|
|
70
|
+
const MONTHS = [
|
|
71
|
+
"January",
|
|
72
|
+
"February",
|
|
73
|
+
"March",
|
|
74
|
+
"April",
|
|
75
|
+
"May",
|
|
76
|
+
"June",
|
|
77
|
+
"July",
|
|
78
|
+
"August",
|
|
79
|
+
"September",
|
|
80
|
+
"October",
|
|
81
|
+
"November",
|
|
82
|
+
"December",
|
|
83
|
+
];
|
|
84
|
+
/** Parse a date-ish value to UTC fields. Accepts ISO date / ISO instant / Date. NaN ⇒ undefined. */
|
|
85
|
+
function toUtcDate(value) {
|
|
86
|
+
if (value instanceof Date) {
|
|
87
|
+
return Number.isNaN(value.getTime()) ? undefined : value;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
// A bare `YYYY-MM-DD` is parsed by the platform as UTC midnight; an instant carries its own zone.
|
|
93
|
+
// A local datetime without zone (`YYYY-MM-DDTHH:mm:ss`) is pinned to UTC for determinism.
|
|
94
|
+
const bareLocal = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?$/.test(value);
|
|
95
|
+
const instant = new Date(bareLocal ? `${value}Z` : value);
|
|
96
|
+
return Number.isNaN(instant.getTime()) ? undefined : instant;
|
|
97
|
+
}
|
|
98
|
+
function pad(n, width) {
|
|
99
|
+
return String(n).padStart(width, "0");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `date(format)` — UTC formatting against an explicit token string so output is timezone-stable.
|
|
103
|
+
* Supported tokens: `YYYY` `YY` `MM` `M` `MMMM` `DD` `D` `HH` `mm` `ss`.
|
|
104
|
+
*/
|
|
105
|
+
const date = (value, args, location) => {
|
|
106
|
+
const format = args[0];
|
|
107
|
+
if (typeof format !== "string") {
|
|
108
|
+
throw new VelloraTemplateError('date() requires a format string, e.g. date("YYYY-MM-DD").', location);
|
|
109
|
+
}
|
|
110
|
+
const d = toUtcDate(value);
|
|
111
|
+
if (!d) {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
const year = d.getUTCFullYear();
|
|
115
|
+
const month = d.getUTCMonth() + 1;
|
|
116
|
+
const day = d.getUTCDate();
|
|
117
|
+
const hours = d.getUTCHours();
|
|
118
|
+
const minutes = d.getUTCMinutes();
|
|
119
|
+
const seconds = d.getUTCSeconds();
|
|
120
|
+
// Longest tokens first so `YYYY` is not consumed as two `YY`, and `MMMM` before `MM`.
|
|
121
|
+
return format.replace(/YYYY|MMMM|YY|MM|DD|HH|mm|ss|M|D/g, (token) => {
|
|
122
|
+
switch (token) {
|
|
123
|
+
case "YYYY":
|
|
124
|
+
return pad(year, 4);
|
|
125
|
+
case "YY":
|
|
126
|
+
return pad(year % 100, 2);
|
|
127
|
+
case "MMMM":
|
|
128
|
+
return MONTHS[month - 1] ?? "";
|
|
129
|
+
case "MM":
|
|
130
|
+
return pad(month, 2);
|
|
131
|
+
case "M":
|
|
132
|
+
return String(month);
|
|
133
|
+
case "DD":
|
|
134
|
+
return pad(day, 2);
|
|
135
|
+
case "D":
|
|
136
|
+
return String(day);
|
|
137
|
+
case "HH":
|
|
138
|
+
return pad(hours, 2);
|
|
139
|
+
case "mm":
|
|
140
|
+
return pad(minutes, 2);
|
|
141
|
+
case "ss":
|
|
142
|
+
return pad(seconds, 2);
|
|
143
|
+
default:
|
|
144
|
+
return token;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
/** The built-in helper registry. Unknown helper names reject with a `VelloraTemplateError`. */
|
|
149
|
+
export const HELPERS = { currency, number, date };
|
|
150
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/template/helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in templating engine entry point.
|
|
3
|
+
*
|
|
4
|
+
* `renderTemplate(html, data)` tokenizes → parses → renders to a finalized HTML string with all
|
|
5
|
+
* `{{ }}` / `{% %}` tokens resolved. Syntax errors (unclosed blocks, unknown tags/helpers) reject
|
|
6
|
+
* with a located `VelloraTemplateError` before any native call. No arbitrary code is executed.
|
|
7
|
+
*/
|
|
8
|
+
import type { RenderData } from "../types.js";
|
|
9
|
+
export { HELPERS } from "./helpers.js";
|
|
10
|
+
/** Apply the templating engine. Throws `VelloraTemplateError` on any syntax error. */
|
|
11
|
+
export declare function renderTemplate(html: string, data?: RenderData): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { render } from "./interpreter.js";
|
|
2
|
+
import { parse } from "./parser.js";
|
|
3
|
+
import { tokenize } from "./tokenizer.js";
|
|
4
|
+
export { HELPERS } from "./helpers.js";
|
|
5
|
+
/** Apply the templating engine. Throws `VelloraTemplateError` on any syntax error. */
|
|
6
|
+
export function renderTemplate(html, data = {}) {
|
|
7
|
+
const tokens = tokenize(html);
|
|
8
|
+
const ast = parse(tokens);
|
|
9
|
+
return render(ast, data);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/template/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,sFAAsF;AACtF,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,OAAmB,EAAE;IAChE,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,OAAO,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interpreter: resolve dotted paths against `data`, evaluate interpolation expressions
|
|
3
|
+
* (`path | helper(args)`), conditions (`path`, `not path`, `path == literal`, `path != literal`),
|
|
4
|
+
* and render the AST to a finalized HTML string.
|
|
5
|
+
*
|
|
6
|
+
* Escape-by-default: every interpolated value (helpers included) is HTML-escaped so data cannot
|
|
7
|
+
* inject markup. Missing paths resolve to `""` and never throw. No arbitrary code is executed: only
|
|
8
|
+
* the documented token grammar is interpreted; data values are inert text.
|
|
9
|
+
*/
|
|
10
|
+
import { VelloraTemplateError } from "../errors.js";
|
|
11
|
+
import { HELPERS } from "./helpers.js";
|
|
12
|
+
function escapeHtml(value) {
|
|
13
|
+
return value
|
|
14
|
+
.replace(/&/g, "&")
|
|
15
|
+
.replace(/</g, "<")
|
|
16
|
+
.replace(/>/g, ">")
|
|
17
|
+
.replace(/"/g, """)
|
|
18
|
+
.replace(/'/g, "'");
|
|
19
|
+
}
|
|
20
|
+
/** Resolve a dotted path against the scope chain (innermost first). Missing ⇒ `undefined`. */
|
|
21
|
+
function resolvePath(path, scopes) {
|
|
22
|
+
const segments = path.split(".");
|
|
23
|
+
const head = segments[0];
|
|
24
|
+
if (head === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
let base;
|
|
28
|
+
let found = false;
|
|
29
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
30
|
+
const scope = scopes[i];
|
|
31
|
+
if (scope && Object.hasOwn(scope, head)) {
|
|
32
|
+
base = scope[head];
|
|
33
|
+
found = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!found) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
for (let i = 1; i < segments.length; i++) {
|
|
41
|
+
if (base === null || base === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const key = segments[i];
|
|
45
|
+
if (key === undefined) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
// Gate EVERY segment on own-property access so a dotted path can never walk the prototype chain
|
|
49
|
+
// (`x.__proto__`, `x.constructor.name`); such reads resolve to `undefined` (coerced to "").
|
|
50
|
+
if (!Object.hasOwn(base, key)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
base = base[key];
|
|
54
|
+
}
|
|
55
|
+
return base;
|
|
56
|
+
}
|
|
57
|
+
/** Coerce a resolved value to its string form for escaping. `null`/`undefined` ⇒ `""`. */
|
|
58
|
+
function coerce(value) {
|
|
59
|
+
if (value === null || value === undefined) {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "string") {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
return String(value);
|
|
66
|
+
}
|
|
67
|
+
/** Parse a literal argument: a quoted string or a number. */
|
|
68
|
+
function parseLiteral(raw, pos) {
|
|
69
|
+
const trimmed = raw.trim();
|
|
70
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
71
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
72
|
+
return trimmed.slice(1, -1);
|
|
73
|
+
}
|
|
74
|
+
if (trimmed !== "" && !Number.isNaN(Number(trimmed))) {
|
|
75
|
+
return Number(trimmed);
|
|
76
|
+
}
|
|
77
|
+
throw new VelloraTemplateError(`Unsupported helper argument: ${raw}.`, pos);
|
|
78
|
+
}
|
|
79
|
+
/** Split a `helper(arg, arg)` arg list, respecting quotes. */
|
|
80
|
+
function splitArgs(rawArgs) {
|
|
81
|
+
const args = [];
|
|
82
|
+
let depth = 0;
|
|
83
|
+
let quote;
|
|
84
|
+
let current = "";
|
|
85
|
+
for (const ch of rawArgs) {
|
|
86
|
+
if (quote) {
|
|
87
|
+
current += ch;
|
|
88
|
+
if (ch === quote) {
|
|
89
|
+
quote = undefined;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === '"' || ch === "'") {
|
|
94
|
+
quote = ch;
|
|
95
|
+
current += ch;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === "(") {
|
|
99
|
+
depth++;
|
|
100
|
+
current += ch;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === ")") {
|
|
104
|
+
depth--;
|
|
105
|
+
current += ch;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ch === "," && depth === 0) {
|
|
109
|
+
args.push(current);
|
|
110
|
+
current = "";
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
current += ch;
|
|
114
|
+
}
|
|
115
|
+
if (current.trim() !== "") {
|
|
116
|
+
args.push(current);
|
|
117
|
+
}
|
|
118
|
+
return args;
|
|
119
|
+
}
|
|
120
|
+
/** Evaluate an interpolation expression to its (unescaped) string value. */
|
|
121
|
+
function evalInterpolation(expr, scopes, pos) {
|
|
122
|
+
const pipeIdx = expr.indexOf("|");
|
|
123
|
+
if (pipeIdx === -1) {
|
|
124
|
+
return coerce(resolvePath(expr.trim(), scopes));
|
|
125
|
+
}
|
|
126
|
+
const path = expr.slice(0, pipeIdx).trim();
|
|
127
|
+
const helperPart = expr.slice(pipeIdx + 1).trim();
|
|
128
|
+
const callMatch = helperPart.match(/^([A-Za-z_$][\w$]*)\s*(?:\((.*)\))?$/s);
|
|
129
|
+
if (!callMatch) {
|
|
130
|
+
throw new VelloraTemplateError(`Malformed helper expression: ${helperPart}.`, pos);
|
|
131
|
+
}
|
|
132
|
+
const name = callMatch[1] ?? "";
|
|
133
|
+
const helper = HELPERS[name];
|
|
134
|
+
if (!helper) {
|
|
135
|
+
throw new VelloraTemplateError(`Unknown helper: ${name}.`, pos);
|
|
136
|
+
}
|
|
137
|
+
const rawArgs = callMatch[2];
|
|
138
|
+
const args = rawArgs === undefined || rawArgs.trim() === ""
|
|
139
|
+
? []
|
|
140
|
+
: splitArgs(rawArgs).map((a) => parseLiteral(a, pos));
|
|
141
|
+
const value = resolvePath(path, scopes);
|
|
142
|
+
try {
|
|
143
|
+
return helper(value, args, pos);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (err instanceof VelloraTemplateError) {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
// A helper may throw a raw V8 error (e.g. `Intl` `RangeError` on a bad currency/digits arg).
|
|
150
|
+
// Re-wrap as a located `VelloraTemplateError` so every error leaving the engine is typed.
|
|
151
|
+
throw new VelloraTemplateError(`Helper "${name}" failed: ${err instanceof Error ? err.message : String(err)}.`, pos);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Evaluate a condition expression to a boolean. Supports path, `not path`, `==`/`!=` literals. */
|
|
155
|
+
function evalCondition(condition, scopes, pos) {
|
|
156
|
+
const negMatch = condition.match(/^not\s+(.+)$/);
|
|
157
|
+
if (negMatch) {
|
|
158
|
+
return !truthy(resolvePath((negMatch[1] ?? "").trim(), scopes));
|
|
159
|
+
}
|
|
160
|
+
const eqMatch = condition.match(/^(.+?)\s*(==|!=)\s*(.+)$/);
|
|
161
|
+
if (eqMatch) {
|
|
162
|
+
const left = resolvePath((eqMatch[1] ?? "").trim(), scopes);
|
|
163
|
+
const right = parseLiteral(eqMatch[3] ?? "", pos);
|
|
164
|
+
const equal = left === right;
|
|
165
|
+
return eqMatch[2] === "==" ? equal : !equal;
|
|
166
|
+
}
|
|
167
|
+
return truthy(resolvePath(condition.trim(), scopes));
|
|
168
|
+
}
|
|
169
|
+
/** Truthiness for conditions: empty arrays are falsy (so `{% if items %}` guards rows). */
|
|
170
|
+
function truthy(value) {
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
return value.length > 0;
|
|
173
|
+
}
|
|
174
|
+
return Boolean(value);
|
|
175
|
+
}
|
|
176
|
+
function renderNodes(nodes, scopes) {
|
|
177
|
+
let out = "";
|
|
178
|
+
for (const node of nodes) {
|
|
179
|
+
switch (node.type) {
|
|
180
|
+
case "text":
|
|
181
|
+
out += node.value;
|
|
182
|
+
break;
|
|
183
|
+
case "interpolation":
|
|
184
|
+
out += escapeHtml(evalInterpolation(node.expr, scopes, node.pos));
|
|
185
|
+
break;
|
|
186
|
+
case "for": {
|
|
187
|
+
const collection = resolvePath(node.collection, scopes);
|
|
188
|
+
if (Array.isArray(collection)) {
|
|
189
|
+
for (const item of collection) {
|
|
190
|
+
out += renderNodes(node.body, [...scopes, { [node.item]: item }]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "if":
|
|
196
|
+
out += evalCondition(node.condition, scopes, node.pos)
|
|
197
|
+
? renderNodes(node.consequent, scopes)
|
|
198
|
+
: renderNodes(node.alternate, scopes);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
/** Render a parsed AST to finalized HTML against `data`. */
|
|
205
|
+
export function render(nodes, data) {
|
|
206
|
+
return renderNodes(nodes, [data]);
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=interpreter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interpreter.js","sourceRoot":"","sources":["../../src/template/interpreter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Position, Token } from "./tokenizer.js";
|
|
2
|
+
export type Node = {
|
|
3
|
+
type: "text";
|
|
4
|
+
value: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "interpolation";
|
|
7
|
+
expr: string;
|
|
8
|
+
pos: Position;
|
|
9
|
+
} | {
|
|
10
|
+
type: "for";
|
|
11
|
+
item: string;
|
|
12
|
+
collection: string;
|
|
13
|
+
body: Node[];
|
|
14
|
+
pos: Position;
|
|
15
|
+
} | {
|
|
16
|
+
type: "if";
|
|
17
|
+
condition: string;
|
|
18
|
+
consequent: Node[];
|
|
19
|
+
alternate: Node[];
|
|
20
|
+
pos: Position;
|
|
21
|
+
};
|
|
22
|
+
export declare function parse(tokens: Token[]): Node[];
|