@ai-gui/core 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,611 @@
1
+ import DOMPurify from "dompurify";
2
+ import MarkdownIt from "markdown-it";
3
+
4
+ //#region src/partial-json.ts
5
+ /**
6
+ * Parse a possibly-incomplete JSON string, as produced by a streaming source.
7
+ *
8
+ * If the input is valid JSON, it is returned with `complete: true`. Otherwise
9
+ * the parser attempts to repair the fragment by dropping any trailing
10
+ * incomplete token (a dangling key, colon, or comma) and auto-closing unclosed
11
+ * strings, objects, and arrays, returning whatever data has arrived so far with
12
+ * `complete: false`. Unparseable input yields `{ data: undefined, complete: false }`
13
+ * and never throws.
14
+ */
15
+ function parsePartialJSON(input) {
16
+ const str = input.trim();
17
+ if (str === "") return {
18
+ data: void 0,
19
+ complete: false
20
+ };
21
+ try {
22
+ return {
23
+ data: JSON.parse(str),
24
+ complete: true
25
+ };
26
+ } catch {}
27
+ for (const candidate of repair(str)) try {
28
+ return {
29
+ data: JSON.parse(candidate),
30
+ complete: false
31
+ };
32
+ } catch {}
33
+ return {
34
+ data: void 0,
35
+ complete: false
36
+ };
37
+ }
38
+ const LITERAL_CHAR = /[0-9a-zA-Z+.\-]/;
39
+ const WHITESPACE = /\s/;
40
+ function repair(str) {
41
+ const stack = [];
42
+ let inString = false;
43
+ let escaped = false;
44
+ let stringIsKey = false;
45
+ let inLiteral = false;
46
+ let mode = "value";
47
+ let lastGoodEnd = -1;
48
+ for (let i = 0; i < str.length; i++) {
49
+ const ch = str[i];
50
+ if (inString) {
51
+ if (escaped) escaped = false;
52
+ else if (ch === "\\") escaped = true;
53
+ else if (ch === "\"") {
54
+ inString = false;
55
+ if (stringIsKey) mode = "objColon";
56
+ else {
57
+ mode = "afterValue";
58
+ lastGoodEnd = i + 1;
59
+ }
60
+ }
61
+ continue;
62
+ }
63
+ if (inLiteral) {
64
+ if (LITERAL_CHAR.test(ch)) continue;
65
+ inLiteral = false;
66
+ mode = "afterValue";
67
+ lastGoodEnd = i;
68
+ }
69
+ if (WHITESPACE.test(ch)) continue;
70
+ if (ch === "}" || ch === "]") {
71
+ stack.pop();
72
+ mode = "afterValue";
73
+ lastGoodEnd = i + 1;
74
+ continue;
75
+ }
76
+ switch (mode) {
77
+ case "value":
78
+ if (ch === "{") {
79
+ stack.push("obj");
80
+ mode = "objKey";
81
+ lastGoodEnd = i + 1;
82
+ } else if (ch === "[") {
83
+ stack.push("arr");
84
+ mode = "value";
85
+ lastGoodEnd = i + 1;
86
+ } else if (ch === "\"") {
87
+ inString = true;
88
+ stringIsKey = false;
89
+ } else inLiteral = true;
90
+ break;
91
+ case "objKey":
92
+ if (ch === "\"") {
93
+ inString = true;
94
+ stringIsKey = true;
95
+ }
96
+ break;
97
+ case "objColon":
98
+ if (ch === ":") mode = "value";
99
+ break;
100
+ case "afterValue":
101
+ if (ch === ",") mode = stack[stack.length - 1] === "obj" ? "objKey" : "value";
102
+ break;
103
+ }
104
+ }
105
+ if (inString && !stringIsKey) return [closeContainers(str + "\"", stack)];
106
+ const fallback = lastGoodEnd === -1 ? [] : [closeContainers(str.slice(0, lastGoodEnd), stack)];
107
+ if (inLiteral) return [closeContainers(str, stack), ...fallback];
108
+ return fallback;
109
+ }
110
+ function closeContainers(body, stack) {
111
+ let out = body.replace(/,\s*$/, "");
112
+ for (let i = stack.length - 1; i >= 0; i--) out += stack[i] === "obj" ? "}" : "]";
113
+ return out;
114
+ }
115
+
116
+ //#endregion
117
+ //#region src/repair-markdown.ts
118
+ /**
119
+ * Temporarily repair half-finished markdown so a partial streaming buffer
120
+ * renders smoothly. Operates on a copy of the buffer and returns a new string;
121
+ * it never mutates the input. Pure function, no dependencies.
122
+ *
123
+ * The repairs are intentionally conservative: only unambiguous unclosed
124
+ * inline/fence syntax is completed. Ambiguous constructs (e.g. a dangling
125
+ * link text `[docs`) are left untouched to avoid guessing wrong.
126
+ */
127
+ function repairMarkdown(buffer) {
128
+ let out = buffer;
129
+ const fenceCount = (out.match(/^```/gm) ?? []).length;
130
+ if (fenceCount % 2 === 1) {
131
+ if (!out.endsWith("\n")) out += "\n";
132
+ out += "```";
133
+ return out;
134
+ }
135
+ const tick = (out.match(/`/g) ?? []).length;
136
+ if (tick % 2 === 1) out += "`";
137
+ const bold = (out.match(/\*\*/g) ?? []).length;
138
+ if (bold % 2 === 1) out += "**";
139
+ const strike = (out.match(/~~/g) ?? []).length;
140
+ if (strike % 2 === 1) out += "~~";
141
+ return out;
142
+ }
143
+
144
+ //#endregion
145
+ //#region src/sanitizer.ts
146
+ /** Escape the HTML-significant characters so a string renders as inert text. */
147
+ function escapeHtml(html) {
148
+ return html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
149
+ }
150
+ /**
151
+ * Sanitize an HTML string, stripping scripts and unsafe attributes.
152
+ *
153
+ * When a DOM is available (browser, or Node with jsdom) DOMPurify is used. In a
154
+ * bare Node environment without a global `window`, DOMPurify cannot run, so we
155
+ * fall back to escaping the HTML-significant characters. This never emits raw
156
+ * markup and never throws.
157
+ */
158
+ function sanitizeHtml(html) {
159
+ if (typeof window === "undefined") return escapeHtml(html);
160
+ return DOMPurify.sanitize(html);
161
+ }
162
+
163
+ //#endregion
164
+ //#region src/card-registry.ts
165
+ var CardRegistry = class {
166
+ cards = new Map();
167
+ register(def) {
168
+ this.cards.set(def.type, def);
169
+ }
170
+ has(type) {
171
+ return this.cards.has(type);
172
+ }
173
+ getRender(type) {
174
+ return this.cards.get(type)?.render;
175
+ }
176
+ parse(type, rawJson) {
177
+ const def = this.cards.get(type);
178
+ const { data, complete } = parsePartialJSON(rawJson);
179
+ if (!def) return {
180
+ data,
181
+ complete,
182
+ valid: false
183
+ };
184
+ const valid = complete && this.validate(def, data);
185
+ return {
186
+ data,
187
+ complete,
188
+ valid
189
+ };
190
+ }
191
+ validate(def, data) {
192
+ if (def.validate) return def.validate(data);
193
+ if (def.schema) return validateSchema(def.schema, data);
194
+ return true;
195
+ }
196
+ toPromptSpec() {
197
+ const lines = ["You can output cards. Format: a ```card:<type> fenced block with JSON inside. Available cards:"];
198
+ for (const def of this.cards.values()) {
199
+ lines.push(`- \`card:${def.type}\`: ${def.description}`);
200
+ if (def.schema?.properties) {
201
+ const fields = Object.entries(def.schema.properties).map(([k, v]) => `${k}(${v.type ?? "any"})`).join(", ");
202
+ lines.push(` fields: ${fields}`);
203
+ }
204
+ if (def.example !== void 0) lines.push(` example: ${JSON.stringify(def.example)}`);
205
+ }
206
+ return lines.join("\n");
207
+ }
208
+ toJSONSchema() {
209
+ const properties = {};
210
+ for (const def of this.cards.values()) if (def.schema) properties[def.type] = def.schema;
211
+ return {
212
+ type: "object",
213
+ properties
214
+ };
215
+ }
216
+ };
217
+ /** Minimal JSON Schema validation: covers type / required / properties, enough for cards. */
218
+ function validateSchema(schema, data) {
219
+ if (schema.type === "object") {
220
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
221
+ const obj = data;
222
+ for (const req of schema.required ?? []) if (!(req in obj)) return false;
223
+ for (const [key, sub] of Object.entries(schema.properties ?? {})) if (key in obj && !validateSchema(sub, obj[key])) return false;
224
+ return true;
225
+ }
226
+ if (schema.type === "array") {
227
+ if (!Array.isArray(data)) return false;
228
+ return schema.items ? data.every((d) => validateSchema(schema.items, d)) : true;
229
+ }
230
+ if (schema.type === "string") return typeof data === "string";
231
+ if (schema.type === "number") return typeof data === "number";
232
+ if (schema.type === "boolean") return typeof data === "boolean";
233
+ return true;
234
+ }
235
+
236
+ //#endregion
237
+ //#region src/plugins.ts
238
+ /** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
239
+ function collectNodeRenderers(plugins = []) {
240
+ const map = {};
241
+ for (const p of plugins) for (const [k, v] of Object.entries(p.nodeRenderers ?? {})) map[k] = v;
242
+ return map;
243
+ }
244
+ /** The set of node types claimed by the given plugins. */
245
+ function pluginNodeTypes(plugins = []) {
246
+ return new Set(Object.keys(collectNodeRenderers(plugins)));
247
+ }
248
+
249
+ //#endregion
250
+ //#region src/parser.ts
251
+ /** Build a parser that turns markdown source into a flat list of ASTNodes. */
252
+ function createParser(options = {}) {
253
+ const md = new MarkdownIt({
254
+ html: true,
255
+ linkify: true
256
+ });
257
+ options.configureMd?.(md);
258
+ for (const plugin of options.plugins ?? []) plugin.extendParser?.(md);
259
+ const pluginTypes = pluginNodeTypes(options.plugins);
260
+ return (src) => {
261
+ const tokens = md.parse(src, {});
262
+ const nodes = [];
263
+ let index = 0;
264
+ for (let i = 0; i < tokens.length; i++) {
265
+ const t = tokens[i];
266
+ if (t.type === "fence") {
267
+ const info = t.info.trim();
268
+ if (info.startsWith("card:") && options.registry) {
269
+ const cardType = info.slice(5);
270
+ const res = options.registry.parse(cardType, t.content);
271
+ nodes.push({
272
+ key: `${index++}:card`,
273
+ type: "card",
274
+ card: {
275
+ type: cardType,
276
+ data: res.data,
277
+ complete: res.complete,
278
+ valid: res.valid
279
+ }
280
+ });
281
+ } else if (pluginTypes.has(info)) nodes.push({
282
+ key: `${index++}:${info}`,
283
+ type: info,
284
+ content: t.content,
285
+ attrs: { info }
286
+ });
287
+ else nodes.push({
288
+ key: `${index++}:code`,
289
+ type: "code",
290
+ tag: "code",
291
+ attrs: info ? { lang: info } : void 0,
292
+ content: t.content
293
+ });
294
+ continue;
295
+ }
296
+ if (t.type === "hr") {
297
+ nodes.push({
298
+ key: `${index++}:hr`,
299
+ type: "hr",
300
+ tag: "hr"
301
+ });
302
+ continue;
303
+ }
304
+ if (t.type === "code_block") {
305
+ nodes.push({
306
+ key: `${index++}:code`,
307
+ type: "code",
308
+ tag: "code",
309
+ content: t.content
310
+ });
311
+ continue;
312
+ }
313
+ if (t.type === "html_block") {
314
+ nodes.push({
315
+ key: `${index++}:html`,
316
+ type: "html",
317
+ content: t.content
318
+ });
319
+ continue;
320
+ }
321
+ if (t.type === "heading_open") {
322
+ const inline = tokens[i + 1];
323
+ const raw = inline?.content ?? "";
324
+ nodes.push({
325
+ key: `${index++}:heading`,
326
+ type: "heading",
327
+ tag: t.tag,
328
+ content: raw,
329
+ html: md.renderInline(raw)
330
+ });
331
+ i += 2;
332
+ continue;
333
+ }
334
+ if (t.type === "paragraph_open") {
335
+ const inline = tokens[i + 1];
336
+ const raw = inline?.content ?? "";
337
+ nodes.push({
338
+ key: `${index++}:paragraph`,
339
+ type: "paragraph",
340
+ tag: "p",
341
+ content: raw,
342
+ html: md.renderInline(raw)
343
+ });
344
+ i += 2;
345
+ continue;
346
+ }
347
+ if (t.type.endsWith("_open") && t.level === 0) {
348
+ const closeType = t.type.replace("_open", "_close");
349
+ let j = i;
350
+ let depth = 0;
351
+ for (; j < tokens.length; j++) {
352
+ if (tokens[j].type === t.type) depth++;
353
+ if (tokens[j].type === closeType) {
354
+ depth--;
355
+ if (depth === 0) break;
356
+ }
357
+ }
358
+ const slice = tokens.slice(i, j + 1);
359
+ nodes.push({
360
+ key: `${index++}:${t.type}`,
361
+ type: "html",
362
+ content: md.renderer.render(slice, md.options, {})
363
+ });
364
+ i = j;
365
+ continue;
366
+ }
367
+ if (t.block && t.level === 0) {
368
+ nodes.push({
369
+ key: `${index++}:${t.type}`,
370
+ type: "html",
371
+ content: md.renderer.render([t], md.options, {})
372
+ });
373
+ continue;
374
+ }
375
+ }
376
+ return nodes;
377
+ };
378
+ }
379
+
380
+ //#endregion
381
+ //#region src/diff.ts
382
+ /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
383
+ function diffAst(prev, next) {
384
+ const patches = [];
385
+ const prevByKey = new Map(prev.map((n) => [n.key, n]));
386
+ const nextKeys = new Set(next.map((n) => n.key));
387
+ next.forEach((node, index) => {
388
+ const old = prevByKey.get(node.key);
389
+ if (!old) patches.push({
390
+ op: "insert",
391
+ index,
392
+ node
393
+ });
394
+ else if (!nodeEqual(old, node)) patches.push({
395
+ op: "update",
396
+ key: node.key,
397
+ node
398
+ });
399
+ });
400
+ for (const node of prev) if (!nextKeys.has(node.key)) patches.push({
401
+ op: "remove",
402
+ key: node.key
403
+ });
404
+ return patches;
405
+ }
406
+ function nodeEqual(a, b) {
407
+ return JSON.stringify(a) === JSON.stringify(b);
408
+ }
409
+
410
+ //#endregion
411
+ //#region src/renderer.ts
412
+ /**
413
+ * Streaming render orchestrator: accumulate incoming markdown chunks, repair the
414
+ * partial buffer, parse it into an AST, diff against the previous AST, and emit
415
+ * the resulting patches via `onPatch`.
416
+ */
417
+ var Renderer = class {
418
+ buffer = "";
419
+ prevAst = [];
420
+ parse;
421
+ options;
422
+ sanitize;
423
+ constructor(options = {}) {
424
+ this.options = options;
425
+ this.sanitize = options.sanitize !== false;
426
+ if (options.registry) for (const plugin of options.plugins ?? []) for (const card of plugin.cards ?? []) options.registry.register(card);
427
+ this.parse = createParser({
428
+ registry: options.registry,
429
+ plugins: options.plugins
430
+ });
431
+ }
432
+ push(chunk) {
433
+ this.buffer += chunk;
434
+ this.render();
435
+ }
436
+ async feed(source) {
437
+ if (Symbol.asyncIterator in source) {
438
+ for await (const chunk of source) this.push(chunk);
439
+ return;
440
+ }
441
+ const reader = source.getReader();
442
+ for (;;) {
443
+ const { done, value } = await reader.read();
444
+ if (done) break;
445
+ if (value != null) this.push(value);
446
+ }
447
+ }
448
+ reset() {
449
+ this.buffer = "";
450
+ this.prevAst = [];
451
+ }
452
+ render() {
453
+ const repaired = repairMarkdown(this.buffer);
454
+ const nextAst = this.parse(repaired);
455
+ if (this.sanitize) sanitizeNodes(nextAst);
456
+ const patches = diffAst(this.prevAst, nextAst);
457
+ this.prevAst = nextAst;
458
+ if (patches.length > 0) this.options.onPatch?.(patches, nextAst);
459
+ }
460
+ };
461
+ /**
462
+ * Recursively sanitize node markup in place: the content of `html` nodes and
463
+ * the rendered inline `html` field carried by any node.
464
+ */
465
+ function sanitizeNodes(nodes) {
466
+ for (const node of nodes) {
467
+ if (node.type === "html" && typeof node.content === "string") node.content = sanitizeHtml(node.content);
468
+ if (node.html) node.html = sanitizeHtml(node.html);
469
+ if (node.children) sanitizeNodes(node.children);
470
+ }
471
+ }
472
+
473
+ //#endregion
474
+ //#region src/stream-router.ts
475
+ const DEFAULT_CHANNEL = "content";
476
+ /**
477
+ * Demultiplexes one incoming stream into multiple named channels so different
478
+ * UI regions can update independently (e.g. `progress` + `content` from a single
479
+ * SSE). Framing is auto-detected per line and supports:
480
+ *
481
+ * 1. JSON envelope: `{"ch":"<channel>","delta":"text"}` (a text delta) or
482
+ * `{"ch":"<channel>","data":<any>}` (a structured value). May appear bare or
483
+ * after a `data: ` prefix.
484
+ * 2. SSE `event: <name>` sets the channel for the following `data:` line(s).
485
+ * 3. A plain `data: <text>` line with no `ch` and no preceding `event:` is a
486
+ * text delta on the default `content` channel.
487
+ */
488
+ var StreamRouter = class {
489
+ sinks = new Map();
490
+ handlers = new Map();
491
+ /** Bind a text-stream sink to a channel: text deltas call `sink.push(delta)`. */
492
+ channel(name, sink) {
493
+ this.sinks.set(name, sink);
494
+ return this;
495
+ }
496
+ /** Bind a handler to a channel: data values (and deltas as strings) call it. */
497
+ on(name, handler) {
498
+ this.handlers.set(name, handler);
499
+ return this;
500
+ }
501
+ /**
502
+ * Consume a stream to completion, dispatching each parsed line to its channel.
503
+ * Accepts an async iterable of strings, or a `ReadableStream` of either strings
504
+ * or `Uint8Array` bytes (decoded as UTF-8).
505
+ */
506
+ async feed(source) {
507
+ let buffer = "";
508
+ let currentEvent;
509
+ const consume = (chunk) => {
510
+ buffer += chunk;
511
+ let index;
512
+ while ((index = buffer.indexOf("\n")) !== -1) {
513
+ const line = buffer.slice(0, index);
514
+ buffer = buffer.slice(index + 1);
515
+ currentEvent = this.processLine(line, currentEvent);
516
+ }
517
+ };
518
+ if ("getReader" in source && typeof source.getReader === "function") {
519
+ const reader = source.getReader();
520
+ const decoder = new TextDecoder();
521
+ for (;;) {
522
+ const { done, value } = await reader.read();
523
+ if (done) break;
524
+ if (value == null) continue;
525
+ consume(typeof value === "string" ? value : decoder.decode(value, { stream: true }));
526
+ }
527
+ const tail = decoder.decode();
528
+ if (tail) consume(tail);
529
+ } else for await (const chunk of source) consume(chunk);
530
+ if (buffer.length > 0) this.processLine(buffer, currentEvent);
531
+ }
532
+ /** Process a single raw line; returns the (possibly updated) current event. */
533
+ processLine(rawLine, currentEvent) {
534
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
535
+ if (line.trim() === "") return void 0;
536
+ if (line.startsWith("event:")) return line.slice(6).trim();
537
+ let payload = line;
538
+ if (payload.startsWith("data:")) {
539
+ payload = payload.slice(5);
540
+ if (payload.startsWith(" ")) payload = payload.slice(1);
541
+ }
542
+ if (payload.startsWith("{")) {
543
+ const parsed = tryParseJson(payload);
544
+ if (parsed !== void 0 && isRecord(parsed) && typeof parsed.ch === "string") {
545
+ if ("delta" in parsed) this.routeDelta(parsed.ch, String(parsed.delta));
546
+ else this.routeData(parsed.ch, parsed.data);
547
+ return currentEvent;
548
+ }
549
+ if (parsed !== void 0) {
550
+ this.routeData(currentEvent ?? DEFAULT_CHANNEL, parsed);
551
+ return currentEvent;
552
+ }
553
+ }
554
+ if (currentEvent !== void 0) {
555
+ const parsed = tryParseJson(payload);
556
+ this.routeData(currentEvent, parsed !== void 0 ? parsed : payload);
557
+ return currentEvent;
558
+ }
559
+ this.routeDelta(DEFAULT_CHANNEL, payload);
560
+ return currentEvent;
561
+ }
562
+ /** Route a text delta: to a bound sink and/or an on() handler (either/both). */
563
+ routeDelta(channel, text) {
564
+ const sink = this.sinks.get(channel);
565
+ const handler = this.handlers.get(channel);
566
+ if (sink) sink.push(text);
567
+ if (handler) handler(text);
568
+ }
569
+ /** Route a structured value: to the handler, or a string value to a lone sink. */
570
+ routeData(channel, value) {
571
+ const handler = this.handlers.get(channel);
572
+ if (handler) {
573
+ handler(value);
574
+ return;
575
+ }
576
+ const sink = this.sinks.get(channel);
577
+ if (sink && typeof value === "string") sink.push(value);
578
+ }
579
+ };
580
+ function tryParseJson(text) {
581
+ try {
582
+ return JSON.parse(text);
583
+ } catch {
584
+ return void 0;
585
+ }
586
+ }
587
+ function isRecord(value) {
588
+ return typeof value === "object" && value !== null;
589
+ }
590
+
591
+ //#endregion
592
+ //#region src/build-system-prompt.ts
593
+ /**
594
+ * Assembles the LLM system-prompt guidance: an optional base, the registered
595
+ * cards' spec, and every plugin's promptSpec. Consumers prepend this to their
596
+ * own system prompt so the model knows which fenced blocks it may emit.
597
+ */
598
+ function buildSystemPrompt(options = {}) {
599
+ const parts = [];
600
+ if (options.base) parts.push(options.base);
601
+ const cardSpec = options.registry?.toPromptSpec();
602
+ if (options.registry && hasCards(options.registry) && cardSpec) parts.push(cardSpec);
603
+ for (const p of options.plugins ?? []) if (p.promptSpec) parts.push(p.promptSpec);
604
+ return parts.join("\n\n");
605
+ }
606
+ function hasCards(registry) {
607
+ return registry.toPromptSpec().includes("card:");
608
+ }
609
+
610
+ //#endregion
611
+ export { CardRegistry, Renderer, StreamRouter, buildSystemPrompt, collectNodeRenderers, createParser, diffAst, parsePartialJSON, pluginNodeTypes, repairMarkdown, sanitizeHtml };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@ai-gui/core",
3
+ "version": "0.1.0",
4
+ "description": "Headless streaming renderer core for LLM-generated UI — markdown/JSON repair, card registry, AST diff, plugin engine, sanitizer.",
5
+ "keywords": [
6
+ "llm",
7
+ "ai",
8
+ "streaming",
9
+ "markdown",
10
+ "renderer",
11
+ "headless",
12
+ "sanitizer",
13
+ "ast-diff",
14
+ "aigui"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Liang Li <ll_faw@hotmail.com>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/liliang-cn/aigui.git",
21
+ "directory": "packages/core"
22
+ },
23
+ "homepage": "https://github.com/liliang-cn/aigui#readme",
24
+ "bugs": "https://github.com/liliang-cn/aigui/issues",
25
+ "type": "module",
26
+ "main": "./dist/index.cjs",
27
+ "module": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js",
33
+ "require": "./dist/index.cjs"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "dompurify": "^3.4.12",
46
+ "markdown-it": "^14.1.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/markdown-it": "^14.1.2",
50
+ "jsdom": "^29.1.1"
51
+ },
52
+ "scripts": {
53
+ "build": "tsdown",
54
+ "typecheck": "tsc --noEmit"
55
+ }
56
+ }