@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,97 @@
|
|
|
1
|
+
# Tokenize
|
|
2
|
+
|
|
3
|
+
`Tokenize` receives a character stream from `Parse` and groups characters into mustache tokens. It emits a `token` event for each plain-text run and each mustache tag it recognizes, then emits `null` to signal end-of-stream.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
### Character input
|
|
8
|
+
|
|
9
|
+
Characters are pushed one at a time via `push(char)`. Each call first updates the state machine (`#updateStatus`) then processes the character against the new state (`#processChar`). `null` signals end-of-stream and flushes any buffered text or tag before emitting a final `null` token.
|
|
10
|
+
|
|
11
|
+
### State machine
|
|
12
|
+
|
|
13
|
+
Each character moves the tokenizer through one of four states:
|
|
14
|
+
|
|
15
|
+
| State | Meaning |
|
|
16
|
+
| ------------------ | ------------------------------------------ |
|
|
17
|
+
| `outsideTag` | Reading plain text between tags |
|
|
18
|
+
| `tagEnteringMaybe` | Seen one or more `{`; may be opening a tag |
|
|
19
|
+
| `inTagValue` | Inside a tag, reading its key |
|
|
20
|
+
| `tagExitingMaybe` | Seen one or more `}`; may be closing a tag |
|
|
21
|
+
|
|
22
|
+
Transitions are driven by a declarative `stateModel` table. Each entry lists the conditions (flags derived from the current character and buffer lengths) that must be true or false for the transition to fire. `#updateStateFromModel` evaluates the table and returns the next state. An optional `instant` flag allows passing through an intermediate state in the same call.
|
|
23
|
+
|
|
24
|
+
### Buffers
|
|
25
|
+
|
|
26
|
+
Four buffers track the in-progress token:
|
|
27
|
+
|
|
28
|
+
| Buffer | Contents |
|
|
29
|
+
| ------------------- | ------------------------------------------- |
|
|
30
|
+
| `#leftBuffer` | Plain text accumulated outside any tag |
|
|
31
|
+
| `#leftTokenBuffer` | Opening brace characters (`{`, `{{`, `{{{`) |
|
|
32
|
+
| `#middleBuffer` | Tag key (the content between the braces) |
|
|
33
|
+
| `#rightTokenBuffer` | Closing brace characters |
|
|
34
|
+
|
|
35
|
+
A tag is complete when `#leftTokenBuffer.length === #rightTokenBuffer.length`. At that point the concatenated string is passed to `Tag.parse()` and a `TokenizeTagEvent` is emitted.
|
|
36
|
+
|
|
37
|
+
### Adjacent tags
|
|
38
|
+
|
|
39
|
+
When two tags appear back-to-back (e.g. `{{a}}{{b}}`), the closing `}` of the first tag and the opening `{` of the second arrive in sequence. The state machine transitions from `tagExitingMaybe` to `tagEnteringMaybe` when a `{` is seen and the exit brace count matches the enter brace count. `#processChar` detects this transition and calls `#emitTagToken()` to flush the completed first tag before the second tag starts accumulating.
|
|
40
|
+
|
|
41
|
+
### Finalization
|
|
42
|
+
|
|
43
|
+
When `push(null)` is called, any text remaining in `#leftBuffer` is emitted as a `text` token, `#emitTagToken()` is called to flush any in-progress tag, then a `null` token is emitted to signal end-of-stream.
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { Tokenize } from "./Tokenize";
|
|
49
|
+
import { Parse } from "../parse/Parse";
|
|
50
|
+
|
|
51
|
+
const tokenize = new Tokenize().on("token", async (token) => {
|
|
52
|
+
if (token === null) return; // end of stream
|
|
53
|
+
if (token.type === "text") {
|
|
54
|
+
/* plain text */
|
|
55
|
+
}
|
|
56
|
+
if (token.type === "tag") {
|
|
57
|
+
/* mustache tag */
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const parse = new Parse({ onChar: tokenize.push.bind(tokenize) });
|
|
62
|
+
parse.read(myReadableStream);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
### `tokenize.on(eventName, listener)`
|
|
68
|
+
|
|
69
|
+
Inherited from `EventEmitter`. Registers a listener for the `token` event.
|
|
70
|
+
|
|
71
|
+
### `tokenize.push(char: string | null): Promise<void>`
|
|
72
|
+
|
|
73
|
+
Pushes a single character into the tokenizer. Pass `null` to signal end-of-stream and flush remaining buffers.
|
|
74
|
+
|
|
75
|
+
### Events
|
|
76
|
+
|
|
77
|
+
| Event | Payload | Description |
|
|
78
|
+
| ------- | ------------------- | -------------------------------------------- |
|
|
79
|
+
| `token` | `TokenizeTextEvent` | A run of plain text outside any tag. |
|
|
80
|
+
| `token` | `TokenizeTagEvent` | A complete mustache tag. |
|
|
81
|
+
| `token` | `null` | End of stream; all tokens have been emitted. |
|
|
82
|
+
|
|
83
|
+
#### `TokenizeTextEvent`
|
|
84
|
+
|
|
85
|
+
| Property | Type | Description |
|
|
86
|
+
| -------- | -------- | ------------------------------------------- |
|
|
87
|
+
| `type` | `"text"` | Discriminant. |
|
|
88
|
+
| `data` | `string` | The plain-text content. |
|
|
89
|
+
| `uid` | `number` | `performance.now()` timestamp at emit time. |
|
|
90
|
+
|
|
91
|
+
#### `TokenizeTagEvent`
|
|
92
|
+
|
|
93
|
+
| Property | Type | Description |
|
|
94
|
+
| -------- | -------- | ------------------------------------------- |
|
|
95
|
+
| `type` | `"tag"` | Discriminant. |
|
|
96
|
+
| `data` | `Tag` | Parsed tag object (see `Tag`). |
|
|
97
|
+
| `uid` | `number` | `performance.now()` timestamp at emit time. |
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { TextDecoderStream } from "node:stream/web";
|
|
2
|
+
import { createReadableStream } from "../test/streams";
|
|
3
|
+
import { Parse, type ParseOptions } from "../parse/Parse";
|
|
4
|
+
import { Tokenize, TokenizeTagEvent, TokenizeTextEvent } from "./Tokenize";
|
|
5
|
+
|
|
6
|
+
(process.env as any).LOG_LEVEL = "debug";
|
|
7
|
+
|
|
8
|
+
describe("Tokenize", () => {
|
|
9
|
+
it("tokenizes a mustache template: single variable tag", async () => {
|
|
10
|
+
const queue: (TokenizeTagEvent | TokenizeTextEvent | null)[] = [];
|
|
11
|
+
|
|
12
|
+
const t = new Tokenize().on("token", (evt) => queue.push(evt));
|
|
13
|
+
|
|
14
|
+
const options: ParseOptions = {
|
|
15
|
+
onChar: t.push.bind(t),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const mockHandleInactive = vitest.fn();
|
|
19
|
+
await new Promise<void>((resolve) => {
|
|
20
|
+
new Parse(options)
|
|
21
|
+
.on("inactive", mockHandleInactive.mockImplementation(resolve))
|
|
22
|
+
.read(
|
|
23
|
+
createReadableStream("a{{m}}z").pipeThrough(new TextDecoderStream())
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(queue).toHaveLength(4);
|
|
28
|
+
|
|
29
|
+
expect(queue[0]).toStrictEqual(
|
|
30
|
+
expect.objectContaining({
|
|
31
|
+
data: "a",
|
|
32
|
+
type: "text",
|
|
33
|
+
uid: expect.any(Number),
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const { data, type } = queue[1] as TokenizeTagEvent;
|
|
38
|
+
expect(type).toBe("tag");
|
|
39
|
+
expect(data.type).toBe("variable");
|
|
40
|
+
expect(data.value).toBe("m");
|
|
41
|
+
|
|
42
|
+
expect(queue[2]).toStrictEqual(
|
|
43
|
+
expect.objectContaining({
|
|
44
|
+
data: "z",
|
|
45
|
+
type: "text",
|
|
46
|
+
uid: expect.any(Number),
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(queue[3]).toBeNull();
|
|
51
|
+
|
|
52
|
+
expect(mockHandleInactive).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("tokenizes a mustache template: single partial tag", async () => {
|
|
56
|
+
const queue: (TokenizeTagEvent | TokenizeTextEvent | null)[] = [];
|
|
57
|
+
|
|
58
|
+
const t = new Tokenize();
|
|
59
|
+
t.on("token", (evt) => queue.push(evt));
|
|
60
|
+
|
|
61
|
+
const options: ParseOptions = {
|
|
62
|
+
onChar: t.push.bind(t),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const mockHandleInactive = vitest.fn();
|
|
66
|
+
await new Promise<void>((resolve) => {
|
|
67
|
+
new Parse(options)
|
|
68
|
+
.on("inactive", mockHandleInactive.mockImplementation(resolve))
|
|
69
|
+
.read(
|
|
70
|
+
createReadableStream("a{{>m}}z").pipeThrough(new TextDecoderStream())
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(queue.length).toBe(4);
|
|
75
|
+
|
|
76
|
+
let { data, type } = queue[0] as TokenizeTextEvent;
|
|
77
|
+
expect(type).toBe("text");
|
|
78
|
+
expect(data).toBe("a");
|
|
79
|
+
|
|
80
|
+
let { data: tagData, type: tagType } = queue[1] as TokenizeTagEvent;
|
|
81
|
+
expect(tagType).toBe("tag");
|
|
82
|
+
expect(tagData.type).toBe("partial");
|
|
83
|
+
expect(tagData.value).toBe(">m");
|
|
84
|
+
|
|
85
|
+
({ data, type } = queue[2] as TokenizeTextEvent);
|
|
86
|
+
expect(type).toBe("text");
|
|
87
|
+
expect(data).toBe("z");
|
|
88
|
+
|
|
89
|
+
expect(queue[3]).toBeNull();
|
|
90
|
+
|
|
91
|
+
expect(mockHandleInactive).toHaveBeenCalledTimes(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("tokenizes a mustache template: single section tag", async () => {
|
|
95
|
+
const queue: (TokenizeTagEvent | TokenizeTextEvent | null)[] = [];
|
|
96
|
+
|
|
97
|
+
const t = new Tokenize();
|
|
98
|
+
t.on("token", (evt) => queue.push(evt));
|
|
99
|
+
|
|
100
|
+
const options: ParseOptions = {
|
|
101
|
+
onChar: t.push.bind(t),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const mockHandleInactive = vitest.fn();
|
|
105
|
+
await new Promise<void>((resolve) => {
|
|
106
|
+
new Parse(options)
|
|
107
|
+
.on("inactive", mockHandleInactive.mockImplementation(resolve))
|
|
108
|
+
.read(
|
|
109
|
+
createReadableStream("ab{{#g}}m{{/g}}yz").pipeThrough(
|
|
110
|
+
new TextDecoderStream()
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(queue).toHaveLength(6);
|
|
116
|
+
|
|
117
|
+
let { data: textData, type: textType } = queue[0] as TokenizeTagEvent;
|
|
118
|
+
expect(textType).toBe("text");
|
|
119
|
+
expect(textData).toBe("ab");
|
|
120
|
+
|
|
121
|
+
let { data, type } = queue[1] as TokenizeTagEvent;
|
|
122
|
+
expect(type).toBe("tag");
|
|
123
|
+
expect(data.type).toBe("section");
|
|
124
|
+
expect(data.value).toBe("#g");
|
|
125
|
+
|
|
126
|
+
({ data: textData, type: textType } = queue[2] as TokenizeTagEvent);
|
|
127
|
+
expect(textType).toBe("text");
|
|
128
|
+
expect(textData).toBe("m");
|
|
129
|
+
|
|
130
|
+
({ data, type } = queue[3] as TokenizeTagEvent);
|
|
131
|
+
expect(type).toBe("tag");
|
|
132
|
+
expect(data.type).toBe("end");
|
|
133
|
+
expect(data.value).toBe("/g");
|
|
134
|
+
|
|
135
|
+
({ data: textData, type: textType } = queue[4] as TokenizeTagEvent);
|
|
136
|
+
expect(textType).toBe("text");
|
|
137
|
+
expect(textData).toBe("yz");
|
|
138
|
+
|
|
139
|
+
expect(queue[5]).toBeNull();
|
|
140
|
+
|
|
141
|
+
expect(mockHandleInactive).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("tokenizes a mustache template: single variable context tag", async () => {
|
|
145
|
+
const queue: (TokenizeTagEvent | TokenizeTextEvent | null)[] = [];
|
|
146
|
+
|
|
147
|
+
const t = new Tokenize();
|
|
148
|
+
t.on("token", (evt) => queue.push(evt));
|
|
149
|
+
|
|
150
|
+
const options: ParseOptions = {
|
|
151
|
+
onChar: t.push.bind(t),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const mockHandleInactive = vitest.fn();
|
|
155
|
+
await new Promise<void>((resolve) => {
|
|
156
|
+
new Parse(options)
|
|
157
|
+
.on("inactive", mockHandleInactive.mockImplementation(resolve))
|
|
158
|
+
.read(
|
|
159
|
+
createReadableStream("a{{.}}z").pipeThrough(new TextDecoderStream())
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
console.log(`\n${"-".repeat(80)}\nqueue=`, JSON.stringify(queue));
|
|
164
|
+
|
|
165
|
+
expect(queue).toHaveLength(4);
|
|
166
|
+
|
|
167
|
+
let { type } = queue[0] as TokenizeTextEvent;
|
|
168
|
+
expect(type).toBe("text");
|
|
169
|
+
|
|
170
|
+
let { data: tagData } = queue[1] as TokenizeTagEvent;
|
|
171
|
+
expect(tagData.type).toBe("implicit");
|
|
172
|
+
expect(tagData.value).toBe(".");
|
|
173
|
+
|
|
174
|
+
({ type } = queue[2] as TokenizeTextEvent);
|
|
175
|
+
expect(type).toBe("text");
|
|
176
|
+
|
|
177
|
+
expect(queue[3]).toBeNull();
|
|
178
|
+
|
|
179
|
+
expect(mockHandleInactive).toHaveBeenCalledTimes(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("tokenizes a mustache template: nested tags", async () => {
|
|
183
|
+
const queue: (TokenizeTagEvent | TokenizeTextEvent | null)[] = [];
|
|
184
|
+
|
|
185
|
+
const t = new Tokenize();
|
|
186
|
+
t.on("token", (evt) => queue.push(evt));
|
|
187
|
+
|
|
188
|
+
const options: ParseOptions = {
|
|
189
|
+
onChar: vitest.fn(t.push.bind(t)),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const mockHandleInactive = vitest.fn();
|
|
193
|
+
await new Promise<void>((resolve) => {
|
|
194
|
+
new Parse(options)
|
|
195
|
+
.on("inactive", mockHandleInactive.mockImplementation(resolve))
|
|
196
|
+
.read(
|
|
197
|
+
createReadableStream("a{{#e}}{{>m}}{{/e}}z").pipeThrough(
|
|
198
|
+
new TextDecoderStream()
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(queue.length).toBe(6);
|
|
204
|
+
|
|
205
|
+
let { data: textData, type: textType } = queue[0] as TokenizeTextEvent;
|
|
206
|
+
expect(textType).toBe("text");
|
|
207
|
+
expect(textData).toBe("a");
|
|
208
|
+
|
|
209
|
+
let { data } = queue[1] as TokenizeTagEvent;
|
|
210
|
+
expect(data.type).toBe("section");
|
|
211
|
+
expect(data.value).toBe("#e");
|
|
212
|
+
|
|
213
|
+
({ data } = queue[2] as TokenizeTagEvent);
|
|
214
|
+
expect(data.type).toBe("partial");
|
|
215
|
+
expect(data.value).toBe(">m");
|
|
216
|
+
|
|
217
|
+
({ data } = queue[3] as TokenizeTagEvent);
|
|
218
|
+
expect(data.type).toBe("end");
|
|
219
|
+
expect(data.value).toBe("/e");
|
|
220
|
+
|
|
221
|
+
({ data: textData, type: textType } = queue[4] as TokenizeTextEvent);
|
|
222
|
+
expect(textType).toBe("text");
|
|
223
|
+
expect(textData).toBe("z");
|
|
224
|
+
|
|
225
|
+
expect(queue[5]).toBeNull();
|
|
226
|
+
|
|
227
|
+
expect(mockHandleInactive).toHaveBeenCalledTimes(1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("tokenizes an empty section", async () => {
|
|
231
|
+
const queue: (TokenizeTagEvent | TokenizeTextEvent | null)[] = [];
|
|
232
|
+
|
|
233
|
+
const t = new Tokenize();
|
|
234
|
+
t.on("token", (evt) => queue.push(evt));
|
|
235
|
+
|
|
236
|
+
const options: ParseOptions = {
|
|
237
|
+
onChar: vitest.fn(t.push.bind(t)),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const mockHandleInactive = vitest.fn();
|
|
241
|
+
await new Promise<void>((resolve) => {
|
|
242
|
+
new Parse(options)
|
|
243
|
+
.on("inactive", mockHandleInactive.mockImplementation(resolve))
|
|
244
|
+
.read(
|
|
245
|
+
createReadableStream("a{{#e}}{{/e}}z").pipeThrough(
|
|
246
|
+
new TextDecoderStream()
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(queue.length).toBe(5);
|
|
252
|
+
|
|
253
|
+
let { data: textData, type: textType } = queue[0] as TokenizeTextEvent;
|
|
254
|
+
expect(textType).toBe("text");
|
|
255
|
+
expect(textData).toBe("a");
|
|
256
|
+
|
|
257
|
+
let { data } = queue[1] as TokenizeTagEvent;
|
|
258
|
+
expect(data.type).toBe("section");
|
|
259
|
+
expect(data.value).toBe("#e");
|
|
260
|
+
|
|
261
|
+
({ data } = queue[2] as TokenizeTagEvent);
|
|
262
|
+
expect(data.type).toBe("end");
|
|
263
|
+
expect(data.value).toBe("/e");
|
|
264
|
+
|
|
265
|
+
({ data: textData, type: textType } = queue[3] as TokenizeTextEvent);
|
|
266
|
+
expect(textType).toBe("text");
|
|
267
|
+
expect(textData).toBe("z");
|
|
268
|
+
|
|
269
|
+
expect(queue[4]).toBeNull();
|
|
270
|
+
|
|
271
|
+
expect(mockHandleInactive).toHaveBeenCalledTimes(1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("tokenizes text containing single curly braces as plain text", async () => {
|
|
275
|
+
const queue: (TokenizeTagEvent | TokenizeTextEvent | null)[] = [];
|
|
276
|
+
|
|
277
|
+
const t = new Tokenize().on("token", (evt) => queue.push(evt));
|
|
278
|
+
|
|
279
|
+
const options: ParseOptions = {
|
|
280
|
+
onChar: t.push.bind(t),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const mockHandleInactive = vitest.fn();
|
|
284
|
+
await new Promise<void>((resolve) => {
|
|
285
|
+
new Parse(options)
|
|
286
|
+
.on("inactive", mockHandleInactive.mockImplementation(resolve))
|
|
287
|
+
.read(
|
|
288
|
+
createReadableStream("a {\n color: red;\n} b").pipeThrough(
|
|
289
|
+
new TextDecoderStream()
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(queue).toHaveLength(2);
|
|
295
|
+
|
|
296
|
+
expect(queue[0]).toStrictEqual(
|
|
297
|
+
expect.objectContaining({
|
|
298
|
+
data: "a {\n color: red;\n} b",
|
|
299
|
+
type: "text",
|
|
300
|
+
uid: expect.any(Number),
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(queue[1]).toBeNull();
|
|
305
|
+
|
|
306
|
+
expect(mockHandleInactive).toHaveBeenCalledTimes(1);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("tokenizes nested sections", async () => {
|
|
310
|
+
const queue: (TokenizeTagEvent | TokenizeTextEvent | null)[] = [];
|
|
311
|
+
|
|
312
|
+
const t = new Tokenize();
|
|
313
|
+
t.on("token", (evt) => queue.push(evt));
|
|
314
|
+
|
|
315
|
+
const options: ParseOptions = {
|
|
316
|
+
onChar: vitest.fn(t.push.bind(t)),
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const mockHandleInactive = vitest.fn();
|
|
320
|
+
await new Promise<void>((resolve) => {
|
|
321
|
+
new Parse(options)
|
|
322
|
+
.on("inactive", mockHandleInactive.mockImplementation(resolve))
|
|
323
|
+
.read(
|
|
324
|
+
createReadableStream(
|
|
325
|
+
"aa{{#ee}}{{#.}}{{.}}{{/.}}{{/ee}}zz"
|
|
326
|
+
).pipeThrough(new TextDecoderStream())
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(queue.length).toBe(8);
|
|
331
|
+
|
|
332
|
+
let { data: textData, type: textType } = queue[0] as TokenizeTextEvent;
|
|
333
|
+
expect(textType).toBe("text");
|
|
334
|
+
expect(textData).toBe("aa");
|
|
335
|
+
|
|
336
|
+
let { data } = queue[1] as TokenizeTagEvent;
|
|
337
|
+
expect(data.type).toBe("section");
|
|
338
|
+
expect(data.value).toBe("#ee");
|
|
339
|
+
|
|
340
|
+
({ data } = queue[2] as TokenizeTagEvent);
|
|
341
|
+
expect(data.type).toBe("section");
|
|
342
|
+
expect(data.value).toBe("#.");
|
|
343
|
+
|
|
344
|
+
({ data } = queue[3] as TokenizeTagEvent);
|
|
345
|
+
expect(data.type).toBe("implicit");
|
|
346
|
+
expect(data.value).toBe(".");
|
|
347
|
+
|
|
348
|
+
({ data } = queue[4] as TokenizeTagEvent);
|
|
349
|
+
expect(data.type).toBe("end");
|
|
350
|
+
expect(data.value).toBe("/.");
|
|
351
|
+
|
|
352
|
+
({ data } = queue[5] as TokenizeTagEvent);
|
|
353
|
+
expect(data.type).toBe("end");
|
|
354
|
+
expect(data.value).toBe("/ee");
|
|
355
|
+
|
|
356
|
+
({ data: textData, type: textType } = queue[6] as TokenizeTextEvent);
|
|
357
|
+
expect(textType).toBe("text");
|
|
358
|
+
expect(textData).toBe("zz");
|
|
359
|
+
|
|
360
|
+
expect(queue[7]).toBeNull();
|
|
361
|
+
|
|
362
|
+
expect(mockHandleInactive).toHaveBeenCalledTimes(1);
|
|
363
|
+
});
|
|
364
|
+
});
|