@mdocui/core 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/README.md +299 -0
- package/dist/index.cjs +614 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +140 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +580 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# @mdocui/core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic streaming parser, component registry, and system prompt generator for LLM generative UI. Parses Markdoc `{% %}` tag syntax from streamed LLM output into a typed AST that any renderer can consume.
|
|
4
|
+
|
|
5
|
+
Part of the [mdocui](https://github.com/mdocui/mdocui) monorepo.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @mdocui/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
`@mdocui/core` provides three things:
|
|
16
|
+
|
|
17
|
+
1. **Streaming parser** -- incrementally tokenizes and parses Markdoc tags from chunked text (e.g. an LLM response stream).
|
|
18
|
+
2. **Component registry** -- defines available components with Zod schemas so the parser can validate props and the prompt generator can describe them to the model.
|
|
19
|
+
3. **Prompt generator** -- turns a registry into a system prompt section that teaches an LLM how to emit mdocUI markup.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## API Reference
|
|
24
|
+
|
|
25
|
+
### `Tokenizer`
|
|
26
|
+
|
|
27
|
+
Low-level character-by-character tokenizer that splits raw text into `Token` objects. Handles `{% tag %}` boundaries, string quoting, and escape sequences. Most users should use `StreamingParser` instead.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { Tokenizer, TokenType } from '@mdocui/core'
|
|
31
|
+
|
|
32
|
+
const tokenizer = new Tokenizer()
|
|
33
|
+
|
|
34
|
+
const tokens = tokenizer.write('Hello {% button action="go" label="Click" /%}')
|
|
35
|
+
// tokens[0] => { type: 'PROSE', raw: 'Hello ' }
|
|
36
|
+
// tokens[1] => { type: 'TAG_SELF_CLOSE', raw: '{% button ... /%}', name: 'button', attrs: 'action="go" label="Click"' }
|
|
37
|
+
|
|
38
|
+
// Flush any remaining buffer when the stream ends
|
|
39
|
+
const remaining = tokenizer.flush()
|
|
40
|
+
|
|
41
|
+
// Reset for reuse
|
|
42
|
+
tokenizer.reset()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Token types:** `PROSE`, `TAG_OPEN`, `TAG_SELF_CLOSE`, `TAG_CLOSE`
|
|
46
|
+
|
|
47
|
+
**Tokenizer states:** `IN_PROSE`, `IN_TAG`, `IN_STRING`
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### `StreamingParser`
|
|
52
|
+
|
|
53
|
+
Incremental parser that converts a stream of text chunks into an `ASTNode[]` tree. Tags are matched by name, nested correctly, and force-closed on flush if unclosed.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { StreamingParser } from '@mdocui/core'
|
|
57
|
+
|
|
58
|
+
const parser = new StreamingParser({
|
|
59
|
+
knownTags: new Set(['card', 'button']),
|
|
60
|
+
dropUnknown: true, // default -- silently drops unknown tags
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Feed chunks as they arrive from the LLM
|
|
64
|
+
let newNodes = parser.write('Here is a card:\n{% card title="Hello" %}')
|
|
65
|
+
newNodes = parser.write('Card body content')
|
|
66
|
+
newNodes = parser.write('{% /card %}')
|
|
67
|
+
|
|
68
|
+
// Finalize -- force-closes any unclosed tags
|
|
69
|
+
const finalNodes = parser.flush()
|
|
70
|
+
|
|
71
|
+
// Access the full AST
|
|
72
|
+
const allNodes = parser.getNodes() // ASTNode[]
|
|
73
|
+
|
|
74
|
+
// Inspect errors and status
|
|
75
|
+
const meta = parser.getMeta() // ParseMeta
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### `ParserOptions`
|
|
79
|
+
|
|
80
|
+
| Option | Type | Default | Description |
|
|
81
|
+
|--------|------|---------|-------------|
|
|
82
|
+
| `knownTags` | `Set<string>` | `new Set()` (allow all) | Tags the parser accepts. Empty set allows everything. |
|
|
83
|
+
| `dropUnknown` | `boolean` | `true` | When `true`, unknown tags are silently dropped. When `false`, they are emitted as prose. |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### `ComponentRegistry`
|
|
88
|
+
|
|
89
|
+
Typed store of component definitions. Used to generate the `knownTags` set for the parser and the system prompt for the LLM.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { ComponentRegistry, defineComponent } from '@mdocui/core'
|
|
93
|
+
import { z } from 'zod'
|
|
94
|
+
|
|
95
|
+
const registry = new ComponentRegistry()
|
|
96
|
+
|
|
97
|
+
registry.register(
|
|
98
|
+
defineComponent({
|
|
99
|
+
name: 'alert',
|
|
100
|
+
description: 'Displays a colored alert box',
|
|
101
|
+
props: z.object({
|
|
102
|
+
severity: z.enum(['info', 'warning', 'error']).describe('Alert severity'),
|
|
103
|
+
title: z.string().optional().describe('Optional heading'),
|
|
104
|
+
}),
|
|
105
|
+
children: 'any', // 'none' | 'any' | string[]
|
|
106
|
+
})
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// Batch register
|
|
110
|
+
registry.registerAll([alertDef, cardDef])
|
|
111
|
+
|
|
112
|
+
// Query
|
|
113
|
+
registry.has('alert') // true
|
|
114
|
+
registry.get('alert') // ComponentDefinition | undefined
|
|
115
|
+
registry.names() // ['alert', ...]
|
|
116
|
+
registry.all() // ComponentDefinition[]
|
|
117
|
+
registry.knownTags() // Set<string>
|
|
118
|
+
|
|
119
|
+
// Validate props against the Zod schema
|
|
120
|
+
const result = registry.validate('alert', { severity: 'info' })
|
|
121
|
+
// { valid: true, errors: [], props: { severity: 'info' } }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### `defineComponent`
|
|
127
|
+
|
|
128
|
+
Identity helper that returns a `ComponentDefinition` unchanged. Provides type inference when defining components outside a registry.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { defineComponent } from '@mdocui/core'
|
|
132
|
+
import { z } from 'zod'
|
|
133
|
+
|
|
134
|
+
export const myComponent = defineComponent({
|
|
135
|
+
name: 'my-component',
|
|
136
|
+
description: 'Does something useful',
|
|
137
|
+
props: z.object({
|
|
138
|
+
value: z.number().describe('A numeric value'),
|
|
139
|
+
}),
|
|
140
|
+
children: 'none',
|
|
141
|
+
streaming: { value: true }, // mark props that can stream partial values
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### `generatePrompt`
|
|
148
|
+
|
|
149
|
+
Generates a system prompt section from a registry that teaches an LLM the available components, tag syntax rules, and streaming guidelines.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { generatePrompt, ComponentRegistry } from '@mdocui/core'
|
|
153
|
+
|
|
154
|
+
const registry = new ComponentRegistry()
|
|
155
|
+
// ... register components ...
|
|
156
|
+
|
|
157
|
+
const prompt = generatePrompt(registry, {
|
|
158
|
+
preamble: 'You are a helpful assistant.',
|
|
159
|
+
additionalRules: [
|
|
160
|
+
'Always use a card for structured answers.',
|
|
161
|
+
'Never nest more than 3 levels deep.',
|
|
162
|
+
],
|
|
163
|
+
examples: [
|
|
164
|
+
'{% card title="Weather" %}\nSunny, 72F\n{% /card %}',
|
|
165
|
+
],
|
|
166
|
+
groups: [
|
|
167
|
+
{
|
|
168
|
+
name: 'Layout',
|
|
169
|
+
components: ['stack', 'grid', 'card'],
|
|
170
|
+
notes: ['Use stack for vertical/horizontal layouts'],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### `PromptOptions`
|
|
177
|
+
|
|
178
|
+
| Option | Type | Description |
|
|
179
|
+
|--------|------|-------------|
|
|
180
|
+
| `preamble` | `string` | Text prepended before the syntax section |
|
|
181
|
+
| `additionalRules` | `string[]` | Extra rules appended as a bullet list |
|
|
182
|
+
| `examples` | `string[]` | Example markup blocks appended at the end |
|
|
183
|
+
| `groups` | `ComponentGroup[]` | Groups components under named headings with optional notes |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### `parseAttributes`
|
|
188
|
+
|
|
189
|
+
Parses the attribute string inside a `{% tag ... %}` into a key-value record. Handles quoted strings (with escape sequences), arrays via JSON `[...]`, bare booleans, numbers, and null. Prototype pollution keys (`__proto__`, `constructor`, `prototype`) are silently skipped.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { parseAttributes } from '@mdocui/core'
|
|
193
|
+
|
|
194
|
+
parseAttributes('action="go" label="Click me" count=42 disabled')
|
|
195
|
+
// { action: 'go', label: 'Click me', count: 42, disabled: true }
|
|
196
|
+
|
|
197
|
+
parseAttributes('options=["a","b","c"] required=true')
|
|
198
|
+
// { options: ['a', 'b', 'c'], required: true }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Types
|
|
204
|
+
|
|
205
|
+
### `ASTNode`
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
type ASTNode = ProseNode | ComponentNode
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `ProseNode`
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
interface ProseNode {
|
|
215
|
+
type: 'prose'
|
|
216
|
+
content: string
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `ComponentNode`
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
interface ComponentNode {
|
|
224
|
+
type: 'component'
|
|
225
|
+
name: string
|
|
226
|
+
props: Record<string, unknown>
|
|
227
|
+
children: ASTNode[]
|
|
228
|
+
selfClosing: boolean
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### `ComponentDefinition`
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
interface ComponentDefinition {
|
|
236
|
+
name: string
|
|
237
|
+
description: string
|
|
238
|
+
props: z.ZodObject<z.ZodRawShape>
|
|
239
|
+
children?: 'none' | 'any' | string[]
|
|
240
|
+
streaming?: Record<string, boolean>
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `ActionEvent`
|
|
245
|
+
|
|
246
|
+
Fired by interactive components in the renderer layer.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
interface ActionEvent {
|
|
250
|
+
type: 'button_click' | 'form_submit' | 'select_change' | 'link_click'
|
|
251
|
+
action: string
|
|
252
|
+
label?: string
|
|
253
|
+
formName?: string
|
|
254
|
+
formState?: Record<string, unknown>
|
|
255
|
+
tagName: string
|
|
256
|
+
params?: Record<string, unknown>
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### `ParseMeta`
|
|
261
|
+
|
|
262
|
+
Returned by `parser.getMeta()`.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
interface ParseMeta {
|
|
266
|
+
errors: ParseError[]
|
|
267
|
+
nodeCount: number
|
|
268
|
+
isComplete: boolean
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### `ParseError`
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
interface ParseError {
|
|
276
|
+
code: 'unknown_tag' | 'validation' | 'malformed' | 'unclosed'
|
|
277
|
+
tagName: string
|
|
278
|
+
message: string
|
|
279
|
+
raw?: string
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### `ValidationResult`
|
|
284
|
+
|
|
285
|
+
Returned by `registry.validate()`.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
interface ValidationResult {
|
|
289
|
+
valid: boolean
|
|
290
|
+
errors: string[]
|
|
291
|
+
props?: Record<string, unknown>
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
See the root [mdocui](https://github.com/mdocui/mdocui) repository for license details.
|