@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,142 @@
1
+ import { TextDecoderStream } from "node:stream/web";
2
+ import { Template } from "./Template";
3
+ import { createReadableStream } from "../test/streams";
4
+ import type { ContextTypes } from "../../types";
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ (process.env as any).LOG_LEVEL = "debug";
8
+
9
+ describe("Template sections", () => {
10
+ async function render(
11
+ template: string,
12
+ context: ContextTypes
13
+ ): Promise<string[]> {
14
+ return new Promise<string[]>((resolve) => {
15
+ const calls: string[] = [];
16
+ new Template({
17
+ contextProvider: { context },
18
+ readable: createReadableStream(template).pipeThrough(
19
+ new TextDecoderStream()
20
+ ),
21
+ writeToOutput: async (text) => {
22
+ calls.push(text);
23
+ },
24
+ })
25
+ .on("inactive", () => resolve(calls))
26
+ .read();
27
+ });
28
+ }
29
+
30
+ it("renders when context is true", async () => {
31
+ expect(await render("{{#s}}text{{/s}}", { s: true })).toEqual(["text"]);
32
+ });
33
+
34
+ it("renders nothing when context is false", async () => {
35
+ expect(await render("{{#s}}text{{/s}}", { s: false })).toEqual([]);
36
+ });
37
+
38
+ it("renders nothing when context is empty list", async () => {
39
+ expect(await render("{{#s}}text{{/s}}", { s: [] })).toEqual([]);
40
+ });
41
+
42
+ it("renders with object context", async () => {
43
+ expect(
44
+ await render("{{#s}}{{name}}{{/s}}", { s: { name: "Alice" } })
45
+ ).toEqual(["Alice"]);
46
+ });
47
+
48
+ it("renders once per item in a list", async () => {
49
+ expect(
50
+ await render("{{#s}}{{name}}{{/s}}", {
51
+ s: [{ name: "Alice" }, { name: "Bob" }],
52
+ })
53
+ ).toEqual(["Alice", "Bob"]);
54
+ });
55
+
56
+ it("renders implicit iterator per item in a list", async () => {
57
+ expect(await render("{{#s}}{{.}}{{/s}}", { s: ["a", "b"] })).toEqual([
58
+ "a",
59
+ "b",
60
+ ]);
61
+ });
62
+
63
+ it("renders using a lambda", async () => {
64
+ expect(
65
+ await render("{{#s}}text{{/s}}", {
66
+ s: (content: string) => `<em>${content}</em>`,
67
+ })
68
+ ).toEqual(["<em>text</em>"]);
69
+ });
70
+
71
+ it("renders nested sections", async () => {
72
+ expect(
73
+ await render("{{#outer}}{{#inner}}text{{/inner}}{{/outer}}", {
74
+ outer: { inner: true },
75
+ })
76
+ ).toEqual(["text"]);
77
+ });
78
+
79
+ it("falls back to parent context for missing keys", async () => {
80
+ expect(
81
+ await render("{{#s}}{{name}}{{/s}}", { s: true, name: "Alice" })
82
+ ).toEqual(["Alice"]);
83
+ });
84
+
85
+ it("flushes pending whitespace before section tag", async () => {
86
+ expect(await render(" {{#s}}text{{/s}}", { s: true })).toEqual([
87
+ " ",
88
+ "text",
89
+ ]);
90
+ });
91
+
92
+ it("renders surrounding text with nested sections", async () => {
93
+ expect(
94
+ await render(
95
+ "a {{#outer}} L {{#inner}}text{{/inner}} N {{/outer}} z",
96
+ { outer: { inner: true } }
97
+ )
98
+ ).toEqual(["a ", " L ", "text", " N ", " z"]);
99
+ });
100
+
101
+ describe("standalone suppression", () => {
102
+ it("suppresses standalone section tag lines", async () => {
103
+ expect((await render("{{#s}}\nbody\n{{/s}}\n", { s: true })).join("")).toBe("body\n");
104
+ });
105
+
106
+ it("suppresses standalone section tag lines when context is false", async () => {
107
+ expect((await render("{{#s}}\nbody\n{{/s}}\n", { s: false })).join("")).toBe("");
108
+ });
109
+
110
+ it("suppresses standalone section in middle of content", async () => {
111
+ expect((await render("before\n{{#s}}\nbody\n{{/s}}\nafter", { s: true })).join("")).toBe("before\nbody\nafter");
112
+ });
113
+
114
+ it("suppresses standalone section in middle of content when context is false", async () => {
115
+ expect((await render("before\n{{#s}}\nbody\n{{/s}}\nafter", { s: false })).join("")).toBe("before\nafter");
116
+ });
117
+
118
+ it("suppresses leading whitespace on a standalone section tag line", async () => {
119
+ expect((await render(" {{#s}}\nbody\n{{/s}}\nafter", { s: true })).join("")).toBe("body\nafter");
120
+ });
121
+
122
+ it("suppresses standalone section tag lines with leading whitespace when context is false", async () => {
123
+ expect((await render(" {{#s}}\nbody\n{{/s}}\nafter", { s: false })).join("")).toBe("after");
124
+ });
125
+
126
+ it("flushes pending whitespace for a non-standalone section tag", async () => {
127
+ expect((await render(" {{#s}}text{{/s}}\n", { s: true })).join("")).toBe(" text\n");
128
+ });
129
+
130
+ it("does not suppress when content precedes the opening tag", async () => {
131
+ expect((await render("a {{#s}}body{{/s}}\nz", { s: true })).join("")).toBe("a body\nz");
132
+ });
133
+
134
+ it("suppresses standalone nested section tag lines", async () => {
135
+ expect((await render("{{#a}}\n{{#b}}\nbody\n{{/b}}\n{{/a}}\n", { a: true, b: true })).join("")).toBe("body\n");
136
+ });
137
+
138
+ it("suppresses standalone section containing a standalone comment", async () => {
139
+ expect((await render("{{#s}}\n{{! comment }}\n{{/s}}", { s: true })).join("")).toBe("");
140
+ });
141
+ });
142
+ });
@@ -0,0 +1,178 @@
1
+ import { TextDecoderStream } from "node:stream/web";
2
+ import { Template } from "./Template";
3
+ import { createReadableStream } from "../test/streams";
4
+ import type { ContextTypes } from "../../types";
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ (process.env as any).LOG_LEVEL = "debug";
8
+
9
+ describe("Template", () => {
10
+ it("writes text", async () => {
11
+ let resolve: () => void;
12
+ const mockWriteToOutput = vitest.fn();
13
+ new Template({
14
+ readable: createReadableStream("hello").pipeThrough(
15
+ new TextDecoderStream()
16
+ ),
17
+ writeToOutput: mockWriteToOutput,
18
+ })
19
+ .on("inactive", () => resolve())
20
+ .read();
21
+
22
+ await new Promise<void>((res) => {
23
+ resolve = res;
24
+ });
25
+
26
+ expect(mockWriteToOutput).toHaveBeenCalledTimes(1);
27
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(1, "hello");
28
+ });
29
+
30
+ it("processes a simple template", async () => {
31
+ let resolve: () => void;
32
+ const mockWriteToOutput = vitest.fn();
33
+ new Template({
34
+ contextProvider: { context: { m: "HELLO WORLD" } },
35
+ readable: createReadableStream("ab{{m}}yz").pipeThrough(
36
+ new TextDecoderStream()
37
+ ),
38
+ writeToOutput: mockWriteToOutput,
39
+ })
40
+ .on("inactive", () => resolve())
41
+ .read();
42
+
43
+ await new Promise<void>((res) => {
44
+ resolve = res;
45
+ });
46
+
47
+ expect(mockWriteToOutput).toHaveBeenCalledTimes(3);
48
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(1, "ab");
49
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(2, "HELLO WORLD");
50
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(3, "yz");
51
+ });
52
+
53
+ it("flushes pending whitespace before variable tag", async () => {
54
+ let resolve: () => void;
55
+ const mockWriteToOutput = vitest.fn();
56
+ new Template({
57
+ contextProvider: { context: { name: "Alice" } },
58
+ readable: createReadableStream(" {{name}}").pipeThrough(
59
+ new TextDecoderStream()
60
+ ),
61
+ writeToOutput: mockWriteToOutput,
62
+ })
63
+ .on("inactive", () => resolve())
64
+ .read();
65
+
66
+ await new Promise<void>((res) => {
67
+ resolve = res;
68
+ });
69
+
70
+ expect(mockWriteToOutput).toHaveBeenCalledTimes(2);
71
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(1, " ");
72
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(2, "Alice");
73
+ });
74
+
75
+ describe("implicit iterator", () => {
76
+ async function render(
77
+ template: string,
78
+ context: ContextTypes
79
+ ): Promise<string[]> {
80
+ return new Promise<string[]>((resolve) => {
81
+ const calls: string[] = [];
82
+ new Template({
83
+ contextProvider: { context },
84
+ readable: createReadableStream(template).pipeThrough(
85
+ new TextDecoderStream()
86
+ ),
87
+ writeToOutput: async (text) => {
88
+ calls.push(text);
89
+ },
90
+ })
91
+ .on("inactive", () => resolve(calls))
92
+ .read();
93
+ });
94
+ }
95
+
96
+ it("renders a string context", async () => {
97
+ expect(await render("{{.}}", "hello")).toEqual(["hello"]);
98
+ });
99
+
100
+ it("renders a number context", async () => {
101
+ expect(await render("{{.}}", 42)).toEqual(["42"]);
102
+ });
103
+
104
+ it("HTML-escapes the context value", async () => {
105
+ expect(await render("{{.}}", '& " < >')).toEqual([
106
+ "&amp; &quot; &lt; &gt;",
107
+ ]);
108
+ });
109
+
110
+ it("renders surrounding text alongside the implicit value", async () => {
111
+ expect(await render("Hello, {{.}}!", "world")).toEqual([
112
+ "Hello, ",
113
+ "world",
114
+ "!",
115
+ ]);
116
+ });
117
+
118
+ it("produces no output when context is an object", async () => {
119
+ expect(await render("{{.}}", { name: "rich" })).toEqual([]);
120
+ });
121
+
122
+ it("flushes pending whitespace before implicit tag", async () => {
123
+ expect(await render(" {{.}}", "hello")).toEqual([" ", "hello"]);
124
+ });
125
+
126
+ it("triple-brace {{{.}}} outputs without HTML escaping", async () => {
127
+ expect(await render("{{{.}}}", '& " < >')).toEqual(['& " < >']);
128
+ });
129
+
130
+ it("ampersand {{&.}} outputs without HTML escaping", async () => {
131
+ expect(await render("{{&.}}", '& " < >')).toEqual(['& " < >']);
132
+ });
133
+ });
134
+
135
+ it("flushes trailing whitespace at end of template", async () => {
136
+ let resolve: () => void;
137
+ const mockWriteToOutput = vitest.fn();
138
+ new Template({
139
+ readable: createReadableStream("hello\n ").pipeThrough(
140
+ new TextDecoderStream()
141
+ ),
142
+ writeToOutput: mockWriteToOutput,
143
+ })
144
+ .on("inactive", () => resolve())
145
+ .read();
146
+
147
+ await new Promise<void>((res) => {
148
+ resolve = res;
149
+ });
150
+
151
+ expect(mockWriteToOutput).toHaveBeenCalledTimes(2);
152
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(1, "hello\n");
153
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(2, " ");
154
+ });
155
+
156
+ it.skip("processes a nested, implicit, template", async () => {
157
+ let resolve: () => void;
158
+ const mockWriteToOutput = vitest.fn();
159
+ new Template({
160
+ contextProvider: { context: { m: "HELLO WORLD" } },
161
+ readable: createReadableStream(
162
+ "aa{{#ee}}{{#.}}{{.}}{{/.}}{{/ee}}zz"
163
+ ).pipeThrough(new TextDecoderStream()),
164
+ writeToOutput: mockWriteToOutput,
165
+ })
166
+ .on("inactive", () => resolve())
167
+ .read();
168
+
169
+ await new Promise<void>((res) => {
170
+ resolve = res;
171
+ });
172
+
173
+ expect(mockWriteToOutput).toHaveBeenCalledTimes(3);
174
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(1, "ab");
175
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(2, "HELLO WORLD");
176
+ expect(mockWriteToOutput).toHaveBeenNthCalledWith(3, "yz");
177
+ });
178
+ });