@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.
Files changed (109) hide show
  1. package/.eslintrc.json +30 -0
  2. package/.swcrc +29 -0
  3. package/DEV.md +84 -0
  4. package/README.md +145 -0
  5. package/TASKS.md +13 -0
  6. package/TODO.md +28 -0
  7. package/docs/.nojekyll +1 -0
  8. package/docs/assets/hierarchy.js +1 -0
  9. package/docs/assets/highlight.css +120 -0
  10. package/docs/assets/icons.js +18 -0
  11. package/docs/assets/icons.svg +1 -0
  12. package/docs/assets/main.js +60 -0
  13. package/docs/assets/navigation.js +1 -0
  14. package/docs/assets/search.js +1 -0
  15. package/docs/assets/style.css +1633 -0
  16. package/docs/classes/StacheTransformStream.html +13 -0
  17. package/docs/hierarchy.html +1 -0
  18. package/docs/index.html +73 -0
  19. package/docs/interfaces/Context.html +3 -0
  20. package/docs/interfaces/ContextProvider.html +10 -0
  21. package/docs/interfaces/PartialTagContextLambda.html +11 -0
  22. package/docs/interfaces/PartialTagContextLambdaResult.html +7 -0
  23. package/docs/interfaces/SectionTagCallback.html +12 -0
  24. package/docs/interfaces/SectionTagContextRecord.html +4 -0
  25. package/docs/interfaces/Tag.html +45 -0
  26. package/docs/interfaces/VariableTagContextLambda.html +4 -0
  27. package/docs/interfaces/VariableTagContextRecord.html +3 -0
  28. package/docs/media/StacheStream.ts +79 -0
  29. package/docs/modules.html +1 -0
  30. package/docs/types/ContextTypes.html +3 -0
  31. package/docs/types/JsonType.html +2 -0
  32. package/docs/types/PartialTagContext.html +4 -0
  33. package/docs/types/SectionTagContext.html +4 -0
  34. package/docs/types/TemplateName.html +9 -0
  35. package/docs/types/VariableTagContext.html +4 -0
  36. package/docs/types/VariableTagContextPrimitive.html +3 -0
  37. package/docs-assets/images/context-dotted-found.png +0 -0
  38. package/docs-assets/images/context-dotted-not-found.png +0 -0
  39. package/docs-assets/images/context-not-found.png +0 -0
  40. package/package.json +3 -6
  41. package/project.json +26 -0
  42. package/src/global.d.ts +10 -0
  43. package/src/index.ts +67 -0
  44. package/src/lib/parse/Parse.spec.ts +50 -0
  45. package/src/lib/parse/Parse.ts +92 -0
  46. package/src/lib/parse/README.md +62 -0
  47. package/src/lib/plan_base_v2.md +33 -0
  48. package/src/lib/plan_comment.md +53 -0
  49. package/src/lib/plan_implicit-iterator.md +213 -0
  50. package/src/lib/plan_inverted-sections.md +160 -0
  51. package/src/lib/plan_partials.md +237 -0
  52. package/src/lib/plan_sections.md +167 -0
  53. package/src/lib/plan_stache-stream.md +110 -0
  54. package/src/lib/plan_whitespace.md +98 -0
  55. package/src/lib/queue/Queue.spec.ts +275 -0
  56. package/src/lib/queue/Queue.ts +253 -0
  57. package/src/lib/queue/README.md +110 -0
  58. package/src/lib/stache-stream/README.md +45 -0
  59. package/src/lib/stache-stream/StacheStream.spec.ts +107 -0
  60. package/src/lib/stache-stream/StacheStream.ts +79 -0
  61. package/src/lib/tag/README.md +95 -0
  62. package/src/lib/tag/Tag.spec.ts +212 -0
  63. package/src/lib/tag/Tag.ts +295 -0
  64. package/src/lib/template/README.md +102 -0
  65. package/src/lib/template/Template-comment.spec.ts +76 -0
  66. package/src/lib/template/Template-inverted-section.spec.ts +85 -0
  67. package/src/lib/template/Template-partials.spec.ts +125 -0
  68. package/src/lib/template/Template-section.spec.ts +142 -0
  69. package/src/lib/template/Template.spec.ts +178 -0
  70. package/src/lib/template/Template.ts +614 -0
  71. package/src/lib/test/streams.ts +36 -0
  72. package/src/lib/tokenize/README.md +97 -0
  73. package/src/lib/tokenize/Tokenize.spec.ts +364 -0
  74. package/src/lib/tokenize/Tokenize.ts +374 -0
  75. package/src/lib/{types.d.ts → types.ts} +73 -25
  76. package/tsconfig.json +21 -0
  77. package/tsconfig.lib.json +16 -0
  78. package/tsconfig.spec.json +21 -0
  79. package/typedoc.mjs +15 -0
  80. package/vite.config.ts +27 -0
  81. package/vitest.setup.ts +6 -0
  82. package/src/global.d.js +0 -8
  83. package/src/global.d.js.map +0 -1
  84. package/src/index.d.ts +0 -7
  85. package/src/index.js +0 -24
  86. package/src/index.js.map +0 -1
  87. package/src/lib/parse/Parse.d.ts +0 -14
  88. package/src/lib/parse/Parse.js +0 -79
  89. package/src/lib/parse/Parse.js.map +0 -1
  90. package/src/lib/queue/Queue.d.ts +0 -32
  91. package/src/lib/queue/Queue.js +0 -181
  92. package/src/lib/queue/Queue.js.map +0 -1
  93. package/src/lib/stache-stream/StacheStream.d.ts +0 -22
  94. package/src/lib/stache-stream/StacheStream.js +0 -71
  95. package/src/lib/stache-stream/StacheStream.js.map +0 -1
  96. package/src/lib/tag/Tag.d.ts +0 -33
  97. package/src/lib/tag/Tag.js +0 -231
  98. package/src/lib/tag/Tag.js.map +0 -1
  99. package/src/lib/template/Template.d.ts +0 -18
  100. package/src/lib/template/Template.js +0 -428
  101. package/src/lib/template/Template.js.map +0 -1
  102. package/src/lib/test/streams.d.ts +0 -2
  103. package/src/lib/test/streams.js +0 -39
  104. package/src/lib/test/streams.js.map +0 -1
  105. package/src/lib/tokenize/Tokenize.d.ts +0 -22
  106. package/src/lib/tokenize/Tokenize.js +0 -268
  107. package/src/lib/tokenize/Tokenize.js.map +0 -1
  108. package/src/lib/types.js +0 -33
  109. 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&amp;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
+ });