@stream-mdx/core 0.0.3 → 0.1.1
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/CHANGELOG.md +12 -0
- package/README.md +6 -0
- package/dist/index.cjs +305 -46
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +304 -46
- package/dist/inline-parser.cjs +76 -3
- package/dist/inline-parser.d.cts +5 -0
- package/dist/inline-parser.d.ts +5 -0
- package/dist/inline-parser.mjs +76 -3
- package/dist/mixed-content.cjs +130 -9
- package/dist/mixed-content.d.cts +16 -2
- package/dist/mixed-content.d.ts +16 -2
- package/dist/mixed-content.mjs +130 -9
- package/dist/streaming/inline-streaming.cjs +99 -34
- package/dist/streaming/inline-streaming.d.cts +13 -2
- package/dist/streaming/inline-streaming.d.ts +13 -2
- package/dist/streaming/inline-streaming.mjs +98 -34
- package/dist/types.d.cts +41 -2
- package/dist/types.d.ts +41 -2
- package/dist/worker-html-sanitizer.cjs +16 -2
- package/dist/worker-html-sanitizer.mjs +16 -2
- package/package.json +2 -2
|
@@ -1,63 +1,127 @@
|
|
|
1
1
|
// src/streaming/inline-streaming.ts
|
|
2
|
+
var DEFAULT_FORMAT_ANTICIPATION = {
|
|
3
|
+
inline: false,
|
|
4
|
+
mathInline: false,
|
|
5
|
+
mathBlock: false,
|
|
6
|
+
html: false,
|
|
7
|
+
mdx: false,
|
|
8
|
+
regex: false
|
|
9
|
+
};
|
|
10
|
+
function normalizeFormatAnticipation(input) {
|
|
11
|
+
if (input === true) {
|
|
12
|
+
return { ...DEFAULT_FORMAT_ANTICIPATION, inline: true };
|
|
13
|
+
}
|
|
14
|
+
if (!input) {
|
|
15
|
+
return { ...DEFAULT_FORMAT_ANTICIPATION };
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
inline: input.inline ?? false,
|
|
19
|
+
mathInline: input.mathInline ?? false,
|
|
20
|
+
mathBlock: input.mathBlock ?? false,
|
|
21
|
+
html: input.html ?? false,
|
|
22
|
+
mdx: input.mdx ?? false,
|
|
23
|
+
regex: input.regex ?? false
|
|
24
|
+
};
|
|
25
|
+
}
|
|
2
26
|
function prepareInlineStreamingContent(content, options) {
|
|
3
|
-
const enableAnticipation = Boolean(options?.formatAnticipation);
|
|
4
27
|
const enableMath = options?.math !== false;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
28
|
+
const anticipation = normalizeFormatAnticipation(options?.formatAnticipation);
|
|
29
|
+
const enableInlineAnticipation = anticipation.inline;
|
|
30
|
+
const enableMathInlineAnticipation = anticipation.mathInline;
|
|
31
|
+
const enableMathBlockAnticipation = anticipation.mathBlock;
|
|
32
|
+
const stack = [];
|
|
33
|
+
const toggleToken = (token) => {
|
|
34
|
+
const last = stack[stack.length - 1];
|
|
35
|
+
if (last === token) {
|
|
36
|
+
stack.pop();
|
|
37
|
+
} else {
|
|
38
|
+
stack.push(token);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
let mathDisplayOpen = false;
|
|
42
|
+
let mathDisplayCrossedNewline = false;
|
|
10
43
|
for (let i = 0; i < content.length; i++) {
|
|
11
44
|
const code = content.charCodeAt(i);
|
|
12
|
-
if (code ===
|
|
13
|
-
|
|
45
|
+
if (code === 10 || code === 13) {
|
|
46
|
+
if (mathDisplayOpen) {
|
|
47
|
+
mathDisplayCrossedNewline = true;
|
|
48
|
+
}
|
|
14
49
|
continue;
|
|
15
50
|
}
|
|
16
51
|
if (code === 96) {
|
|
17
|
-
|
|
52
|
+
toggleToken("code");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (code === 126 && i + 1 < content.length && content.charCodeAt(i + 1) === 126) {
|
|
56
|
+
toggleToken("strike");
|
|
57
|
+
i += 1;
|
|
18
58
|
continue;
|
|
19
59
|
}
|
|
20
60
|
if (code === 42) {
|
|
21
61
|
if (i + 1 < content.length && content.charCodeAt(i + 1) === 42) {
|
|
22
|
-
|
|
23
|
-
starCount += 2;
|
|
62
|
+
toggleToken("strong");
|
|
24
63
|
i += 1;
|
|
25
64
|
} else {
|
|
26
|
-
|
|
65
|
+
toggleToken("em");
|
|
27
66
|
}
|
|
28
67
|
continue;
|
|
29
68
|
}
|
|
30
|
-
if (code ===
|
|
31
|
-
if (i + 1 < content.length && content.charCodeAt(i + 1) ===
|
|
32
|
-
|
|
69
|
+
if (enableMath && code === 36) {
|
|
70
|
+
if (i + 1 < content.length && content.charCodeAt(i + 1) === 36) {
|
|
71
|
+
toggleToken("math-display");
|
|
72
|
+
if (mathDisplayOpen) {
|
|
73
|
+
mathDisplayOpen = false;
|
|
74
|
+
mathDisplayCrossedNewline = false;
|
|
75
|
+
} else {
|
|
76
|
+
mathDisplayOpen = true;
|
|
77
|
+
mathDisplayCrossedNewline = false;
|
|
78
|
+
}
|
|
33
79
|
i += 1;
|
|
80
|
+
} else {
|
|
81
|
+
toggleToken("math-inline");
|
|
34
82
|
}
|
|
35
83
|
}
|
|
36
84
|
}
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return { kind: "parse", status: "complete", content, appended: "" };
|
|
85
|
+
const hasIncompleteFormatting = stack.some((token) => token === "code" || token === "strike" || token === "strong" || token === "em");
|
|
86
|
+
const hasIncompleteMathInline = stack.includes("math-inline");
|
|
87
|
+
const hasIncompleteMathDisplay = stack.includes("math-display");
|
|
88
|
+
const hasIncompleteMath = hasIncompleteMathInline || hasIncompleteMathDisplay;
|
|
89
|
+
if (enableMath && hasIncompleteMath) {
|
|
90
|
+
if (hasIncompleteMathInline && !enableMathInlineAnticipation) {
|
|
91
|
+
return { kind: "raw", status: "raw", reason: "incomplete-math" };
|
|
92
|
+
}
|
|
93
|
+
if (hasIncompleteMathDisplay && (!enableMathBlockAnticipation || mathDisplayCrossedNewline)) {
|
|
94
|
+
return { kind: "raw", status: "raw", reason: "incomplete-math" };
|
|
95
|
+
}
|
|
49
96
|
}
|
|
50
|
-
if (!
|
|
97
|
+
if (hasIncompleteFormatting && !enableInlineAnticipation) {
|
|
51
98
|
return { kind: "raw", status: "raw", reason: "incomplete-formatting" };
|
|
52
99
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
100
|
+
if (!hasIncompleteFormatting && !hasIncompleteMath) {
|
|
101
|
+
return { kind: "parse", status: "complete", content, appended: "" };
|
|
102
|
+
}
|
|
103
|
+
const appendForToken = (token) => {
|
|
104
|
+
switch (token) {
|
|
105
|
+
case "code":
|
|
106
|
+
return "`";
|
|
107
|
+
case "strike":
|
|
108
|
+
return "~~";
|
|
109
|
+
case "strong":
|
|
110
|
+
return "**";
|
|
111
|
+
case "em":
|
|
112
|
+
return "*";
|
|
113
|
+
case "math-inline":
|
|
114
|
+
return "$";
|
|
115
|
+
case "math-display":
|
|
116
|
+
return "$$";
|
|
117
|
+
default:
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const appended = stack.slice().reverse().map((token) => appendForToken(token)).join("");
|
|
59
122
|
return { kind: "parse", status: "anticipated", content: content + appended, appended };
|
|
60
123
|
}
|
|
61
124
|
export {
|
|
125
|
+
normalizeFormatAnticipation,
|
|
62
126
|
prepareInlineStreamingContent
|
|
63
127
|
};
|
package/dist/types.d.cts
CHANGED
|
@@ -39,6 +39,14 @@ interface MixedContentSegment {
|
|
|
39
39
|
status?: "pending" | "compiled" | "error";
|
|
40
40
|
error?: string;
|
|
41
41
|
}
|
|
42
|
+
type FormatAnticipationConfig = boolean | {
|
|
43
|
+
inline?: boolean;
|
|
44
|
+
mathInline?: boolean;
|
|
45
|
+
mathBlock?: boolean;
|
|
46
|
+
html?: boolean;
|
|
47
|
+
mdx?: boolean;
|
|
48
|
+
regex?: boolean;
|
|
49
|
+
};
|
|
42
50
|
interface InlineHtmlDescriptor {
|
|
43
51
|
tagName: string;
|
|
44
52
|
attributes: Record<string, string>;
|
|
@@ -115,8 +123,9 @@ type WorkerIn = {
|
|
|
115
123
|
tables?: boolean;
|
|
116
124
|
callouts?: boolean;
|
|
117
125
|
math?: boolean;
|
|
118
|
-
formatAnticipation?:
|
|
126
|
+
formatAnticipation?: FormatAnticipationConfig;
|
|
119
127
|
liveCodeHighlighting?: boolean;
|
|
128
|
+
mdxComponentNames?: string[];
|
|
120
129
|
};
|
|
121
130
|
mdx?: {
|
|
122
131
|
compileMode?: "server" | "worker";
|
|
@@ -126,6 +135,8 @@ type WorkerIn = {
|
|
|
126
135
|
text: string;
|
|
127
136
|
} | {
|
|
128
137
|
type: "FINALIZE";
|
|
138
|
+
} | {
|
|
139
|
+
type: "DEBUG_STATE";
|
|
129
140
|
} | {
|
|
130
141
|
type: "MDX_COMPILED";
|
|
131
142
|
blockId: string;
|
|
@@ -158,6 +169,23 @@ type WorkerOut = {
|
|
|
158
169
|
} | {
|
|
159
170
|
type: "METRICS";
|
|
160
171
|
metrics: PerformanceMetrics;
|
|
172
|
+
} | {
|
|
173
|
+
type: "DEBUG_STATE";
|
|
174
|
+
state: {
|
|
175
|
+
contentLength: number;
|
|
176
|
+
contentTail: string;
|
|
177
|
+
blockCount: number;
|
|
178
|
+
blockTypeCounts: Record<string, number>;
|
|
179
|
+
lastBlockType?: string;
|
|
180
|
+
lastBlockRange?: {
|
|
181
|
+
from: number;
|
|
182
|
+
to: number;
|
|
183
|
+
};
|
|
184
|
+
lastBlockRawTail?: string;
|
|
185
|
+
hasInlineCodeHeading: boolean;
|
|
186
|
+
hasCodeBlocksHeading: boolean;
|
|
187
|
+
hasMediaHeading: boolean;
|
|
188
|
+
};
|
|
161
189
|
} | {
|
|
162
190
|
type: "ERROR";
|
|
163
191
|
phase: WorkerPhase;
|
|
@@ -182,6 +210,17 @@ interface RegexInlinePlugin extends InlinePlugin {
|
|
|
182
210
|
* the regex is skipped for that node.
|
|
183
211
|
*/
|
|
184
212
|
fastCheck?: (text: string) => boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Optional streaming anticipation config. Only used when formatAnticipation.regex is enabled.
|
|
215
|
+
*/
|
|
216
|
+
anticipation?: RegexAnticipationPattern;
|
|
217
|
+
}
|
|
218
|
+
interface RegexAnticipationPattern {
|
|
219
|
+
start: RegExp;
|
|
220
|
+
end: RegExp;
|
|
221
|
+
full?: RegExp;
|
|
222
|
+
append: string | ((match: RegExpExecArray, content: string) => string);
|
|
223
|
+
maxScanChars?: number;
|
|
185
224
|
}
|
|
186
225
|
interface ASTInlinePlugin extends InlinePlugin {
|
|
187
226
|
visit: (node: InlineNode, ctx: {
|
|
@@ -314,4 +353,4 @@ interface CoalescingMetrics {
|
|
|
314
353
|
insertChildCoalesced: number;
|
|
315
354
|
}
|
|
316
355
|
|
|
317
|
-
export { type ASTInlinePlugin, type Block, type CoalescingMetrics, type CompiledMdxModule, type InlineHtmlDescriptor, type InlineNode, type InlinePlugin, LANGUAGE_ALIASES, type MixedContentSegment, type NodePath, type NodeSnapshot, PATCH_ROOT_ID, type Patch, type PatchMetrics, type PerformanceMetrics, type ProtectedRange, type ProtectedRangeKind, type RegexInlinePlugin, type SetPropsBatchEntry, type WorkerErrorPayload, type WorkerIn, type WorkerOut, type WorkerPhase };
|
|
356
|
+
export { type ASTInlinePlugin, type Block, type CoalescingMetrics, type CompiledMdxModule, type FormatAnticipationConfig, type InlineHtmlDescriptor, type InlineNode, type InlinePlugin, LANGUAGE_ALIASES, type MixedContentSegment, type NodePath, type NodeSnapshot, PATCH_ROOT_ID, type Patch, type PatchMetrics, type PerformanceMetrics, type ProtectedRange, type ProtectedRangeKind, type RegexAnticipationPattern, type RegexInlinePlugin, type SetPropsBatchEntry, type WorkerErrorPayload, type WorkerIn, type WorkerOut, type WorkerPhase };
|
package/dist/types.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ interface MixedContentSegment {
|
|
|
39
39
|
status?: "pending" | "compiled" | "error";
|
|
40
40
|
error?: string;
|
|
41
41
|
}
|
|
42
|
+
type FormatAnticipationConfig = boolean | {
|
|
43
|
+
inline?: boolean;
|
|
44
|
+
mathInline?: boolean;
|
|
45
|
+
mathBlock?: boolean;
|
|
46
|
+
html?: boolean;
|
|
47
|
+
mdx?: boolean;
|
|
48
|
+
regex?: boolean;
|
|
49
|
+
};
|
|
42
50
|
interface InlineHtmlDescriptor {
|
|
43
51
|
tagName: string;
|
|
44
52
|
attributes: Record<string, string>;
|
|
@@ -115,8 +123,9 @@ type WorkerIn = {
|
|
|
115
123
|
tables?: boolean;
|
|
116
124
|
callouts?: boolean;
|
|
117
125
|
math?: boolean;
|
|
118
|
-
formatAnticipation?:
|
|
126
|
+
formatAnticipation?: FormatAnticipationConfig;
|
|
119
127
|
liveCodeHighlighting?: boolean;
|
|
128
|
+
mdxComponentNames?: string[];
|
|
120
129
|
};
|
|
121
130
|
mdx?: {
|
|
122
131
|
compileMode?: "server" | "worker";
|
|
@@ -126,6 +135,8 @@ type WorkerIn = {
|
|
|
126
135
|
text: string;
|
|
127
136
|
} | {
|
|
128
137
|
type: "FINALIZE";
|
|
138
|
+
} | {
|
|
139
|
+
type: "DEBUG_STATE";
|
|
129
140
|
} | {
|
|
130
141
|
type: "MDX_COMPILED";
|
|
131
142
|
blockId: string;
|
|
@@ -158,6 +169,23 @@ type WorkerOut = {
|
|
|
158
169
|
} | {
|
|
159
170
|
type: "METRICS";
|
|
160
171
|
metrics: PerformanceMetrics;
|
|
172
|
+
} | {
|
|
173
|
+
type: "DEBUG_STATE";
|
|
174
|
+
state: {
|
|
175
|
+
contentLength: number;
|
|
176
|
+
contentTail: string;
|
|
177
|
+
blockCount: number;
|
|
178
|
+
blockTypeCounts: Record<string, number>;
|
|
179
|
+
lastBlockType?: string;
|
|
180
|
+
lastBlockRange?: {
|
|
181
|
+
from: number;
|
|
182
|
+
to: number;
|
|
183
|
+
};
|
|
184
|
+
lastBlockRawTail?: string;
|
|
185
|
+
hasInlineCodeHeading: boolean;
|
|
186
|
+
hasCodeBlocksHeading: boolean;
|
|
187
|
+
hasMediaHeading: boolean;
|
|
188
|
+
};
|
|
161
189
|
} | {
|
|
162
190
|
type: "ERROR";
|
|
163
191
|
phase: WorkerPhase;
|
|
@@ -182,6 +210,17 @@ interface RegexInlinePlugin extends InlinePlugin {
|
|
|
182
210
|
* the regex is skipped for that node.
|
|
183
211
|
*/
|
|
184
212
|
fastCheck?: (text: string) => boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Optional streaming anticipation config. Only used when formatAnticipation.regex is enabled.
|
|
215
|
+
*/
|
|
216
|
+
anticipation?: RegexAnticipationPattern;
|
|
217
|
+
}
|
|
218
|
+
interface RegexAnticipationPattern {
|
|
219
|
+
start: RegExp;
|
|
220
|
+
end: RegExp;
|
|
221
|
+
full?: RegExp;
|
|
222
|
+
append: string | ((match: RegExpExecArray, content: string) => string);
|
|
223
|
+
maxScanChars?: number;
|
|
185
224
|
}
|
|
186
225
|
interface ASTInlinePlugin extends InlinePlugin {
|
|
187
226
|
visit: (node: InlineNode, ctx: {
|
|
@@ -314,4 +353,4 @@ interface CoalescingMetrics {
|
|
|
314
353
|
insertChildCoalesced: number;
|
|
315
354
|
}
|
|
316
355
|
|
|
317
|
-
export { type ASTInlinePlugin, type Block, type CoalescingMetrics, type CompiledMdxModule, type InlineHtmlDescriptor, type InlineNode, type InlinePlugin, LANGUAGE_ALIASES, type MixedContentSegment, type NodePath, type NodeSnapshot, PATCH_ROOT_ID, type Patch, type PatchMetrics, type PerformanceMetrics, type ProtectedRange, type ProtectedRangeKind, type RegexInlinePlugin, type SetPropsBatchEntry, type WorkerErrorPayload, type WorkerIn, type WorkerOut, type WorkerPhase };
|
|
356
|
+
export { type ASTInlinePlugin, type Block, type CoalescingMetrics, type CompiledMdxModule, type FormatAnticipationConfig, type InlineHtmlDescriptor, type InlineNode, type InlinePlugin, LANGUAGE_ALIASES, type MixedContentSegment, type NodePath, type NodeSnapshot, PATCH_ROOT_ID, type Patch, type PatchMetrics, type PerformanceMetrics, type ProtectedRange, type ProtectedRangeKind, type RegexAnticipationPattern, type RegexInlinePlugin, type SetPropsBatchEntry, type WorkerErrorPayload, type WorkerIn, type WorkerOut, type WorkerPhase };
|
|
@@ -37,9 +37,23 @@ var rehypeParse = __toESM(require("rehype-parse"), 1);
|
|
|
37
37
|
var rehypeSanitize = __toESM(require("rehype-sanitize"), 1);
|
|
38
38
|
var rehypeStringify = __toESM(require("rehype-stringify"), 1);
|
|
39
39
|
var import_unified = require("unified");
|
|
40
|
-
var
|
|
40
|
+
var rehypeSanitizeModule = rehypeSanitize;
|
|
41
|
+
var defaultSchema = rehypeSanitizeModule.defaultSchema;
|
|
42
|
+
var resolvePlugin = (mod) => {
|
|
43
|
+
if (typeof mod === "function") return mod;
|
|
44
|
+
if (mod && typeof mod.default === "function") {
|
|
45
|
+
return mod.default;
|
|
46
|
+
}
|
|
47
|
+
if (mod && typeof mod.default?.default === "function") {
|
|
48
|
+
return mod.default?.default;
|
|
49
|
+
}
|
|
50
|
+
return mod;
|
|
51
|
+
};
|
|
52
|
+
var rehypeParsePlugin = resolvePlugin(rehypeParse);
|
|
53
|
+
var rehypeSanitizePlugin = resolvePlugin(rehypeSanitizeModule);
|
|
54
|
+
var rehypeStringifyPlugin = resolvePlugin(rehypeStringify);
|
|
41
55
|
var SANITIZED_SCHEMA = createSchema();
|
|
42
|
-
var sanitizeProcessor = (0, import_unified.unified)().use(
|
|
56
|
+
var sanitizeProcessor = (0, import_unified.unified)().use(rehypeParsePlugin, { fragment: true }).use(rehypeSanitizePlugin, SANITIZED_SCHEMA).use(rehypeStringifyPlugin).freeze();
|
|
43
57
|
function sanitizeHtmlInWorker(html) {
|
|
44
58
|
if (!html) return "";
|
|
45
59
|
try {
|
|
@@ -3,9 +3,23 @@ import * as rehypeParse from "rehype-parse";
|
|
|
3
3
|
import * as rehypeSanitize from "rehype-sanitize";
|
|
4
4
|
import * as rehypeStringify from "rehype-stringify";
|
|
5
5
|
import { unified } from "unified";
|
|
6
|
-
var
|
|
6
|
+
var rehypeSanitizeModule = rehypeSanitize;
|
|
7
|
+
var defaultSchema = rehypeSanitizeModule.defaultSchema;
|
|
8
|
+
var resolvePlugin = (mod) => {
|
|
9
|
+
if (typeof mod === "function") return mod;
|
|
10
|
+
if (mod && typeof mod.default === "function") {
|
|
11
|
+
return mod.default;
|
|
12
|
+
}
|
|
13
|
+
if (mod && typeof mod.default?.default === "function") {
|
|
14
|
+
return mod.default?.default;
|
|
15
|
+
}
|
|
16
|
+
return mod;
|
|
17
|
+
};
|
|
18
|
+
var rehypeParsePlugin = resolvePlugin(rehypeParse);
|
|
19
|
+
var rehypeSanitizePlugin = resolvePlugin(rehypeSanitizeModule);
|
|
20
|
+
var rehypeStringifyPlugin = resolvePlugin(rehypeStringify);
|
|
7
21
|
var SANITIZED_SCHEMA = createSchema();
|
|
8
|
-
var sanitizeProcessor = unified().use(
|
|
22
|
+
var sanitizeProcessor = unified().use(rehypeParsePlugin, { fragment: true }).use(rehypeSanitizePlugin, SANITIZED_SCHEMA).use(rehypeStringifyPlugin).freeze();
|
|
9
23
|
function sanitizeHtmlInWorker(html) {
|
|
10
24
|
if (!html) return "";
|
|
11
25
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-mdx/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Core types, snapshot utilities, and perf helpers for the Streaming Markdown V2 stack",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"sideEffects": false,
|
|
91
91
|
"scripts": {
|
|
92
92
|
"build": "tsup",
|
|
93
|
-
"test": "tsx
|
|
93
|
+
"test": "tsx ../../scripts/run-tests.ts __tests__",
|
|
94
94
|
"clean": "rm -rf dist",
|
|
95
95
|
"prepack": "npm run build"
|
|
96
96
|
},
|