@protomarkdown/parser 1.0.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/LICENSE +201 -0
- package/README.md +25 -0
- package/dist/ShadcnCodeGenerator.d.ts +45 -0
- package/dist/ShadcnCodeGenerator.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +820 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +823 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/MarkdownParser.d.ts +11 -0
- package/dist/parser/MarkdownParser.d.ts.map +1 -0
- package/dist/parser/index.d.ts +3 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/types.d.ts +30 -0
- package/dist/parser/types.d.ts.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
class MarkdownParser {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.options = {
|
|
4
|
+
strict: false,
|
|
5
|
+
preserveWhitespace: false,
|
|
6
|
+
...options,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
parse(markdown) {
|
|
10
|
+
const lines = markdown.split("\n");
|
|
11
|
+
const nodes = [];
|
|
12
|
+
const errors = [];
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < lines.length) {
|
|
15
|
+
const line = this.options.preserveWhitespace ? lines[i] : lines[i].trim();
|
|
16
|
+
if (!line) {
|
|
17
|
+
i++;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
// Check for table (line with pipes)
|
|
21
|
+
if (line.includes("|") && line.trim().startsWith("|")) {
|
|
22
|
+
const headers = line
|
|
23
|
+
.split("|")
|
|
24
|
+
.map((h) => h.trim())
|
|
25
|
+
.filter((h) => h.length > 0);
|
|
26
|
+
i++;
|
|
27
|
+
// Skip separator line (|---|---|)
|
|
28
|
+
if (i < lines.length) {
|
|
29
|
+
const separatorLine = this.options.preserveWhitespace
|
|
30
|
+
? lines[i]
|
|
31
|
+
: lines[i].trim();
|
|
32
|
+
if (separatorLine.includes("-") && separatorLine.includes("|")) {
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Parse table rows
|
|
37
|
+
const rows = [];
|
|
38
|
+
while (i < lines.length) {
|
|
39
|
+
const rowLine = this.options.preserveWhitespace
|
|
40
|
+
? lines[i]
|
|
41
|
+
: lines[i].trim();
|
|
42
|
+
if (!rowLine || !rowLine.includes("|")) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
const cells = rowLine
|
|
46
|
+
.split("|")
|
|
47
|
+
.map((c) => c.trim())
|
|
48
|
+
.filter((c) => c.length > 0);
|
|
49
|
+
rows.push(cells);
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
nodes.push({
|
|
53
|
+
type: "table",
|
|
54
|
+
headers,
|
|
55
|
+
rows,
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Check for card start
|
|
60
|
+
const cardMatch = line.match(/^\[--\s*(.*)$/);
|
|
61
|
+
if (cardMatch) {
|
|
62
|
+
const result = this.parseCard(lines, i, cardMatch[1] || undefined);
|
|
63
|
+
nodes.push(result.node);
|
|
64
|
+
i = result.nextIndex;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Check for grid start ([grid cols-2 gap-4)
|
|
68
|
+
const gridMatch = line.match(/^\[grid\s+(.*)$/);
|
|
69
|
+
if (gridMatch) {
|
|
70
|
+
const result = this.parseContainer(lines, i, 'grid', gridMatch[1]);
|
|
71
|
+
nodes.push(result.node);
|
|
72
|
+
i = result.nextIndex;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// Check for plain div start ([ or [ class-name)
|
|
76
|
+
const divMatch = line.match(/^\[\s*(.*)$/);
|
|
77
|
+
if (divMatch && !line.includes("]")) {
|
|
78
|
+
const result = this.parseContainer(lines, i, 'div', divMatch[1] || '');
|
|
79
|
+
nodes.push(result.node);
|
|
80
|
+
i = result.nextIndex;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const node = this.parseLine(line);
|
|
84
|
+
if (node) {
|
|
85
|
+
nodes.push(node);
|
|
86
|
+
}
|
|
87
|
+
else if (this.options.strict) {
|
|
88
|
+
errors.push(`Line ${i + 1}: Unable to parse "${line}"`);
|
|
89
|
+
}
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
return { nodes, errors: errors.length > 0 ? errors : undefined };
|
|
93
|
+
}
|
|
94
|
+
parseCard(lines, startIndex, title) {
|
|
95
|
+
const cardChildren = [];
|
|
96
|
+
let i = startIndex + 1;
|
|
97
|
+
let depth = 1;
|
|
98
|
+
// Parse card content until we find the matching closing --]
|
|
99
|
+
while (i < lines.length && depth > 0) {
|
|
100
|
+
const cardLine = this.options.preserveWhitespace ? lines[i] : lines[i].trim();
|
|
101
|
+
// Check for card closing
|
|
102
|
+
if (cardLine === "--]") {
|
|
103
|
+
depth--;
|
|
104
|
+
if (depth === 0) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Check for nested card opening (must be before div check)
|
|
109
|
+
if (cardLine.match(/^\[--\s*(.*)$/)) {
|
|
110
|
+
const nestedTitle = cardLine.match(/^\[--\s*(.*)$/)?.[1] || undefined;
|
|
111
|
+
const result = this.parseCard(lines, i, nestedTitle);
|
|
112
|
+
cardChildren.push(result.node);
|
|
113
|
+
i = result.nextIndex;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Check for nested grid opening
|
|
117
|
+
if (cardLine.match(/^\[grid\s+(.*)$/)) {
|
|
118
|
+
const nestedConfig = cardLine.match(/^\[grid\s+(.*)$/)?.[1] || '';
|
|
119
|
+
const result = this.parseContainer(lines, i, 'grid', nestedConfig);
|
|
120
|
+
cardChildren.push(result.node);
|
|
121
|
+
i = result.nextIndex;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Check for nested div opening
|
|
125
|
+
if (cardLine.match(/^\[\s*(.*)$/) && !cardLine.includes("]")) {
|
|
126
|
+
const nestedConfig = cardLine.match(/^\[\s*(.*)$/)?.[1] || '';
|
|
127
|
+
const result = this.parseContainer(lines, i, 'div', nestedConfig);
|
|
128
|
+
cardChildren.push(result.node);
|
|
129
|
+
i = result.nextIndex;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (cardLine) {
|
|
133
|
+
const childNode = this.parseLine(cardLine);
|
|
134
|
+
if (childNode) {
|
|
135
|
+
cardChildren.push(childNode);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
node: {
|
|
142
|
+
type: "card",
|
|
143
|
+
titleChildren: title ? this.parseInlineEmphasis(title) : undefined,
|
|
144
|
+
children: cardChildren,
|
|
145
|
+
},
|
|
146
|
+
nextIndex: i + 1,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
parseContainer(lines, startIndex, type, config) {
|
|
150
|
+
const containerChildren = [];
|
|
151
|
+
let i = startIndex + 1;
|
|
152
|
+
let depth = 1;
|
|
153
|
+
// Parse container content until we find the matching closing ]
|
|
154
|
+
while (i < lines.length && depth > 0) {
|
|
155
|
+
const containerLine = this.options.preserveWhitespace ? lines[i] : lines[i].trim();
|
|
156
|
+
// Check for container closing
|
|
157
|
+
if (containerLine === "]") {
|
|
158
|
+
depth--;
|
|
159
|
+
if (depth === 0) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Check for nested card opening (must be before div check)
|
|
164
|
+
if (containerLine.match(/^\[--\s*(.*)$/)) {
|
|
165
|
+
const nestedTitle = containerLine.match(/^\[--\s*(.*)$/)?.[1] || undefined;
|
|
166
|
+
const result = this.parseCard(lines, i, nestedTitle);
|
|
167
|
+
containerChildren.push(result.node);
|
|
168
|
+
i = result.nextIndex;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Check for nested grid opening
|
|
172
|
+
if (containerLine.match(/^\[grid\s+(.*)$/)) {
|
|
173
|
+
const nestedConfig = containerLine.match(/^\[grid\s+(.*)$/)?.[1] || '';
|
|
174
|
+
const result = this.parseContainer(lines, i, 'grid', nestedConfig);
|
|
175
|
+
containerChildren.push(result.node);
|
|
176
|
+
i = result.nextIndex;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Check for nested div opening
|
|
180
|
+
if (containerLine.match(/^\[\s*(.*)$/) && !containerLine.includes("]")) {
|
|
181
|
+
const nestedConfig = containerLine.match(/^\[\s*(.*)$/)?.[1] || '';
|
|
182
|
+
const result = this.parseContainer(lines, i, 'div', nestedConfig);
|
|
183
|
+
containerChildren.push(result.node);
|
|
184
|
+
i = result.nextIndex;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (containerLine) {
|
|
188
|
+
const childNode = this.parseLine(containerLine);
|
|
189
|
+
if (childNode) {
|
|
190
|
+
containerChildren.push(childNode);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
const node = {
|
|
196
|
+
type,
|
|
197
|
+
children: containerChildren,
|
|
198
|
+
};
|
|
199
|
+
if (type === 'grid') {
|
|
200
|
+
node.gridConfig = config;
|
|
201
|
+
}
|
|
202
|
+
else if (type === 'div' && config) {
|
|
203
|
+
node.className = config;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
node,
|
|
207
|
+
nextIndex: i + 1,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
parseLine(line) {
|
|
211
|
+
// Parse headers (# H1, ## H2, etc.)
|
|
212
|
+
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
213
|
+
if (headerMatch) {
|
|
214
|
+
return {
|
|
215
|
+
type: "header",
|
|
216
|
+
level: headerMatch[1].length,
|
|
217
|
+
children: this.parseInlineEmphasis(headerMatch[2]),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Parse multiple form fields on one line (inputs, textareas, dropdowns, checkboxes)
|
|
221
|
+
// This must be checked BEFORE single field patterns
|
|
222
|
+
const fieldPattern = /(.+?)\s+(___|\|___\||__\*|__>(?:\s*\[[^\]]+\])?|__\[\])/g;
|
|
223
|
+
const fieldMatches = [...line.matchAll(fieldPattern)];
|
|
224
|
+
if (fieldMatches.length > 1) {
|
|
225
|
+
return {
|
|
226
|
+
type: "container",
|
|
227
|
+
children: fieldMatches.map((match) => {
|
|
228
|
+
const label = match[1].trim();
|
|
229
|
+
const marker = match[2];
|
|
230
|
+
if (marker === '__*') {
|
|
231
|
+
return {
|
|
232
|
+
type: "input",
|
|
233
|
+
label,
|
|
234
|
+
inputType: "password",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
else if (marker === '|___|') {
|
|
238
|
+
return {
|
|
239
|
+
type: "textarea",
|
|
240
|
+
label,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
else if (marker === '__[]') {
|
|
244
|
+
return {
|
|
245
|
+
type: "checkbox",
|
|
246
|
+
label,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
else if (marker.startsWith('__>')) {
|
|
250
|
+
// Handle dropdown with or without options
|
|
251
|
+
const optionsMatch = marker.match(/\[([^\]]+)\]/);
|
|
252
|
+
if (optionsMatch) {
|
|
253
|
+
const options = optionsMatch[1]
|
|
254
|
+
.split(",")
|
|
255
|
+
.map((opt) => opt.trim())
|
|
256
|
+
.filter((opt) => opt.length > 0);
|
|
257
|
+
return {
|
|
258
|
+
type: "dropdown",
|
|
259
|
+
label,
|
|
260
|
+
options,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
return {
|
|
265
|
+
type: "dropdown",
|
|
266
|
+
label,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else { // ___
|
|
271
|
+
return {
|
|
272
|
+
type: "input",
|
|
273
|
+
label,
|
|
274
|
+
inputType: "text",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Parse password inputs (Label __*)
|
|
281
|
+
const passwordMatch = line.match(/^(.+?)\s+__\*$/);
|
|
282
|
+
if (passwordMatch) {
|
|
283
|
+
return {
|
|
284
|
+
type: "input",
|
|
285
|
+
label: passwordMatch[1],
|
|
286
|
+
inputType: "password",
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Parse textarea (Label |___|)
|
|
290
|
+
const textareaMatch = line.match(/^(.+?)\s+\|___\|$/);
|
|
291
|
+
if (textareaMatch) {
|
|
292
|
+
return {
|
|
293
|
+
type: "textarea",
|
|
294
|
+
label: textareaMatch[1],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// Parse text inputs (Label ___)
|
|
298
|
+
const inputMatch = line.match(/^(.+?)\s+___$/);
|
|
299
|
+
if (inputMatch) {
|
|
300
|
+
return {
|
|
301
|
+
type: "input",
|
|
302
|
+
label: inputMatch[1],
|
|
303
|
+
inputType: "text",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// Parse checkbox (Label __[])
|
|
307
|
+
const checkboxMatch = line.match(/^(.+?)\s+__\[\]$/);
|
|
308
|
+
if (checkboxMatch) {
|
|
309
|
+
return {
|
|
310
|
+
type: "checkbox",
|
|
311
|
+
label: checkboxMatch[1],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
// Parse radio group (Label __() [option1, option2, option3])
|
|
315
|
+
const radioGroupMatch = line.match(/^(.+?)\s+__\(\)\s+\[(.+?)\]$/);
|
|
316
|
+
if (radioGroupMatch) {
|
|
317
|
+
const options = radioGroupMatch[2]
|
|
318
|
+
.split(",")
|
|
319
|
+
.map((opt) => opt.trim())
|
|
320
|
+
.filter((opt) => opt.length > 0);
|
|
321
|
+
return {
|
|
322
|
+
type: "radiogroup",
|
|
323
|
+
label: radioGroupMatch[1],
|
|
324
|
+
options,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
// Parse dropdowns with options (Label __> [option1, option2, option3])
|
|
328
|
+
const dropdownWithOptionsMatch = line.match(/^(.+?)\s+__>\s+\[(.+?)\]$/);
|
|
329
|
+
if (dropdownWithOptionsMatch) {
|
|
330
|
+
const options = dropdownWithOptionsMatch[2]
|
|
331
|
+
.split(",")
|
|
332
|
+
.map((opt) => opt.trim())
|
|
333
|
+
.filter((opt) => opt.length > 0);
|
|
334
|
+
return {
|
|
335
|
+
type: "dropdown",
|
|
336
|
+
label: dropdownWithOptionsMatch[1],
|
|
337
|
+
options,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
// Parse dropdowns without options (Label __>)
|
|
341
|
+
const dropdownMatch = line.match(/^(.+?)\s+__>$/);
|
|
342
|
+
if (dropdownMatch) {
|
|
343
|
+
return {
|
|
344
|
+
type: "dropdown",
|
|
345
|
+
label: dropdownMatch[1],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// Parse image ()
|
|
349
|
+
const imageMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
350
|
+
if (imageMatch) {
|
|
351
|
+
return {
|
|
352
|
+
type: "image",
|
|
353
|
+
alt: imageMatch[1],
|
|
354
|
+
src: imageMatch[2],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// Parse multiple buttons on one line ([btn1][(btn2)])
|
|
358
|
+
const multiButtonMatch = line.match(/^(\[\(?[^\[\]|]+\)?\]\s*)+$/);
|
|
359
|
+
if (multiButtonMatch) {
|
|
360
|
+
const buttons = line.match(/\[(\(?)[^\[\]|]+?(\)?)\]/g);
|
|
361
|
+
if (buttons && buttons.length > 1) {
|
|
362
|
+
return {
|
|
363
|
+
type: "container",
|
|
364
|
+
children: buttons.map((btn) => {
|
|
365
|
+
const innerMatch = btn.match(/\[(\(?)(.+?)(\)?)\]/);
|
|
366
|
+
if (innerMatch) {
|
|
367
|
+
const isDefault = innerMatch[1] === "(" && innerMatch[3] === ")";
|
|
368
|
+
const content = innerMatch[2];
|
|
369
|
+
return {
|
|
370
|
+
type: "button",
|
|
371
|
+
content,
|
|
372
|
+
variant: isDefault ? "default" : "outline",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
type: "button",
|
|
377
|
+
content: btn.slice(1, -1),
|
|
378
|
+
variant: "outline",
|
|
379
|
+
};
|
|
380
|
+
}),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Parse default button [(button text)] or [(button text) | classes]
|
|
385
|
+
const defaultButtonMatch = line.match(/^\[\((.+?)\)(?:\s*\|\s*(.+))?\]$/);
|
|
386
|
+
if (defaultButtonMatch) {
|
|
387
|
+
const content = defaultButtonMatch[1];
|
|
388
|
+
const className = defaultButtonMatch[2]?.trim();
|
|
389
|
+
return {
|
|
390
|
+
type: "button",
|
|
391
|
+
content,
|
|
392
|
+
variant: "default",
|
|
393
|
+
...(className && { className }),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
// Parse outline button [button text] or [button text | classes]
|
|
397
|
+
const buttonMatch = line.match(/^\[([^|]+?)(?:\s*\|\s*(.+))?\]$/);
|
|
398
|
+
if (buttonMatch) {
|
|
399
|
+
const content = buttonMatch[1].trim();
|
|
400
|
+
const className = buttonMatch[2]?.trim();
|
|
401
|
+
return {
|
|
402
|
+
type: "button",
|
|
403
|
+
content,
|
|
404
|
+
variant: "outline",
|
|
405
|
+
...(className && { className }),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
// Plain text
|
|
409
|
+
return {
|
|
410
|
+
type: "text",
|
|
411
|
+
children: this.parseInlineEmphasis(line),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
parseInlineEmphasis(text) {
|
|
415
|
+
const nodes = [];
|
|
416
|
+
let remaining = text;
|
|
417
|
+
let position = 0;
|
|
418
|
+
while (position < remaining.length) {
|
|
419
|
+
// Try to match bold-italic (_*text*_)
|
|
420
|
+
const boldItalicMatch = remaining
|
|
421
|
+
.slice(position)
|
|
422
|
+
.match(/^_\*(.+?)\*_/);
|
|
423
|
+
if (boldItalicMatch) {
|
|
424
|
+
const content = boldItalicMatch[1];
|
|
425
|
+
nodes.push({
|
|
426
|
+
type: "bold",
|
|
427
|
+
children: [{ type: "italic", content }],
|
|
428
|
+
});
|
|
429
|
+
position += boldItalicMatch[0].length;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
// Try to match bold (*text*)
|
|
433
|
+
const boldMatch = remaining.slice(position).match(/^\*(.+?)\*/);
|
|
434
|
+
if (boldMatch) {
|
|
435
|
+
nodes.push({
|
|
436
|
+
type: "bold",
|
|
437
|
+
content: boldMatch[1],
|
|
438
|
+
});
|
|
439
|
+
position += boldMatch[0].length;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
// Try to match italic (_text_)
|
|
443
|
+
const italicMatch = remaining.slice(position).match(/^_(.+?)_/);
|
|
444
|
+
if (italicMatch) {
|
|
445
|
+
nodes.push({
|
|
446
|
+
type: "italic",
|
|
447
|
+
content: italicMatch[1],
|
|
448
|
+
});
|
|
449
|
+
position += italicMatch[0].length;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
// No emphasis found, consume until next emphasis marker or end of string
|
|
453
|
+
const nextAsterisk = remaining.slice(position).indexOf("*");
|
|
454
|
+
const nextUnderscore = remaining.slice(position).indexOf("_");
|
|
455
|
+
// Find the nearest marker
|
|
456
|
+
let nextMarker = -1;
|
|
457
|
+
if (nextAsterisk !== -1 && nextUnderscore !== -1) {
|
|
458
|
+
nextMarker = Math.min(nextAsterisk, nextUnderscore);
|
|
459
|
+
}
|
|
460
|
+
else if (nextAsterisk !== -1) {
|
|
461
|
+
nextMarker = nextAsterisk;
|
|
462
|
+
}
|
|
463
|
+
else if (nextUnderscore !== -1) {
|
|
464
|
+
nextMarker = nextUnderscore;
|
|
465
|
+
}
|
|
466
|
+
const textEnd = nextMarker === -1
|
|
467
|
+
? remaining.length
|
|
468
|
+
: position + nextMarker;
|
|
469
|
+
if (textEnd > position) {
|
|
470
|
+
nodes.push({
|
|
471
|
+
type: "text",
|
|
472
|
+
content: remaining.slice(position, textEnd),
|
|
473
|
+
});
|
|
474
|
+
position = textEnd;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
// Edge case: marker that doesn't form emphasis
|
|
478
|
+
nodes.push({
|
|
479
|
+
type: "text",
|
|
480
|
+
content: remaining.slice(position, position + 1),
|
|
481
|
+
});
|
|
482
|
+
position++;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return nodes;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Generates React component code from a Proto Markdown AST using Shadcn UI components
|
|
491
|
+
*/
|
|
492
|
+
class ShadcnCodeGenerator {
|
|
493
|
+
constructor() {
|
|
494
|
+
this.indentLevel = 0;
|
|
495
|
+
this.indentSize = 2;
|
|
496
|
+
this.requiredImports = new Set();
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Generate complete React component code from markdown AST
|
|
500
|
+
*/
|
|
501
|
+
generate(nodes) {
|
|
502
|
+
this.indentLevel = 0;
|
|
503
|
+
this.requiredImports.clear();
|
|
504
|
+
// Generate component body
|
|
505
|
+
const componentBody = this.generateNodes(nodes);
|
|
506
|
+
// Add base indentation (6 spaces = 3 levels for proper JSX nesting)
|
|
507
|
+
const indentedBody = componentBody
|
|
508
|
+
.split('\n')
|
|
509
|
+
.map(line => line ? ` ${line}` : line)
|
|
510
|
+
.join('\n');
|
|
511
|
+
// Collect imports
|
|
512
|
+
const imports = this.generateImports();
|
|
513
|
+
return `${imports}
|
|
514
|
+
|
|
515
|
+
export function GeneratedComponent() {
|
|
516
|
+
return (
|
|
517
|
+
<div className="space-y-2">
|
|
518
|
+
${indentedBody}
|
|
519
|
+
</div>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Generate imports based on used components
|
|
526
|
+
*/
|
|
527
|
+
generateImports() {
|
|
528
|
+
const imports = [];
|
|
529
|
+
if (this.requiredImports.has("Button")) {
|
|
530
|
+
imports.push('import { Button } from "@/components/ui/button";');
|
|
531
|
+
}
|
|
532
|
+
if (this.requiredImports.has("Input")) {
|
|
533
|
+
imports.push('import { Input } from "@/components/ui/input";');
|
|
534
|
+
}
|
|
535
|
+
if (this.requiredImports.has("Textarea")) {
|
|
536
|
+
imports.push('import { Textarea } from "@/components/ui/textarea";');
|
|
537
|
+
}
|
|
538
|
+
if (this.requiredImports.has("Card")) {
|
|
539
|
+
imports.push('import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";');
|
|
540
|
+
}
|
|
541
|
+
if (this.requiredImports.has("Checkbox")) {
|
|
542
|
+
imports.push('import { Checkbox } from "@/components/ui/checkbox";');
|
|
543
|
+
}
|
|
544
|
+
if (this.requiredImports.has("RadioGroup")) {
|
|
545
|
+
imports.push('import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";');
|
|
546
|
+
}
|
|
547
|
+
if (this.requiredImports.has("Select")) {
|
|
548
|
+
imports.push('import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";');
|
|
549
|
+
}
|
|
550
|
+
if (this.requiredImports.has("Table")) {
|
|
551
|
+
imports.push('import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table";');
|
|
552
|
+
}
|
|
553
|
+
if (this.requiredImports.has("Label")) {
|
|
554
|
+
imports.push('import { Label } from "@/components/ui/label";');
|
|
555
|
+
}
|
|
556
|
+
return imports.join("\n");
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Generate code for multiple nodes
|
|
560
|
+
*/
|
|
561
|
+
generateNodes(nodes) {
|
|
562
|
+
return nodes.map((node, index) => this.generateNode(node, index)).join("\n");
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Generate code for a single node
|
|
566
|
+
*/
|
|
567
|
+
generateNode(node, index) {
|
|
568
|
+
switch (node.type) {
|
|
569
|
+
case "header":
|
|
570
|
+
return this.generateHeader(node, index);
|
|
571
|
+
case "input":
|
|
572
|
+
return this.generateInput(node, index);
|
|
573
|
+
case "textarea":
|
|
574
|
+
return this.generateTextarea(node, index);
|
|
575
|
+
case "dropdown":
|
|
576
|
+
return this.generateDropdown(node, index);
|
|
577
|
+
case "checkbox":
|
|
578
|
+
return this.generateCheckbox(node, index);
|
|
579
|
+
case "radiogroup":
|
|
580
|
+
return this.generateRadioGroup(node, index);
|
|
581
|
+
case "button":
|
|
582
|
+
return this.generateButton(node, index);
|
|
583
|
+
case "container":
|
|
584
|
+
return this.generateContainer(node, index);
|
|
585
|
+
case "card":
|
|
586
|
+
return this.generateCard(node, index);
|
|
587
|
+
case "table":
|
|
588
|
+
return this.generateTable(node, index);
|
|
589
|
+
case "grid":
|
|
590
|
+
return this.generateGrid(node, index);
|
|
591
|
+
case "div":
|
|
592
|
+
return this.generateDiv(node, index);
|
|
593
|
+
case "text":
|
|
594
|
+
return this.generateText(node, index);
|
|
595
|
+
case "bold":
|
|
596
|
+
return this.generateBold(node, index);
|
|
597
|
+
case "italic":
|
|
598
|
+
return this.generateItalic(node, index);
|
|
599
|
+
case "image":
|
|
600
|
+
return this.generateImage(node, index);
|
|
601
|
+
default:
|
|
602
|
+
return "";
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
indent() {
|
|
606
|
+
return " ".repeat(this.indentLevel * this.indentSize);
|
|
607
|
+
}
|
|
608
|
+
escapeJSX(text) {
|
|
609
|
+
return text
|
|
610
|
+
.replace(/&/g, "&")
|
|
611
|
+
.replace(/</g, "<")
|
|
612
|
+
.replace(/>/g, ">")
|
|
613
|
+
.replace(/"/g, """)
|
|
614
|
+
.replace(/{/g, "{")
|
|
615
|
+
.replace(/}/g, "}");
|
|
616
|
+
}
|
|
617
|
+
generateHeader(node, index) {
|
|
618
|
+
const Tag = `h${node.level}`;
|
|
619
|
+
const className = `text-${node.level === 1 ? "4xl" : node.level === 2 ? "3xl" : node.level === 3 ? "2xl" : node.level === 4 ? "xl" : node.level === 5 ? "lg" : "base"} font-bold`;
|
|
620
|
+
let content;
|
|
621
|
+
if (node.children && node.children.length > 0) {
|
|
622
|
+
// Inline children (emphasis)
|
|
623
|
+
content = node.children.map((child, i) => this.generateInlineNode(child, i)).join("");
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
content = this.escapeJSX(node.content || "");
|
|
627
|
+
}
|
|
628
|
+
return `${this.indent()}<${Tag} key={${index}} className="${className}">${content}</${Tag}>`;
|
|
629
|
+
}
|
|
630
|
+
generateInlineNode(node, index) {
|
|
631
|
+
switch (node.type) {
|
|
632
|
+
case "bold":
|
|
633
|
+
const boldContent = node.children?.map((child, i) => this.generateInlineNode(child, i)).join("") || this.escapeJSX(node.content || "");
|
|
634
|
+
return `<strong key={${index}}>${boldContent}</strong>`;
|
|
635
|
+
case "italic":
|
|
636
|
+
const italicContent = node.children?.map((child, i) => this.generateInlineNode(child, i)).join("") || this.escapeJSX(node.content || "");
|
|
637
|
+
return `<em key={${index}}>${italicContent}</em>`;
|
|
638
|
+
case "text":
|
|
639
|
+
if (node.children && node.children.length > 0) {
|
|
640
|
+
return node.children.map((child, i) => this.generateInlineNode(child, i)).join("");
|
|
641
|
+
}
|
|
642
|
+
return this.escapeJSX(node.content || "");
|
|
643
|
+
default:
|
|
644
|
+
return this.escapeJSX(node.content || "");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
generateInput(node, index) {
|
|
648
|
+
this.requiredImports.add("Input");
|
|
649
|
+
this.requiredImports.add("Label");
|
|
650
|
+
const id = node.id || `input-${index}`;
|
|
651
|
+
const type = node.inputType || "text";
|
|
652
|
+
return `${this.indent()}<div key={${index}} className="space-y-2">
|
|
653
|
+
${this.indent()} <Label htmlFor="${id}">${this.escapeJSX(node.label || "")}</Label>
|
|
654
|
+
${this.indent()} <Input id="${id}" type="${type}" />
|
|
655
|
+
${this.indent()}</div>`;
|
|
656
|
+
}
|
|
657
|
+
generateTextarea(node, index) {
|
|
658
|
+
this.requiredImports.add("Textarea");
|
|
659
|
+
this.requiredImports.add("Label");
|
|
660
|
+
const id = node.id || `textarea-${index}`;
|
|
661
|
+
return `${this.indent()}<div key={${index}} className="space-y-2">
|
|
662
|
+
${this.indent()} <Label htmlFor="${id}">${this.escapeJSX(node.label || "")}</Label>
|
|
663
|
+
${this.indent()} <Textarea id="${id}" />
|
|
664
|
+
${this.indent()}</div>`;
|
|
665
|
+
}
|
|
666
|
+
generateDropdown(node, index) {
|
|
667
|
+
this.requiredImports.add("Select");
|
|
668
|
+
this.requiredImports.add("Label");
|
|
669
|
+
const id = node.id || `select-${index}`;
|
|
670
|
+
const options = node.options || [];
|
|
671
|
+
return `${this.indent()}<div key={${index}} className="space-y-2">
|
|
672
|
+
${this.indent()} <Label htmlFor="${id}">${this.escapeJSX(node.label || "")}</Label>
|
|
673
|
+
${this.indent()} <Select>
|
|
674
|
+
${this.indent()} <SelectTrigger id="${id}">
|
|
675
|
+
${this.indent()} <SelectValue placeholder="Select an option" />
|
|
676
|
+
${this.indent()} </SelectTrigger>
|
|
677
|
+
${this.indent()} <SelectContent>
|
|
678
|
+
${options.map((opt, i) => `${this.indent()} <SelectItem key={${i}} value="${opt.toLowerCase().replace(/\s+/g, "-")}">${this.escapeJSX(opt)}</SelectItem>`).join("\n")}
|
|
679
|
+
${this.indent()} </SelectContent>
|
|
680
|
+
${this.indent()} </Select>
|
|
681
|
+
${this.indent()}</div>`;
|
|
682
|
+
}
|
|
683
|
+
generateCheckbox(node, index) {
|
|
684
|
+
this.requiredImports.add("Checkbox");
|
|
685
|
+
this.requiredImports.add("Label");
|
|
686
|
+
const id = node.id || `checkbox-${index}`;
|
|
687
|
+
return `${this.indent()}<div key={${index}} className="flex items-center space-x-2">
|
|
688
|
+
${this.indent()} <Checkbox id="${id}" />
|
|
689
|
+
${this.indent()} <Label htmlFor="${id}" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
690
|
+
${this.indent()} ${this.escapeJSX(node.label || "")}
|
|
691
|
+
${this.indent()} </Label>
|
|
692
|
+
${this.indent()}</div>`;
|
|
693
|
+
}
|
|
694
|
+
generateRadioGroup(node, index) {
|
|
695
|
+
this.requiredImports.add("RadioGroup");
|
|
696
|
+
this.requiredImports.add("Label");
|
|
697
|
+
const options = node.options || [];
|
|
698
|
+
return `${this.indent()}<div key={${index}} className="space-y-2">
|
|
699
|
+
${this.indent()} <Label>${this.escapeJSX(node.label || "")}</Label>
|
|
700
|
+
${this.indent()} <RadioGroup>
|
|
701
|
+
${options.map((opt, i) => {
|
|
702
|
+
const optId = `radio-${index}-${i}`;
|
|
703
|
+
const value = opt.toLowerCase().replace(/\s+/g, "-");
|
|
704
|
+
return `${this.indent()} <div className="flex items-center space-x-2">
|
|
705
|
+
${this.indent()} <RadioGroupItem id="${optId}" value="${value}" />
|
|
706
|
+
${this.indent()} <Label htmlFor="${optId}">${this.escapeJSX(opt)}</Label>
|
|
707
|
+
${this.indent()} </div>`;
|
|
708
|
+
}).join("\n")}
|
|
709
|
+
${this.indent()} </RadioGroup>
|
|
710
|
+
${this.indent()}</div>`;
|
|
711
|
+
}
|
|
712
|
+
generateButton(node, index) {
|
|
713
|
+
this.requiredImports.add("Button");
|
|
714
|
+
const variant = node.variant || "default";
|
|
715
|
+
const className = node.className ? ` className="${node.className}"` : "";
|
|
716
|
+
return `${this.indent()}<Button key={${index}} variant="${variant}"${className}>${this.escapeJSX(node.content || "")}</Button>`;
|
|
717
|
+
}
|
|
718
|
+
generateContainer(node, index) {
|
|
719
|
+
this.indentLevel++;
|
|
720
|
+
const children = node.children ? this.generateNodes(node.children) : "";
|
|
721
|
+
this.indentLevel--;
|
|
722
|
+
return `${this.indent()}<div key={${index}} className="flex gap-2">
|
|
723
|
+
${children}
|
|
724
|
+
${this.indent()}</div>`;
|
|
725
|
+
}
|
|
726
|
+
generateCard(node, index) {
|
|
727
|
+
this.requiredImports.add("Card");
|
|
728
|
+
let titleContent = "";
|
|
729
|
+
if (node.titleChildren && node.titleChildren.length > 0) {
|
|
730
|
+
titleContent = node.titleChildren.map((child, i) => this.generateInlineNode(child, i)).join("");
|
|
731
|
+
}
|
|
732
|
+
else if (node.title) {
|
|
733
|
+
titleContent = this.escapeJSX(node.title);
|
|
734
|
+
}
|
|
735
|
+
// Increment by 2 to account for Card wrapper + CardContent nesting
|
|
736
|
+
this.indentLevel += 2;
|
|
737
|
+
const children = node.children ? this.generateNodes(node.children) : "";
|
|
738
|
+
this.indentLevel -= 2;
|
|
739
|
+
const cardContent = titleContent
|
|
740
|
+
? `${this.indent()}<Card key={${index}}>
|
|
741
|
+
${this.indent()} <CardHeader>
|
|
742
|
+
${this.indent()} <CardTitle>${titleContent}</CardTitle>
|
|
743
|
+
${this.indent()} </CardHeader>
|
|
744
|
+
${this.indent()} <CardContent className="space-y-2">
|
|
745
|
+
${children}
|
|
746
|
+
${this.indent()} </CardContent>
|
|
747
|
+
${this.indent()}</Card>`
|
|
748
|
+
: `${this.indent()}<Card key={${index}}>
|
|
749
|
+
${this.indent()} <CardContent className="pt-6 space-y-2">
|
|
750
|
+
${children}
|
|
751
|
+
${this.indent()} </CardContent>
|
|
752
|
+
${this.indent()}</Card>`;
|
|
753
|
+
return cardContent;
|
|
754
|
+
}
|
|
755
|
+
generateTable(node, index) {
|
|
756
|
+
this.requiredImports.add("Table");
|
|
757
|
+
const headers = node.headers || [];
|
|
758
|
+
const rows = node.rows || [];
|
|
759
|
+
return `${this.indent()}<Table key={${index}}>
|
|
760
|
+
${this.indent()} <TableHeader>
|
|
761
|
+
${this.indent()} <TableRow>
|
|
762
|
+
${headers.map((header, i) => `${this.indent()} <TableHead key={${i}}>${this.escapeJSX(header)}</TableHead>`).join("\n")}
|
|
763
|
+
${this.indent()} </TableRow>
|
|
764
|
+
${this.indent()} </TableHeader>
|
|
765
|
+
${this.indent()} <TableBody>
|
|
766
|
+
${rows.map((row, i) => `${this.indent()} <TableRow key={${i}}>
|
|
767
|
+
${row.map((cell, j) => `${this.indent()} <TableCell key={${j}}>${this.escapeJSX(cell)}</TableCell>`).join("\n")}
|
|
768
|
+
${this.indent()} </TableRow>`).join("\n")}
|
|
769
|
+
${this.indent()} </TableBody>
|
|
770
|
+
${this.indent()}</Table>`;
|
|
771
|
+
}
|
|
772
|
+
generateGrid(node, index) {
|
|
773
|
+
const gridClasses = `grid ${node.gridConfig || ""}`;
|
|
774
|
+
this.indentLevel++;
|
|
775
|
+
const children = node.children ? this.generateNodes(node.children) : "";
|
|
776
|
+
this.indentLevel--;
|
|
777
|
+
return `${this.indent()}<div key={${index}} className="${gridClasses}">
|
|
778
|
+
${children}
|
|
779
|
+
${this.indent()}</div>`;
|
|
780
|
+
}
|
|
781
|
+
generateDiv(node, index) {
|
|
782
|
+
const className = node.className || "";
|
|
783
|
+
this.indentLevel++;
|
|
784
|
+
const children = node.children ? this.generateNodes(node.children) : "";
|
|
785
|
+
this.indentLevel--;
|
|
786
|
+
return `${this.indent()}<div key={${index}} className="${className}">
|
|
787
|
+
${children}
|
|
788
|
+
${this.indent()}</div>`;
|
|
789
|
+
}
|
|
790
|
+
generateText(node, index) {
|
|
791
|
+
if (node.children && node.children.length > 0) {
|
|
792
|
+
// Text with inline emphasis
|
|
793
|
+
const inlineContent = node.children.map((child, i) => this.generateInlineNode(child, i)).join("");
|
|
794
|
+
return `${this.indent()}<p key={${index}}>${inlineContent}</p>`;
|
|
795
|
+
}
|
|
796
|
+
return `${this.indent()}<p key={${index}}>${this.escapeJSX(node.content || "")}</p>`;
|
|
797
|
+
}
|
|
798
|
+
generateBold(node, index) {
|
|
799
|
+
if (node.children && node.children.length > 0) {
|
|
800
|
+
const content = node.children.map((child, i) => this.generateInlineNode(child, i)).join("");
|
|
801
|
+
return `${this.indent()}<strong key={${index}}>${content}</strong>`;
|
|
802
|
+
}
|
|
803
|
+
return `${this.indent()}<strong key={${index}}>${this.escapeJSX(node.content || "")}</strong>`;
|
|
804
|
+
}
|
|
805
|
+
generateItalic(node, index) {
|
|
806
|
+
if (node.children && node.children.length > 0) {
|
|
807
|
+
const content = node.children.map((child, i) => this.generateInlineNode(child, i)).join("");
|
|
808
|
+
return `${this.indent()}<em key={${index}}>${content}</em>`;
|
|
809
|
+
}
|
|
810
|
+
return `${this.indent()}<em key={${index}}>${this.escapeJSX(node.content || "")}</em>`;
|
|
811
|
+
}
|
|
812
|
+
generateImage(node, index) {
|
|
813
|
+
const src = node.src || "";
|
|
814
|
+
const alt = node.alt || "";
|
|
815
|
+
return `${this.indent()}<img key={${index}} src="${src}" alt="${this.escapeJSX(alt)}" className="max-w-full h-auto" />`;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export { MarkdownParser, ShadcnCodeGenerator };
|
|
820
|
+
//# sourceMappingURL=index.esm.js.map
|