@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 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.