@funcstache/stache-stream 0.2.2 → 0.2.3
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/.eslintrc.json +30 -0
- package/.swcrc +29 -0
- package/DEV.md +84 -0
- package/README.md +145 -0
- package/TASKS.md +13 -0
- package/TODO.md +28 -0
- package/docs/.nojekyll +1 -0
- package/docs/assets/hierarchy.js +1 -0
- package/docs/assets/highlight.css +120 -0
- package/docs/assets/icons.js +18 -0
- package/docs/assets/icons.svg +1 -0
- package/docs/assets/main.js +60 -0
- package/docs/assets/navigation.js +1 -0
- package/docs/assets/search.js +1 -0
- package/docs/assets/style.css +1633 -0
- package/docs/classes/StacheTransformStream.html +13 -0
- package/docs/hierarchy.html +1 -0
- package/docs/index.html +73 -0
- package/docs/interfaces/Context.html +3 -0
- package/docs/interfaces/ContextProvider.html +10 -0
- package/docs/interfaces/PartialTagContextLambda.html +11 -0
- package/docs/interfaces/PartialTagContextLambdaResult.html +7 -0
- package/docs/interfaces/SectionTagCallback.html +12 -0
- package/docs/interfaces/SectionTagContextRecord.html +4 -0
- package/docs/interfaces/Tag.html +45 -0
- package/docs/interfaces/VariableTagContextLambda.html +4 -0
- package/docs/interfaces/VariableTagContextRecord.html +3 -0
- package/docs/media/StacheStream.ts +79 -0
- package/docs/modules.html +1 -0
- package/docs/types/ContextTypes.html +3 -0
- package/docs/types/JsonType.html +2 -0
- package/docs/types/PartialTagContext.html +4 -0
- package/docs/types/SectionTagContext.html +4 -0
- package/docs/types/TemplateName.html +9 -0
- package/docs/types/VariableTagContext.html +4 -0
- package/docs/types/VariableTagContextPrimitive.html +3 -0
- package/docs-assets/images/context-dotted-found.png +0 -0
- package/docs-assets/images/context-dotted-not-found.png +0 -0
- package/docs-assets/images/context-not-found.png +0 -0
- package/package.json +3 -6
- package/project.json +26 -0
- package/src/global.d.ts +10 -0
- package/src/index.ts +67 -0
- package/src/lib/parse/Parse.spec.ts +50 -0
- package/src/lib/parse/Parse.ts +92 -0
- package/src/lib/parse/README.md +62 -0
- package/src/lib/plan_base_v2.md +33 -0
- package/src/lib/plan_comment.md +53 -0
- package/src/lib/plan_implicit-iterator.md +213 -0
- package/src/lib/plan_inverted-sections.md +160 -0
- package/src/lib/plan_partials.md +237 -0
- package/src/lib/plan_sections.md +167 -0
- package/src/lib/plan_stache-stream.md +110 -0
- package/src/lib/plan_whitespace.md +98 -0
- package/src/lib/queue/Queue.spec.ts +275 -0
- package/src/lib/queue/Queue.ts +253 -0
- package/src/lib/queue/README.md +110 -0
- package/src/lib/stache-stream/README.md +45 -0
- package/src/lib/stache-stream/StacheStream.spec.ts +107 -0
- package/src/lib/stache-stream/StacheStream.ts +79 -0
- package/src/lib/tag/README.md +95 -0
- package/src/lib/tag/Tag.spec.ts +212 -0
- package/src/lib/tag/Tag.ts +295 -0
- package/src/lib/template/README.md +102 -0
- package/src/lib/template/Template-comment.spec.ts +76 -0
- package/src/lib/template/Template-inverted-section.spec.ts +85 -0
- package/src/lib/template/Template-partials.spec.ts +125 -0
- package/src/lib/template/Template-section.spec.ts +142 -0
- package/src/lib/template/Template.spec.ts +178 -0
- package/src/lib/template/Template.ts +614 -0
- package/src/lib/test/streams.ts +36 -0
- package/src/lib/tokenize/README.md +97 -0
- package/src/lib/tokenize/Tokenize.spec.ts +364 -0
- package/src/lib/tokenize/Tokenize.ts +374 -0
- package/src/lib/{types.d.ts → types.ts} +73 -25
- package/tsconfig.json +21 -0
- package/tsconfig.lib.json +16 -0
- package/tsconfig.spec.json +21 -0
- package/typedoc.mjs +15 -0
- package/vite.config.ts +27 -0
- package/vitest.setup.ts +6 -0
- package/src/global.d.js +0 -8
- package/src/global.d.js.map +0 -1
- package/src/index.d.ts +0 -7
- package/src/index.js +0 -24
- package/src/index.js.map +0 -1
- package/src/lib/parse/Parse.d.ts +0 -14
- package/src/lib/parse/Parse.js +0 -79
- package/src/lib/parse/Parse.js.map +0 -1
- package/src/lib/queue/Queue.d.ts +0 -32
- package/src/lib/queue/Queue.js +0 -181
- package/src/lib/queue/Queue.js.map +0 -1
- package/src/lib/stache-stream/StacheStream.d.ts +0 -22
- package/src/lib/stache-stream/StacheStream.js +0 -71
- package/src/lib/stache-stream/StacheStream.js.map +0 -1
- package/src/lib/tag/Tag.d.ts +0 -33
- package/src/lib/tag/Tag.js +0 -231
- package/src/lib/tag/Tag.js.map +0 -1
- package/src/lib/template/Template.d.ts +0 -18
- package/src/lib/template/Template.js +0 -428
- package/src/lib/template/Template.js.map +0 -1
- package/src/lib/test/streams.d.ts +0 -2
- package/src/lib/test/streams.js +0 -39
- package/src/lib/test/streams.js.map +0 -1
- package/src/lib/tokenize/Tokenize.d.ts +0 -22
- package/src/lib/tokenize/Tokenize.js +0 -268
- package/src/lib/tokenize/Tokenize.js.map +0 -1
- package/src/lib/types.js +0 -33
- package/src/lib/types.js.map +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ReadableStream, TextDecoderStream } from "node:stream/web";
|
|
2
|
+
import type {
|
|
3
|
+
ContextProvider,
|
|
4
|
+
ContextTypes,
|
|
5
|
+
PartialTagContextLambda,
|
|
6
|
+
} from "../types";
|
|
7
|
+
import { createReadableStream } from "../test/streams";
|
|
8
|
+
import { StacheTransformStream } from "./StacheStream";
|
|
9
|
+
|
|
10
|
+
async function render(
|
|
11
|
+
template: string,
|
|
12
|
+
contextProvider?: ContextProvider
|
|
13
|
+
): Promise<string[]> {
|
|
14
|
+
const readable = createReadableStream(template);
|
|
15
|
+
const stache = new StacheTransformStream({ contextProvider });
|
|
16
|
+
const transformed = readable
|
|
17
|
+
.pipeThrough(new TextDecoderStream())
|
|
18
|
+
.pipeThrough(stache);
|
|
19
|
+
const chunks: string[] = [];
|
|
20
|
+
for await (const chunk of transformed) {
|
|
21
|
+
chunks.push(chunk);
|
|
22
|
+
}
|
|
23
|
+
return chunks;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makePartial(
|
|
27
|
+
template: string,
|
|
28
|
+
context?: ContextTypes
|
|
29
|
+
): PartialTagContextLambda {
|
|
30
|
+
return async () => ({
|
|
31
|
+
context,
|
|
32
|
+
input: async () =>
|
|
33
|
+
createReadableStream(template).pipeThrough(
|
|
34
|
+
new TextDecoderStream()
|
|
35
|
+
) as ReadableStream,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("StacheTransformStream", () => {
|
|
40
|
+
it("passes through plain text", async () => {
|
|
41
|
+
expect(await render("fubar")).toEqual(["fubar"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("variable", () => {
|
|
45
|
+
it("renders an escaped variable", async () => {
|
|
46
|
+
expect(
|
|
47
|
+
await render("Hi {{name}}!", { context: { name: "Alice" } })
|
|
48
|
+
).toEqual(["Hi ", "Alice", "!"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("HTML-escapes the value", async () => {
|
|
52
|
+
expect(await render("{{val}}", { context: { val: "a&b" } })).toEqual([
|
|
53
|
+
"a&b",
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("renders a raw variable with triple braces", async () => {
|
|
58
|
+
expect(
|
|
59
|
+
await render("{{{html}}}", { context: { html: "<b>ok</b>" } })
|
|
60
|
+
).toEqual(["<b>ok</b>"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders a raw variable with ampersand", async () => {
|
|
64
|
+
expect(
|
|
65
|
+
await render("{{&html}}", { context: { html: "<b>ok</b>" } })
|
|
66
|
+
).toEqual(["<b>ok</b>"]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders the implicit iterator", async () => {
|
|
71
|
+
expect(await render("{{.}}", { context: "hello" })).toEqual(["hello"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders a partial", async () => {
|
|
75
|
+
expect(
|
|
76
|
+
await render("Hello, {{>p}}!", { context: { p: makePartial("world") } })
|
|
77
|
+
).toEqual(["Hello, ", "world", "!"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("section", () => {
|
|
81
|
+
it("renders a truthy section", async () => {
|
|
82
|
+
expect(
|
|
83
|
+
await render("{{#show}}yes{{/show}}", { context: { show: true } })
|
|
84
|
+
).toEqual(["yes"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("skips a falsy section", async () => {
|
|
88
|
+
expect(
|
|
89
|
+
await render("{{#show}}yes{{/show}}", { context: { show: false } })
|
|
90
|
+
).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("renders a dotted-name section", async () => {
|
|
94
|
+
expect(
|
|
95
|
+
await render("{{#a.b}}yes{{/a.b}}", { context: { a: { b: true } } })
|
|
96
|
+
).toEqual(["yes"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("iterates over a list", async () => {
|
|
100
|
+
expect(
|
|
101
|
+
await render("{{#items}}{{.}} {{/items}}", {
|
|
102
|
+
context: { items: ["a", "b"] },
|
|
103
|
+
})
|
|
104
|
+
).toEqual(["a", " ", "b", " "]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ReadableStream, TransformStream } from "node:stream/web";
|
|
2
|
+
import type { ContextProvider } from "../types";
|
|
3
|
+
import { Template } from "../template/Template";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A {@link https://nodejs.org/api/webstreams.html#class-transformstream TransformStream} that
|
|
7
|
+
* renders a Mustache template.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The incoming {@link https://nodejs.org/api/webstreams.html#class-readablestream ReadableStream}
|
|
11
|
+
* provides the contents of a Mustache template. The stream is processed by StacheStreamTransform
|
|
12
|
+
* which emits the rendered Mustache template. The information needed to render the template is made
|
|
13
|
+
* available through `options` by the `ContextProvider`.
|
|
14
|
+
*/
|
|
15
|
+
export class StacheTransformStream extends TransformStream<string, string> {
|
|
16
|
+
/**
|
|
17
|
+
* @param options Options needed to render a Mustache template stream.
|
|
18
|
+
* @param options.contextProvider The `ContextProvider` is used during the render process to
|
|
19
|
+
* access the context for tags.
|
|
20
|
+
*/
|
|
21
|
+
constructor(options?: { contextProvider?: ContextProvider }) {
|
|
22
|
+
const { readable, push, close } = createReadableProducer<string>();
|
|
23
|
+
|
|
24
|
+
let resolveInactive!: () => void;
|
|
25
|
+
let rejectInactive!: (err: unknown) => void;
|
|
26
|
+
const waitInactive = new Promise<void>((resolve, reject) => {
|
|
27
|
+
resolveInactive = resolve;
|
|
28
|
+
rejectInactive = reject;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
super({
|
|
32
|
+
start(controller) {
|
|
33
|
+
new Template({
|
|
34
|
+
contextProvider: options?.contextProvider,
|
|
35
|
+
readable,
|
|
36
|
+
writeToOutput: async (text: string) => {
|
|
37
|
+
try {
|
|
38
|
+
controller.enqueue(text);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
rejectInactive(err);
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
.on("inactive", () => resolveInactive())
|
|
46
|
+
.read();
|
|
47
|
+
},
|
|
48
|
+
transform(chunk) {
|
|
49
|
+
push(chunk);
|
|
50
|
+
},
|
|
51
|
+
async flush(controller) {
|
|
52
|
+
close();
|
|
53
|
+
try {
|
|
54
|
+
await waitInactive;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
controller.error(err);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createReadableProducer<T>(): {
|
|
64
|
+
readable: ReadableStream<T>;
|
|
65
|
+
push: (chunk: T) => void;
|
|
66
|
+
close: () => void;
|
|
67
|
+
} {
|
|
68
|
+
let pushChunk!: (chunk: T) => void;
|
|
69
|
+
let closeStream!: () => void;
|
|
70
|
+
|
|
71
|
+
const readable = new ReadableStream<T>({
|
|
72
|
+
start(controller) {
|
|
73
|
+
pushChunk = controller.enqueue.bind(controller);
|
|
74
|
+
closeStream = controller.close.bind(controller);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return { readable, push: pushChunk, close: closeStream };
|
|
79
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Tag
|
|
2
|
+
|
|
3
|
+
`Tag` parses a raw mustache tag string (e.g. `{{#section}}`) and exposes its type, key, and rendering properties. It is the data object emitted by `Tokenize` and consumed by `Queue` and `Template`.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
### Parsing
|
|
8
|
+
|
|
9
|
+
`Tag.parse(tag)` is the primary entry point. It counts the leading brace characters to determine the delimiter width (2 for `{{`, 3 for `{{{`), strips the delimiters, and constructs a `Tag` from the inner value. Returns `undefined` if the string is not a valid tag (fewer than two opening braces, mismatched brace counts, or empty inner value).
|
|
10
|
+
|
|
11
|
+
Triple-brace tags (`{{{value}}}`) set the `rawBraces` flag, which causes `raw` to return `true` without further inspection of the value prefix.
|
|
12
|
+
|
|
13
|
+
### Type detection
|
|
14
|
+
|
|
15
|
+
Tag type and `raw` flag are derived lazily from the first character of the normalized value (whitespace stripped, no surrounding braces):
|
|
16
|
+
|
|
17
|
+
| First char | Type | `raw` |
|
|
18
|
+
| ----------------- | --------------------------------- | ------- |
|
|
19
|
+
| _(none / letter)_ | `variable` | `false` |
|
|
20
|
+
| `.` | `implicit` | `false` |
|
|
21
|
+
| `>` | `partial` | `false` |
|
|
22
|
+
| `#` | `section` | `false` |
|
|
23
|
+
| `/` | `end` | `false` |
|
|
24
|
+
| `^` | `inverted` | `false` |
|
|
25
|
+
| `!` | `comment` | `false` |
|
|
26
|
+
| `&` | `variable` (or `implicit` if `.`) | `true` |
|
|
27
|
+
| `{{{…}}}` | `variable` (or `implicit` if `.`) | `true` |
|
|
28
|
+
|
|
29
|
+
### Key extraction
|
|
30
|
+
|
|
31
|
+
`key` returns the lookup key used for context resolution. It strips any leading sigil (`>`, `#`, `/`, `^`, `&`, `!`, `$`, `<`) and any leading `*` (dynamic marker) or trailing `?`, then returns the remainder. For `{{name}}` the key is `name`; for `{{#list}}` the key is `list`.
|
|
32
|
+
|
|
33
|
+
### Value normalization
|
|
34
|
+
|
|
35
|
+
`value` trims whitespace and removes all internal whitespace from the raw constructor value. `toString()` / `toJSON()` reconstruct the original tag syntax including braces (`{{value}}` or `{{{value}}}`).
|
|
36
|
+
|
|
37
|
+
### Content
|
|
38
|
+
|
|
39
|
+
`content` holds text appended via `appendContent(text)`. It is only available for block-level tag types (`section`, `inverted`, `end`, `parent`, `block`); it returns `null` for value tag types (`variable`, `implicit`, `partial`, `comment`).
|
|
40
|
+
|
|
41
|
+
### Copying
|
|
42
|
+
|
|
43
|
+
`Tag.copy(tag, options)` creates a new `Tag` from an existing one, optionally overriding constructor options or replacing the `key` within the value string.
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { Tag } from "./Tag";
|
|
49
|
+
|
|
50
|
+
const tag = Tag.parse("{{#items}}");
|
|
51
|
+
// tag.type → "section"
|
|
52
|
+
// tag.key → "items"
|
|
53
|
+
// tag.raw → false
|
|
54
|
+
// tag.value → "#items"
|
|
55
|
+
// tag.toString() → "{{#items}}"
|
|
56
|
+
|
|
57
|
+
const raw = Tag.parse("{{{html}}}");
|
|
58
|
+
// raw.type → "variable"
|
|
59
|
+
// raw.key → "html"
|
|
60
|
+
// raw.raw → true
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
### `Tag.parse(tag, tokens?): Tag | undefined`
|
|
66
|
+
|
|
67
|
+
Parses a raw tag string and returns a `Tag`, or `undefined` if the string is not a valid tag. `tokens` defaults to `["{", "}"]`.
|
|
68
|
+
|
|
69
|
+
### `Tag.copy(tag, options?): Tag`
|
|
70
|
+
|
|
71
|
+
Creates a copy of `tag`. Pass `options` to override `content`, `rawBraces`, or `key` (replaces the key within the value string).
|
|
72
|
+
|
|
73
|
+
### `new Tag(options)`
|
|
74
|
+
|
|
75
|
+
| Option | Type | Description |
|
|
76
|
+
| ----------- | -------------------- | ------------------------------------------------------------ |
|
|
77
|
+
| `value` | `string` | The inner tag string (without surrounding braces). Required. |
|
|
78
|
+
| `content` | `string` (optional) | Initial content for block-level tags. |
|
|
79
|
+
| `rawBraces` | `boolean` (optional) | Set `true` for triple-brace tags. |
|
|
80
|
+
|
|
81
|
+
### `tag.appendContent(text): void`
|
|
82
|
+
|
|
83
|
+
Appends `text` to `content`. No-op for value tag types.
|
|
84
|
+
|
|
85
|
+
### Properties
|
|
86
|
+
|
|
87
|
+
| Property | Type | Description |
|
|
88
|
+
| ------------- | ---------------------------------------- | ------------------------------------------------------------------------- |
|
|
89
|
+
| `type` | `ValueTagTagTypes \| ContentTagTagTypes` | The tag type derived from the value prefix. |
|
|
90
|
+
| `key` | `string` | The context lookup key (sigil and dynamic markers stripped). |
|
|
91
|
+
| `raw` | `boolean` | `true` for `{{{…}}}` or `{{&…}}` tags; output should not be HTML-escaped. |
|
|
92
|
+
| `value` | `string` | Normalised tag value (whitespace removed, no braces). |
|
|
93
|
+
| `content` | `string \| null` | Accumulated inner content for block-level tags; `null` for value tags. |
|
|
94
|
+
| `dynamic` | `boolean` | `true` for dynamic partial/parent tags (not yet implemented). |
|
|
95
|
+
| `valueOption` | `string` | The raw value string passed to the constructor, before normalisation. |
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Tag } from "./Tag";
|
|
2
|
+
|
|
3
|
+
describe("Tag: properties from value", () => {
|
|
4
|
+
it("throws if an empty string is passed to the constructor", () => {
|
|
5
|
+
expect(() => new Tag({ value: "" })).toThrowError(
|
|
6
|
+
"`options.value` must contain a string value."
|
|
7
|
+
);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("normalizes a tag value with a preceding operator", () => {
|
|
11
|
+
const tag = new Tag({
|
|
12
|
+
value: " > A ",
|
|
13
|
+
});
|
|
14
|
+
expect(tag.dynamic).toBe(false);
|
|
15
|
+
expect(tag.key).toBe("A");
|
|
16
|
+
expect(tag.raw).toBe(false);
|
|
17
|
+
expect(tag.type).toBe("partial");
|
|
18
|
+
expect(tag.value).toBe(">A");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("normalizes a tag value with a non-false operator", () => {
|
|
22
|
+
const tag = new Tag({
|
|
23
|
+
value: " #Z ? ",
|
|
24
|
+
});
|
|
25
|
+
expect(tag.dynamic).toBe(false);
|
|
26
|
+
expect(tag.key).toBe("Z");
|
|
27
|
+
expect(tag.raw).toBe(false);
|
|
28
|
+
expect(tag.type).toBe("section");
|
|
29
|
+
expect(tag.value).toBe("#Z?");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("normalizes an implicit tag value", () => {
|
|
33
|
+
const tag = new Tag({ value: " . " });
|
|
34
|
+
expect(tag.dynamic).toBe(false);
|
|
35
|
+
expect(tag.key).toBe(".");
|
|
36
|
+
expect(tag.raw).toBe(false);
|
|
37
|
+
expect(tag.type).toBe("implicit");
|
|
38
|
+
expect(tag.value).toBe(".");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("variable", () => {
|
|
42
|
+
const tag = new Tag({ value: "a" });
|
|
43
|
+
expect(tag.dynamic).toBe(false);
|
|
44
|
+
expect(tag.key).toBe("a");
|
|
45
|
+
expect(tag.raw).toBe(false);
|
|
46
|
+
expect(tag.type).toBe("variable");
|
|
47
|
+
expect(tag.value).toBe("a");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("implicit", () => {
|
|
51
|
+
const tag = new Tag({ value: "." });
|
|
52
|
+
expect(tag.dynamic).toBe(false);
|
|
53
|
+
expect(tag.key).toBe(".");
|
|
54
|
+
expect(tag.raw).toBe(false);
|
|
55
|
+
expect(tag.type).toBe("implicit");
|
|
56
|
+
expect(tag.value).toBe(".");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("triple braces raw implicit", () => {
|
|
60
|
+
const tag = new Tag({
|
|
61
|
+
rawBraces: true,
|
|
62
|
+
value: ".",
|
|
63
|
+
});
|
|
64
|
+
expect(tag.dynamic).toBe(false);
|
|
65
|
+
expect(tag.key).toBe(".");
|
|
66
|
+
expect(tag.raw).toBe(true);
|
|
67
|
+
expect(tag.type).toBe("implicit");
|
|
68
|
+
expect(tag.value).toBe(".");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("triple braces raw variable", () => {
|
|
72
|
+
const tag = new Tag({ rawBraces: true, value: "name" });
|
|
73
|
+
expect(tag.raw).toBe(true);
|
|
74
|
+
expect(tag.type).toBe("variable");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("partial", () => {
|
|
78
|
+
const tag = new Tag({ value: "> p" });
|
|
79
|
+
expect(tag.dynamic).toBe(false);
|
|
80
|
+
expect(tag.key).toBe("p");
|
|
81
|
+
expect(tag.raw).toBe(false);
|
|
82
|
+
expect(tag.type).toBe("partial");
|
|
83
|
+
expect(tag.value).toBe(">p");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("section", () => {
|
|
87
|
+
const tag = new Tag({ value: " #s" });
|
|
88
|
+
expect(tag.dynamic).toBe(false);
|
|
89
|
+
expect(tag.key).toBe("s");
|
|
90
|
+
expect(tag.raw).toBe(false);
|
|
91
|
+
expect(tag.type).toBe("section");
|
|
92
|
+
expect(tag.value).toBe("#s");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("end", () => {
|
|
96
|
+
const tag = new Tag({
|
|
97
|
+
value: " / E",
|
|
98
|
+
});
|
|
99
|
+
expect(tag.dynamic).toBe(false);
|
|
100
|
+
expect(tag.key).toBe("E");
|
|
101
|
+
expect(tag.raw).toBe(false);
|
|
102
|
+
expect(tag.type).toBe("end");
|
|
103
|
+
expect(tag.value).toBe("/E");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("raw", () => {
|
|
107
|
+
const tag = new Tag({
|
|
108
|
+
value: " & r ",
|
|
109
|
+
});
|
|
110
|
+
expect(tag.dynamic).toBe(false);
|
|
111
|
+
expect(tag.key).toBe("r");
|
|
112
|
+
expect(tag.raw).toBe(true);
|
|
113
|
+
expect(tag.type).toBe("variable");
|
|
114
|
+
expect(tag.value).toBe("&r");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("inverted", () => {
|
|
118
|
+
const tag = new Tag({
|
|
119
|
+
value: " ^ iv ",
|
|
120
|
+
});
|
|
121
|
+
expect(tag.dynamic).toBe(false);
|
|
122
|
+
expect(tag.key).toBe("iv");
|
|
123
|
+
expect(tag.raw).toBe(false);
|
|
124
|
+
expect(tag.type).toBe("inverted");
|
|
125
|
+
expect(tag.value).toBe("^iv");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("comment", () => {
|
|
129
|
+
const tag = new Tag({
|
|
130
|
+
value: " ! c ",
|
|
131
|
+
});
|
|
132
|
+
expect(tag.dynamic).toBe(false);
|
|
133
|
+
expect(tag.key).toBe("c");
|
|
134
|
+
expect(tag.raw).toBe(false);
|
|
135
|
+
expect(tag.type).toBe("comment");
|
|
136
|
+
expect(tag.value).toBe("!c");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("Tag: content", () => {
|
|
141
|
+
it("variable: default is null", () => {
|
|
142
|
+
const t = new Tag({
|
|
143
|
+
value: "name",
|
|
144
|
+
});
|
|
145
|
+
expect(t.type).toBe("variable");
|
|
146
|
+
expect(t.content).toBe(null);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("variable: append has no effect", () => {
|
|
150
|
+
const t = new Tag({
|
|
151
|
+
value: "name",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
t.appendContent("CONTENT");
|
|
155
|
+
expect(t.type).toBe("variable");
|
|
156
|
+
expect(t.content).toBe(null);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("content: default is ''", () => {
|
|
160
|
+
const t = new Tag({
|
|
161
|
+
value: "#name",
|
|
162
|
+
});
|
|
163
|
+
expect(t.type).toBe("section");
|
|
164
|
+
expect(t.content).toBe("");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("content: can append coontent", () => {
|
|
168
|
+
const t = new Tag({
|
|
169
|
+
value: "#name",
|
|
170
|
+
});
|
|
171
|
+
expect(t.type).toBe("section");
|
|
172
|
+
expect(t.content).toBe("");
|
|
173
|
+
|
|
174
|
+
t.appendContent("_CONTENT1");
|
|
175
|
+
expect(t.content).toBe("_CONTENT1");
|
|
176
|
+
|
|
177
|
+
t.appendContent("_CONTENT2");
|
|
178
|
+
expect(t.content).toBe("_CONTENT1_CONTENT2");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("Tag: context", () => {
|
|
183
|
+
it("gets context", () => {
|
|
184
|
+
const t = new Tag({
|
|
185
|
+
value: "name",
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("implicit iterator", () => {
|
|
191
|
+
it("triple-brace {{{.}}} produces type implicit with raw true", () => {
|
|
192
|
+
const tag = Tag.parse("{{{.}}}");
|
|
193
|
+
expect(tag?.type).toBe("implicit");
|
|
194
|
+
expect(tag?.raw).toBe(true);
|
|
195
|
+
expect(tag?.key).toBe(".");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("ampersand {{&.}} produces type implicit with raw true", () => {
|
|
199
|
+
const tag = Tag.parse("{{&.}}");
|
|
200
|
+
expect(tag?.type).toBe("implicit");
|
|
201
|
+
expect(tag?.raw).toBe(true);
|
|
202
|
+
expect(tag?.key).toBe(".");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("Tag.copy", () => {
|
|
207
|
+
it("copies a tag and replaces value", () => {
|
|
208
|
+
const tag = new Tag({ value: "> one.two ?" });
|
|
209
|
+
const copy = Tag.copy(tag, { key: "three" });
|
|
210
|
+
expect(copy.valueOption).toBe("> three ?");
|
|
211
|
+
});
|
|
212
|
+
});
|