@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,614 @@
1
+ import { ReadableStream } from "node:stream/web";
2
+ import { EventEmitter } from "node:events";
3
+ import { escape } from "lodash";
4
+ import { Log } from "@funcstache/logger";
5
+ import type {
6
+ Context,
7
+ ContextProvider,
8
+ ContextTypes,
9
+ PartialTagContext,
10
+ SectionTagCallback,
11
+ Tag as TagType,
12
+ VariableTagContext,
13
+ VariableTagContextLambda,
14
+ WriteToOutput,
15
+ } from "../types";
16
+ import { Parse } from "../parse/Parse";
17
+ import { Queue, type TokenizeAllEvent } from "../queue/Queue";
18
+ import {
19
+ Tokenize,
20
+ type TokenizeTagEvent,
21
+ type TokenizeTextEvent,
22
+ } from "../tokenize/Tokenize";
23
+
24
+ let instance = 0;
25
+
26
+ export class Template
27
+ extends EventEmitter<{ inactive: [undefined] }>
28
+ implements ContextProvider
29
+ {
30
+ #logger = new Log({
31
+ category: "TML",
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ level: (process.env as any).LOG_LEVEL || "warn",
34
+ });
35
+
36
+ #_parse: Parse | undefined;
37
+ #_queue: Queue | undefined;
38
+ #_tokenize: Tokenize | undefined;
39
+ /** The context for this template. AKA "hash" in the mustache documentation. */
40
+ #contextProvider: ContextProvider | undefined;
41
+ #consumeNextNewline = false;
42
+ #instance: number;
43
+ #lineHasContent = false;
44
+ #pendingWhitespace = "";
45
+ #readable: ReadableStream<string>;
46
+ #writeToOutput: WriteToOutput;
47
+
48
+ constructor({
49
+ contextProvider,
50
+ readable,
51
+ writeToOutput,
52
+ ...options
53
+ }: ConstructorParameters<
54
+ typeof EventEmitter<{ inactive: [undefined] }>
55
+ >[0] & {
56
+ contextProvider?: ContextProvider;
57
+ readable: ReadableStream<string>;
58
+ writeToOutput: WriteToOutput;
59
+ }) {
60
+ super(options);
61
+
62
+ instance++;
63
+ this.#instance = instance;
64
+
65
+ if (contextProvider) {
66
+ this.#contextProvider = contextProvider;
67
+ }
68
+
69
+ this.#readable = readable;
70
+ this.#writeToOutput = (...args: Parameters<WriteToOutput>) => {
71
+ this.#logger.debug(() => [
72
+ `~~~ ctor.writeToOutput[${this.#instance}]: args=`,
73
+ args,
74
+ ]);
75
+ return writeToOutput(...args);
76
+ };
77
+ }
78
+
79
+ async getContextValue<CTX extends ContextTypes>(
80
+ tag: TagType
81
+ ): Promise<CTX | undefined> {
82
+ // this.#logger.debug(() => [
83
+ // `~~~ getContextValue[${this.#instance}]: tag='${
84
+ // tag.value
85
+ // }', this.context='\n%o\n'`,
86
+ // this.context,
87
+ // ]);
88
+
89
+ let found = false;
90
+ let value: CTX | undefined;
91
+ if (isContext(this.context)) {
92
+ ({ found, value } = await this.#findContextForTag<CTX>(
93
+ tag,
94
+ this.context
95
+ ));
96
+ }
97
+
98
+ // this.#logger.debug(
99
+ // () =>
100
+ // `~~~ getContextValue[${
101
+ // this.#instance
102
+ // }]: found='${found}', value='${value}'`
103
+ // );
104
+
105
+ if (!found) {
106
+ if (this.#contextProvider?.getContextValue) {
107
+ value = await this.#contextProvider.getContextValue(tag);
108
+ }
109
+ }
110
+
111
+ // this.#logger.debug(() => [
112
+ // `~~~ getContextValue[${this.#instance}]: tag='${
113
+ // tag.value
114
+ // }', value='${value}'`,
115
+ // this.context,
116
+ // ]);
117
+
118
+ return value;
119
+ }
120
+
121
+ read(): Template {
122
+ this.#parse.read(this.#readable);
123
+ return this;
124
+ }
125
+
126
+ get context(): ContextTypes {
127
+ return this.#contextProvider?.context || {};
128
+ }
129
+
130
+ get #parse(): Parse {
131
+ if (!this.#_parse) {
132
+ this.#_parse = new Parse({
133
+ onChar: this.#tokenize.push.bind(this.#tokenize),
134
+ });
135
+ }
136
+
137
+ return this.#_parse;
138
+ }
139
+
140
+ get #queue(): Queue {
141
+ if (!this.#_queue) {
142
+ this.#_queue = new Queue()
143
+ .on("error", (err) => {
144
+ throw err;
145
+ })
146
+ .on("comment", this.#handleComment.bind(this))
147
+ .on("implicit", this.#handleImplicit.bind(this))
148
+ .on("inactive", this.#handleInactive.bind(this))
149
+ .on("inverted", this.#handleInverted.bind(this))
150
+ .on("partial", this.#handlePartial.bind(this))
151
+ .on("section", this.#handleSection.bind(this))
152
+ .on("text", this.#handleText.bind(this))
153
+ .on("variable", this.#handleVariable.bind(this));
154
+ }
155
+
156
+ return this.#_queue;
157
+ }
158
+
159
+ get #tokenize(): Tokenize {
160
+ if (!this.#_tokenize) {
161
+ this.#_tokenize = new Tokenize().on(
162
+ "token",
163
+ this.#queue.push.bind(this.#queue)
164
+ );
165
+ }
166
+
167
+ return this.#_tokenize;
168
+ }
169
+
170
+ async #applyStandaloneState(
171
+ bodyEvents: TokenizeAllEvent[]
172
+ ): Promise<boolean> {
173
+ const firstBodyEvent = bodyEvents[0];
174
+ const isStandalone =
175
+ !this.#lineHasContent &&
176
+ firstBodyEvent !== undefined &&
177
+ firstBodyEvent.type === "text" &&
178
+ /^(\r\n|\n)/.test(firstBodyEvent.data.toString());
179
+
180
+ if (isStandalone) {
181
+ this.#pendingWhitespace = "";
182
+ this.#consumeNextNewline = true;
183
+ } else {
184
+ await this.#flushPendingWhitespace();
185
+ }
186
+
187
+ return isStandalone;
188
+ }
189
+
190
+ /**
191
+ * Gets the context for a tag. This is a complex operation so it's been broken into several
192
+ * functions. If context is not found in this template's context an attempt will be made to find
193
+ * the context in this Template's parent - if it exists.
194
+ * @param tag Find context for this tag.
195
+ * @param context The context provided to the template.
196
+ */
197
+ async #findContextForTag<CTX extends ContextTypes>(
198
+ tag: TagType,
199
+ context: Context
200
+ ): Promise<{ found: boolean; value: CTX | undefined }> {
201
+ // this.#logger.debug(() => [
202
+ // `~~~ #findContextForTag[${this.#instance}]: tag='${
203
+ // tag.value
204
+ // }', this.context='\n%o\n'`,
205
+ // this.context,
206
+ // ]);
207
+
208
+ let ctx = context;
209
+ let found = false;
210
+ let keys = tag.key.split(".");
211
+ let value: Context | ContextTypes | undefined = undefined;
212
+ while (keys.length) {
213
+ try {
214
+ ({ found = true, keys, value } = this.#getFromContext(keys, ctx));
215
+
216
+ // this.#logger.debug(
217
+ // () =>
218
+ // `~~~ #findContextForTag[${
219
+ // this.#instance
220
+ // }]: found='${found}', keys='${keys}', value=${value}`
221
+ // );
222
+
223
+ if (!found) {
224
+ break;
225
+ }
226
+
227
+ if (keys.length < 1) {
228
+ // No keys left, return the value.
229
+ found = true;
230
+ break;
231
+ }
232
+
233
+ if (typeof value === "function") {
234
+ // We got a function for the key and there are more keys, invoke the lambda function and
235
+ // expect the result to be an object we can use for the remainder of the keys.
236
+ ctx = (await (value as VariableTagContextLambda)()) as Context;
237
+ continue;
238
+ }
239
+
240
+ ctx = value as Context;
241
+ } catch (err) {
242
+ this.#logger.error(() => [
243
+ `#findContextForTag[${this.#instance}]: error;\n tag='${
244
+ tag.value
245
+ }',\n keys='%0',\n ctx=%1,\n err=%2`,
246
+ keys,
247
+ ctx,
248
+ err,
249
+ ]);
250
+ break;
251
+ }
252
+ }
253
+
254
+ // this.#logger.debug(
255
+ // () =>
256
+ // `~~~ #findContextForTag[${
257
+ // this.#instance
258
+ // }]: found='${found}', value=${value}`
259
+ // );
260
+
261
+ return { found, value: value as CTX };
262
+ }
263
+
264
+ #getFromContext(keys: string[], context: Context | undefined) {
265
+ if (isContext(context)) {
266
+ const key = keys[0];
267
+ if (!key) {
268
+ throw Error(`No keys were provided.`);
269
+ }
270
+
271
+ if (!(key in context)) {
272
+ return { found: false, keys };
273
+ }
274
+
275
+ const value = context[key];
276
+
277
+ if (isPrimitive(value)) {
278
+ if (1 < keys.length) {
279
+ throw Error(
280
+ `key '${key}', returned '${typeof value}' from context however, more keys remain so expected an object.`
281
+ );
282
+ }
283
+
284
+ return { value, keys: [] };
285
+ }
286
+
287
+ return { value, keys: keys.slice(1) };
288
+ }
289
+
290
+ throw new Error(
291
+ `Expect an object for context, received a '${typeof context}'.`
292
+ );
293
+ }
294
+
295
+ async #handleComment(): Promise<void> {
296
+ if (!this.#lineHasContent) {
297
+ this.#pendingWhitespace = "";
298
+ this.#consumeNextNewline = true;
299
+ } else {
300
+ await this.#flushPendingWhitespace();
301
+ }
302
+ }
303
+
304
+ async #handleInverted(events: TokenizeAllEvent[]): Promise<void> {
305
+ const bodyEvents = events.slice(1, -1);
306
+ const isStandalone = await this.#applyStandaloneState(bodyEvents);
307
+
308
+ const startEvent = events[0];
309
+ if (!startEvent || startEvent.type !== "tag") {
310
+ return;
311
+ }
312
+
313
+ const value = await this.getContextValue(startEvent.data);
314
+
315
+ const isFalsy =
316
+ value === undefined ||
317
+ value === false ||
318
+ value === "" ||
319
+ (Array.isArray(value) && value.length === 0);
320
+
321
+ if (!isFalsy) {
322
+ return;
323
+ }
324
+
325
+ let templateStr = bodyEvents.map((e) => e.data.toString()).join("");
326
+ if (isStandalone) {
327
+ templateStr = stripLeadingNewline(templateStr);
328
+ }
329
+
330
+ await this.#renderSectionContent(templateStr, this.context);
331
+ }
332
+
333
+ async #handleImplicit({ data: tag }: TokenizeTagEvent): Promise<void> {
334
+ await this.#flushPendingWhitespace();
335
+ const ctx = this.context;
336
+
337
+ if (isPrimitive(ctx)) {
338
+ this.#writeToOutput(tag.raw ? "" + ctx : escape("" + ctx));
339
+ }
340
+ }
341
+
342
+ async #handleInactive(): Promise<void> {
343
+ await this.#flushPendingWhitespace();
344
+ this.emit("inactive", undefined);
345
+ }
346
+
347
+ async #handlePartial(event: TokenizeTagEvent): Promise<void> {
348
+ const partialContext = await this.getContextValue<PartialTagContext>(
349
+ event.data
350
+ );
351
+ if (typeof partialContext !== "function") {
352
+ await this.#flushPendingWhitespace();
353
+ return;
354
+ }
355
+
356
+ const isStandalone = !this.#lineHasContent;
357
+ const indent = isStandalone ? this.#pendingWhitespace : "";
358
+
359
+ if (isStandalone) {
360
+ this.#pendingWhitespace = "";
361
+ this.#consumeNextNewline = true;
362
+ } else {
363
+ await this.#flushPendingWhitespace();
364
+ }
365
+
366
+ const { input, context } = await partialContext(event.data);
367
+ const readable = await input();
368
+
369
+ await this.#renderPartialContent(
370
+ readable as ReadableStream<string>,
371
+ context,
372
+ indent
373
+ );
374
+ }
375
+
376
+ async #handleSection(events: TokenizeAllEvent[]): Promise<void> {
377
+ const bodyEvents = events.slice(1, -1);
378
+ const isStandalone = await this.#applyStandaloneState(bodyEvents);
379
+
380
+ const startEvent = events[0];
381
+ if (!startEvent || startEvent.type !== "tag") {
382
+ return;
383
+ }
384
+
385
+ const sectionContext =
386
+ startEvent.data.key === "."
387
+ ? this.context
388
+ : await this.getContextValue(startEvent.data);
389
+
390
+ if (
391
+ sectionContext === undefined ||
392
+ sectionContext === false ||
393
+ sectionContext === "" ||
394
+ (Array.isArray(sectionContext) && sectionContext.length === 0)
395
+ ) {
396
+ return;
397
+ }
398
+
399
+ let templateStr = bodyEvents.map((e) => e.data.toString()).join("");
400
+ if (isStandalone) {
401
+ templateStr = stripLeadingNewline(templateStr);
402
+ }
403
+
404
+ if (isSectionLambda(sectionContext)) {
405
+ // TODO: make `SectionTagCallback` async
406
+ await this.#writeToOutput(sectionContext(templateStr));
407
+ return;
408
+ }
409
+
410
+ const items: ContextTypes[] = Array.isArray(sectionContext)
411
+ ? sectionContext
412
+ : [sectionContext];
413
+
414
+ for (const item of items) {
415
+ await this.#renderSectionContent(templateStr, item);
416
+ }
417
+ }
418
+
419
+ async #renderSectionContent(
420
+ templateStr: string,
421
+ context: ContextTypes
422
+ ): Promise<void> {
423
+ return new Promise<void>((resolve) => {
424
+ const childContextProvider: ContextProvider = {
425
+ context,
426
+ getContextValue: <CTX extends ContextTypes>(
427
+ tag: TagType
428
+ ): Promise<CTX | undefined> => this.getContextValue<CTX>(tag),
429
+ };
430
+
431
+ new Template({
432
+ contextProvider: childContextProvider,
433
+ readable: stringToReadableStream(templateStr),
434
+ writeToOutput: this.#writeToOutput,
435
+ })
436
+ .on("inactive", () => resolve())
437
+ .read();
438
+ });
439
+ }
440
+
441
+ async #flushPendingWhitespace(): Promise<void> {
442
+ if (this.#pendingWhitespace) {
443
+ await this.#writeToOutput(this.#pendingWhitespace);
444
+ this.#pendingWhitespace = "";
445
+ }
446
+ }
447
+
448
+ async #handleText(event: TokenizeTextEvent): Promise<void> {
449
+ let text = event.data;
450
+ // After a standalone partial the trailing newline belongs to the partial tag, not the output.
451
+ if (this.#consumeNextNewline) {
452
+ if (text.startsWith("\r\n")) {
453
+ text = text.slice(2);
454
+ } else if (text.startsWith("\n")) {
455
+ text = text.slice(1);
456
+ }
457
+ this.#consumeNextNewline = false;
458
+ if (!text) {
459
+ return;
460
+ }
461
+ }
462
+
463
+ const lastNewlineEnd = lastNewlineIndex(text);
464
+ if (lastNewlineEnd !== -1) {
465
+ // Flush everything up to and including the last newline, then reset line state.
466
+ await this.#writeToOutput(
467
+ this.#pendingWhitespace + text.slice(0, lastNewlineEnd)
468
+ );
469
+ this.#pendingWhitespace = "";
470
+ this.#lineHasContent = false;
471
+ text = text.slice(lastNewlineEnd);
472
+ if (!text) {
473
+ return;
474
+ }
475
+ }
476
+
477
+ // text contains no newlines — process as current-line fragment.
478
+ if (isHorizontalWhitespace(text)) {
479
+ this.#pendingWhitespace += text;
480
+ } else {
481
+ await this.#writeToOutput(this.#pendingWhitespace + text);
482
+ this.#pendingWhitespace = "";
483
+ this.#lineHasContent = true;
484
+ }
485
+ }
486
+
487
+ async #renderPartialContent(
488
+ readable: ReadableStream<string>,
489
+ context: ContextTypes | undefined,
490
+ indent: string
491
+ ): Promise<void> {
492
+ return new Promise<void>((resolve) => {
493
+ const childContextProvider: ContextProvider = {
494
+ context: context ?? this.context,
495
+ getContextValue: <CTX extends ContextTypes>(
496
+ tag: TagType
497
+ ): Promise<CTX | undefined> => this.getContextValue<CTX>(tag),
498
+ };
499
+
500
+ const indentingWriter: WriteToOutput = indent
501
+ ? makeIndentingWriter(this.#writeToOutput, indent)
502
+ : this.#writeToOutput;
503
+
504
+ new Template({
505
+ contextProvider: childContextProvider,
506
+ readable,
507
+ writeToOutput: indentingWriter,
508
+ })
509
+ .on("inactive", () => resolve())
510
+ .read();
511
+ });
512
+ }
513
+
514
+ async #handleVariable({ data: tag }: TokenizeTagEvent): Promise<void> {
515
+ await this.#flushPendingWhitespace();
516
+ // this.#logger.debug(
517
+ // () => `~~~ #handleVariable[${this.#instance}]: tag=${tag.toString()}`
518
+ // );
519
+
520
+ let tagVariableContext = await this.getContextValue<VariableTagContext>(
521
+ tag
522
+ );
523
+
524
+ // this.#logger.debug(() => [
525
+ // `~~~ #handleVariable[${this.#instance}]: tagVariableContext=`,
526
+ // tagVariableContext,
527
+ // ]);
528
+
529
+ // If the variable context value is a function then invoke it to get the value.
530
+ if (typeof tagVariableContext === "function") {
531
+ tagVariableContext = await (
532
+ tagVariableContext as VariableTagContextLambda
533
+ )();
534
+ }
535
+
536
+ if (isPrimitive(tagVariableContext)) {
537
+ this.#writeToOutput(
538
+ tag.raw ? "" + tagVariableContext : escape("" + tagVariableContext)
539
+ );
540
+ } else {
541
+ this.#logger.log(
542
+ () =>
543
+ `#handleVariable[${this.#instance}]: variable tag '${
544
+ tag.valueOption
545
+ }' returns context value '${tagVariableContext}'`
546
+ );
547
+ }
548
+
549
+ return;
550
+ }
551
+ }
552
+
553
+ function isContext(obj: unknown): obj is Context {
554
+ return !!(
555
+ obj &&
556
+ typeof obj !== "boolean" &&
557
+ typeof obj !== "number" &&
558
+ typeof obj !== "string" &&
559
+ typeof obj !== "function" &&
560
+ !Array.isArray(obj)
561
+ );
562
+ }
563
+
564
+ function isHorizontalWhitespace(text: string): boolean {
565
+ return /^[ \t]*$/.test(text);
566
+ }
567
+
568
+ function isPrimitive(obj: unknown): obj is boolean | number | string {
569
+ return (
570
+ typeof obj === "boolean" ||
571
+ typeof obj === "number" ||
572
+ typeof obj === "string"
573
+ );
574
+ }
575
+
576
+ function isSectionLambda(value: ContextTypes): value is SectionTagCallback {
577
+ return !!value && typeof value === "function";
578
+ }
579
+
580
+ /** Returns the index of the first character after the last `\n` (or `\r\n`), or -1 if none. */
581
+ function lastNewlineIndex(text: string): number {
582
+ const index = text.lastIndexOf("\n");
583
+ if (index === -1) {
584
+ return -1;
585
+ }
586
+ return index + 1;
587
+ }
588
+
589
+ function makeIndentingWriter(
590
+ writeToOutput: WriteToOutput,
591
+ indent: string
592
+ ): WriteToOutput {
593
+ let atLineStart = true;
594
+ return async (text: string) => {
595
+ const indented =
596
+ (atLineStart ? indent : "") + text.replace(/\n(?!\n|$)/g, "\n" + indent);
597
+ atLineStart = text.endsWith("\n");
598
+ return writeToOutput(indented);
599
+ };
600
+ }
601
+
602
+ function stripLeadingNewline(str: string): string {
603
+ const index = str.startsWith("\r\n") ? 2 : str.startsWith("\n") ? 1 : 0;
604
+ return !index ? str : str.slice(index);
605
+ }
606
+
607
+ function stringToReadableStream(str: string): ReadableStream<string> {
608
+ return new ReadableStream<string>({
609
+ start(controller) {
610
+ controller.enqueue(str);
611
+ controller.close();
612
+ },
613
+ });
614
+ }
@@ -0,0 +1,36 @@
1
+ import { ReadableStream } from "node:stream/web";
2
+
3
+ export function createReadableStream(
4
+ source: string,
5
+ charCount = -1,
6
+ pause = 0
7
+ ) {
8
+ const encoder = new TextEncoder();
9
+ let sourceIndex = 0;
10
+ let pullCount = 0;
11
+
12
+ return new ReadableStream<AllowSharedBufferSource>({
13
+ pull: async (controller) => {
14
+ pullCount++;
15
+ if (source.length <= sourceIndex) {
16
+ controller.close();
17
+ return;
18
+ }
19
+
20
+ for (let i = 0; i < (0 < charCount ? charCount : source.length); i++) {
21
+ if (sourceIndex < source.length) {
22
+ controller.enqueue(encoder.encode(source.charAt(sourceIndex)));
23
+ } else {
24
+ controller.close();
25
+ return;
26
+ }
27
+
28
+ sourceIndex++;
29
+
30
+ if (0 < pause) {
31
+ await new Promise<void>((resolve) => setTimeout(resolve, pause));
32
+ }
33
+ }
34
+ },
35
+ });
36
+ }