@refrakt-md/editor 0.7.2 → 0.8.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/app/dist/assets/{index-4SP4_AaD.js → index-BBinZAiy.js} +1 -1
- package/app/dist/assets/index-BD2EBUrQ.css +1 -0
- package/app/dist/assets/{index-D77rckeh.js → index-BLuaHLN3.js} +1 -1
- package/app/dist/assets/{index-30gAspk8.js → index-BgCNqcSo.js} +1 -1
- package/app/dist/assets/index-BlAOhWAQ.js +453 -0
- package/app/dist/assets/{index-BZ4adnS0.js → index-BwFn9q4x.js} +1 -1
- package/app/dist/assets/{index-DFkteo0w.js → index-C72UC2ga.js} +1 -1
- package/app/dist/assets/{index-x67KGOIr.js → index-COIPZ34u.js} +1 -1
- package/app/dist/assets/{index-BEFUVB_B.js → index-CW02bulk.js} +1 -1
- package/app/dist/assets/{index-CI5PewQM.js → index-CXFMPmtf.js} +1 -1
- package/app/dist/assets/{index-ByHhigzw.js → index-CeU_s7BB.js} +1 -1
- package/app/dist/assets/{index-DvgOtlru.js → index-CqHjo2YT.js} +1 -1
- package/app/dist/assets/{index-DKnhR16N.js → index-D3TQo8gu.js} +1 -1
- package/app/dist/assets/{index-Baf7ZSct.js → index-DVM3uoxc.js} +1 -1
- package/app/dist/assets/{index-C9w1RpYY.js → index-DW2zI-Ss.js} +1 -1
- package/app/dist/assets/{index--rGC9bba.js → index-D_Y6J00B.js} +1 -1
- package/app/dist/assets/{index-kPhFxtn-.js → index-DgIg-QAA.js} +2 -2
- package/app/dist/assets/{index-DIuFNfTc.js → index-DmY6uqAw.js} +1 -1
- package/app/dist/assets/{index-D1WOi3EN.js → index-DzHt8ZRh.js} +1 -1
- package/app/dist/assets/{index-BwWzfQVn.js → index-ZLvRNfLb.js} +1 -1
- package/app/dist/index.html +2 -2
- package/app/src/lib/api/client.ts +49 -0
- package/app/src/lib/components/ActionEditPopover.svelte +245 -0
- package/app/src/lib/components/BlockCard.svelte +255 -1
- package/app/src/lib/components/BlockEditPanel.svelte +697 -138
- package/app/src/lib/components/BlockEditor.svelte +467 -389
- package/app/src/lib/components/CodeEditPopover.svelte +226 -0
- package/app/src/lib/components/ContentModelTree.svelte +562 -0
- package/app/src/lib/components/ContentTree.svelte +181 -0
- package/app/src/lib/components/EditorLayout.svelte +1 -6
- package/app/src/lib/components/FrontmatterEditPanel.svelte +0 -1
- package/app/src/lib/components/HeaderBar.svelte +38 -0
- package/app/src/lib/components/InlineEditPopover.svelte +593 -0
- package/app/src/lib/components/InsertBlockDialog.svelte +429 -0
- package/app/src/lib/components/PageCard.svelte +3 -4
- package/app/src/lib/components/PreviewPane.svelte +19 -1
- package/app/src/lib/components/RuneAttributes.svelte +249 -100
- package/app/src/lib/editor/block-parser.ts +463 -0
- package/app/src/lib/preview/block-renderer.ts +30 -14
- package/dist/community-tags-builder.d.ts.map +1 -1
- package/dist/community-tags-builder.js +5 -1
- package/dist/community-tags-builder.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +92 -6
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/preview-runtime/App.svelte +2 -0
- package/app/dist/assets/index-DlrXwdpb.css +0 -1
- package/app/dist/assets/index-GlUHQ_jL.js +0 -324
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RuneInfo } from '../api/client.js';
|
|
2
|
+
import type { ResolvedStructure, ResolvedField } from './content-model-resolver.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Block types recognized by the visual editor.
|
|
@@ -105,12 +106,19 @@ function parseAttributes(raw: string): Record<string, string> {
|
|
|
105
106
|
return attrs;
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/** Check if a string value represents a number */
|
|
110
|
+
function isNumericValue(value: string): boolean {
|
|
111
|
+
return value !== '' && !isNaN(Number(value));
|
|
112
|
+
}
|
|
113
|
+
|
|
108
114
|
/** Serialize attributes back to Markdoc syntax */
|
|
109
115
|
export function serializeAttributes(attrs: Record<string, string>): string {
|
|
110
116
|
const parts: string[] = [];
|
|
111
117
|
for (const [key, value] of Object.entries(attrs)) {
|
|
112
118
|
if (value === 'true') {
|
|
113
119
|
parts.push(key);
|
|
120
|
+
} else if (value === 'false' || isNumericValue(value)) {
|
|
121
|
+
parts.push(`${key}=${value}`);
|
|
114
122
|
} else {
|
|
115
123
|
parts.push(`${key}="${value}"`);
|
|
116
124
|
}
|
|
@@ -348,6 +356,461 @@ function isBlockStart(line: string): boolean {
|
|
|
348
356
|
return false;
|
|
349
357
|
}
|
|
350
358
|
|
|
359
|
+
// ── Content tree parser (for nested rune editing) ────────────────────
|
|
360
|
+
|
|
361
|
+
export interface ContentNode {
|
|
362
|
+
type: 'rune' | 'heading' | 'paragraph' | 'fence' | 'list' | 'quote' | 'hr' | 'image';
|
|
363
|
+
label: string;
|
|
364
|
+
source: string;
|
|
365
|
+
// Rune-specific
|
|
366
|
+
runeName?: string;
|
|
367
|
+
selfClosing?: boolean;
|
|
368
|
+
attributes?: Record<string, string>;
|
|
369
|
+
innerContent?: string;
|
|
370
|
+
children?: ContentNode[];
|
|
371
|
+
// Type-specific (populated during parsing)
|
|
372
|
+
headingLevel?: number;
|
|
373
|
+
headingText?: string;
|
|
374
|
+
fenceLanguage?: string;
|
|
375
|
+
fenceCode?: string;
|
|
376
|
+
listOrdered?: boolean;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Parse a content string into a tree of ContentNodes.
|
|
381
|
+
* Similar to parseBlocks() but builds a tree: rune nodes recursively
|
|
382
|
+
* parse their inner content into children.
|
|
383
|
+
*/
|
|
384
|
+
export function parseContentTree(content: string): ContentNode[] {
|
|
385
|
+
if (!content.trim()) return [];
|
|
386
|
+
|
|
387
|
+
const lines = content.split('\n');
|
|
388
|
+
const nodes: ContentNode[] = [];
|
|
389
|
+
let i = 0;
|
|
390
|
+
|
|
391
|
+
while (i < lines.length) {
|
|
392
|
+
const line = lines[i];
|
|
393
|
+
const trimmed = line.trimStart();
|
|
394
|
+
|
|
395
|
+
if (trimmed === '') { i++; continue; }
|
|
396
|
+
|
|
397
|
+
// Fenced code
|
|
398
|
+
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
399
|
+
const fence = trimmed.slice(0, 3);
|
|
400
|
+
const lang = trimmed.slice(3).trim();
|
|
401
|
+
const start = i;
|
|
402
|
+
i++;
|
|
403
|
+
const codeStart = i;
|
|
404
|
+
while (i < lines.length && !lines[i].trimStart().startsWith(fence)) i++;
|
|
405
|
+
const code = lines.slice(codeStart, i).join('\n');
|
|
406
|
+
if (i < lines.length) i++;
|
|
407
|
+
nodes.push({
|
|
408
|
+
type: 'fence',
|
|
409
|
+
label: lang ? `Code (${lang})` : 'Code',
|
|
410
|
+
source: lines.slice(start, i).join('\n'),
|
|
411
|
+
fenceLanguage: lang,
|
|
412
|
+
fenceCode: code,
|
|
413
|
+
});
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Rune tags
|
|
418
|
+
const runeMatch = RUNE_OPEN_RE.exec(trimmed);
|
|
419
|
+
if (runeMatch) {
|
|
420
|
+
const name = runeMatch[1];
|
|
421
|
+
const attrStr = runeMatch[2] ?? '';
|
|
422
|
+
const selfClose = runeMatch[3] === '/';
|
|
423
|
+
const start = i;
|
|
424
|
+
|
|
425
|
+
if (selfClose) {
|
|
426
|
+
i++;
|
|
427
|
+
nodes.push({
|
|
428
|
+
type: 'rune',
|
|
429
|
+
label: name,
|
|
430
|
+
source: lines.slice(start, i).join('\n'),
|
|
431
|
+
runeName: name,
|
|
432
|
+
selfClosing: true,
|
|
433
|
+
attributes: parseAttributes(attrStr),
|
|
434
|
+
innerContent: '',
|
|
435
|
+
children: [],
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
i++;
|
|
439
|
+
const closeRe = runeCloseRe(name);
|
|
440
|
+
const innerLines: string[] = [];
|
|
441
|
+
let depth = 1;
|
|
442
|
+
while (i < lines.length) {
|
|
443
|
+
const lt = lines[i].trimStart();
|
|
444
|
+
const nestedOpen = RUNE_OPEN_RE.exec(lt);
|
|
445
|
+
if (nestedOpen && nestedOpen[1] === name && nestedOpen[3] !== '/') depth++;
|
|
446
|
+
if (closeRe.test(lt)) { depth--; if (depth === 0) break; }
|
|
447
|
+
innerLines.push(lines[i]);
|
|
448
|
+
i++;
|
|
449
|
+
}
|
|
450
|
+
if (i < lines.length) i++;
|
|
451
|
+
const inner = innerLines.join('\n');
|
|
452
|
+
nodes.push({
|
|
453
|
+
type: 'rune',
|
|
454
|
+
label: name,
|
|
455
|
+
source: lines.slice(start, i).join('\n'),
|
|
456
|
+
runeName: name,
|
|
457
|
+
selfClosing: false,
|
|
458
|
+
attributes: parseAttributes(attrStr),
|
|
459
|
+
innerContent: inner,
|
|
460
|
+
children: parseContentTree(inner),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Heading
|
|
467
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)/);
|
|
468
|
+
if (headingMatch) {
|
|
469
|
+
const level = headingMatch[1].length;
|
|
470
|
+
const text = headingMatch[2];
|
|
471
|
+
i++;
|
|
472
|
+
nodes.push({
|
|
473
|
+
type: 'heading',
|
|
474
|
+
label: `${'#'.repeat(level)} ${text}`,
|
|
475
|
+
source: lines.slice(i - 1, i).join('\n'),
|
|
476
|
+
headingLevel: level,
|
|
477
|
+
headingText: text,
|
|
478
|
+
});
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// HR
|
|
483
|
+
if (/^(---+|___+|\*\*\*+)\s*$/.test(trimmed)) {
|
|
484
|
+
i++;
|
|
485
|
+
nodes.push({ type: 'hr', label: 'Divider', source: lines.slice(i - 1, i).join('\n') });
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Image
|
|
490
|
+
if (/^!\[.*\]\(.*\)\s*$/.test(trimmed)) {
|
|
491
|
+
i++;
|
|
492
|
+
nodes.push({ type: 'image', label: 'Image', source: lines.slice(i - 1, i).join('\n') });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Blockquote
|
|
497
|
+
if (trimmed.startsWith('>')) {
|
|
498
|
+
const start = i;
|
|
499
|
+
while (i < lines.length && (lines[i].trimStart().startsWith('>') || (lines[i].trim() !== '' && !isBlockStart(lines[i])))) i++;
|
|
500
|
+
const src = lines.slice(start, i).join('\n');
|
|
501
|
+
nodes.push({ type: 'quote', label: 'Blockquote', source: src });
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// List
|
|
506
|
+
if (/^(\d+\.|[-*+])\s/.test(trimmed)) {
|
|
507
|
+
const start = i;
|
|
508
|
+
while (i < lines.length) {
|
|
509
|
+
const lt = lines[i].trimStart();
|
|
510
|
+
if (/^(\d+\.|[-*+])\s/.test(lt) || (lt === '' && i + 1 < lines.length && /^(\s+|\d+\.|[-*+]\s)/.test(lines[i + 1])) || (lt !== '' && lines[i].startsWith(' '))) {
|
|
511
|
+
i++;
|
|
512
|
+
} else { break; }
|
|
513
|
+
}
|
|
514
|
+
const ordered = /^\d+\./.test(trimmed);
|
|
515
|
+
nodes.push({ type: 'list', label: ordered ? 'Ordered list' : 'List', source: lines.slice(start, i).join('\n'), listOrdered: ordered });
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Paragraph
|
|
520
|
+
{
|
|
521
|
+
const start = i;
|
|
522
|
+
while (i < lines.length && lines[i].trim() !== '' && !isBlockStart(lines[i])) i++;
|
|
523
|
+
const src = lines.slice(start, i).join('\n');
|
|
524
|
+
const preview = src.length > 40 ? src.slice(0, 40) + '…' : src;
|
|
525
|
+
nodes.push({ type: 'paragraph', label: preview, source: src });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return nodes;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Replace a nested rune node's source within a parent content string.
|
|
534
|
+
* Finds the node's original source and replaces it with the new source.
|
|
535
|
+
*/
|
|
536
|
+
export function replaceNodeSource(parentContent: string, oldSource: string, newSource: string): string {
|
|
537
|
+
const idx = parentContent.indexOf(oldSource);
|
|
538
|
+
if (idx === -1) return parentContent;
|
|
539
|
+
return parentContent.slice(0, idx) + newSource + parentContent.slice(idx + oldSource.length);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ── Content model field insertion / removal ──────────────────────────
|
|
543
|
+
|
|
544
|
+
/** Default markdown template for a field's match type */
|
|
545
|
+
function defaultTemplate(match: string): string {
|
|
546
|
+
const primary = match.includes('|') ? match.split('|')[0] : match;
|
|
547
|
+
switch (primary) {
|
|
548
|
+
case 'heading': return '## Heading';
|
|
549
|
+
case 'heading:1': return '# Heading';
|
|
550
|
+
case 'heading:2': return '## Heading';
|
|
551
|
+
case 'heading:3': return '### Heading';
|
|
552
|
+
case 'heading:4': return '#### Heading';
|
|
553
|
+
case 'heading:5': return '##### Heading';
|
|
554
|
+
case 'heading:6': return '###### Heading';
|
|
555
|
+
case 'paragraph': return 'Text content';
|
|
556
|
+
case 'list': case 'list:unordered': return '- Item 1\n- Item 2';
|
|
557
|
+
case 'list:ordered': return '1. Item 1\n2. Item 2';
|
|
558
|
+
case 'fence': return '```\ncode\n```';
|
|
559
|
+
case 'image': return '';
|
|
560
|
+
case 'blockquote': case 'quote': return '> Quote';
|
|
561
|
+
case 'any': return 'Content';
|
|
562
|
+
default: return 'Content';
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Insert content for an empty field in a resolved structure.
|
|
568
|
+
* Returns the updated innerContent string.
|
|
569
|
+
*/
|
|
570
|
+
export function insertFieldContent(
|
|
571
|
+
innerContent: string,
|
|
572
|
+
structure: ResolvedStructure,
|
|
573
|
+
fieldName: string,
|
|
574
|
+
zoneName?: string,
|
|
575
|
+
): string {
|
|
576
|
+
const { field, fields } = findField(structure, fieldName, zoneName);
|
|
577
|
+
if (!field || field.filled) return innerContent;
|
|
578
|
+
|
|
579
|
+
const template = field.template || defaultTemplate(field.match);
|
|
580
|
+
|
|
581
|
+
if (structure.type === 'delimited' && zoneName) {
|
|
582
|
+
return insertInDelimited(innerContent, structure, zoneName, fieldName, fields!, template);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (structure.type === 'sequence') {
|
|
586
|
+
return insertInSequence(innerContent, structure.fields, fieldName, template);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return innerContent;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Remove content for a filled field in a resolved structure.
|
|
594
|
+
* Returns the updated innerContent string.
|
|
595
|
+
*/
|
|
596
|
+
export function removeFieldContent(
|
|
597
|
+
innerContent: string,
|
|
598
|
+
structure: ResolvedStructure,
|
|
599
|
+
fieldName: string,
|
|
600
|
+
zoneName?: string,
|
|
601
|
+
): string {
|
|
602
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
603
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
604
|
+
|
|
605
|
+
let result = innerContent;
|
|
606
|
+
// Remove all matched nodes' source text (reverse order to preserve indices)
|
|
607
|
+
for (let i = field.nodes.length - 1; i >= 0; i--) {
|
|
608
|
+
const nodeSource = field.nodes[i].source;
|
|
609
|
+
result = removeNodeSource(result, nodeSource);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Clean up double blank lines left by removal
|
|
613
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
614
|
+
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Append a new item to a filled list field.
|
|
620
|
+
* Returns the updated innerContent string.
|
|
621
|
+
*/
|
|
622
|
+
export function appendListItem(
|
|
623
|
+
innerContent: string,
|
|
624
|
+
structure: ResolvedStructure,
|
|
625
|
+
fieldName: string,
|
|
626
|
+
zoneName?: string,
|
|
627
|
+
): string {
|
|
628
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
629
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
630
|
+
|
|
631
|
+
const template = field.template || '- New item';
|
|
632
|
+
const lastNode = field.nodes[field.nodes.length - 1];
|
|
633
|
+
const idx = innerContent.indexOf(lastNode.source);
|
|
634
|
+
if (idx === -1) return innerContent;
|
|
635
|
+
|
|
636
|
+
const afterEnd = idx + lastNode.source.length;
|
|
637
|
+
return innerContent.slice(0, afterEnd) + '\n' + template + innerContent.slice(afterEnd);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Remove a single item from a list field by index.
|
|
642
|
+
* If the last item is removed, removes the entire list node.
|
|
643
|
+
*/
|
|
644
|
+
export function removeListItem(
|
|
645
|
+
innerContent: string,
|
|
646
|
+
structure: ResolvedStructure,
|
|
647
|
+
fieldName: string,
|
|
648
|
+
itemIndex: number,
|
|
649
|
+
zoneName?: string,
|
|
650
|
+
): string {
|
|
651
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
652
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
653
|
+
|
|
654
|
+
// Get the list source from the first list node
|
|
655
|
+
const listNode = field.nodes[0];
|
|
656
|
+
const items = splitListItems(listNode.source);
|
|
657
|
+
if (itemIndex < 0 || itemIndex >= items.length) return innerContent;
|
|
658
|
+
|
|
659
|
+
if (items.length === 1) {
|
|
660
|
+
// Removing the last item — remove the entire list node
|
|
661
|
+
return removeNodeSource(innerContent, listNode.source);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Remove the item and rejoin
|
|
665
|
+
const remaining = items.filter((_, i) => i !== itemIndex);
|
|
666
|
+
const newListSource = remaining.join('\n\n');
|
|
667
|
+
const idx = innerContent.indexOf(listNode.source);
|
|
668
|
+
if (idx === -1) return innerContent;
|
|
669
|
+
return innerContent.slice(0, idx) + newListSource + innerContent.slice(idx + listNode.source.length);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Split a list source into individual item sources.
|
|
674
|
+
* Each item starts with a list marker (-, *, +, or 1.) and may have
|
|
675
|
+
* indented continuation lines or blank-line separated paragraphs.
|
|
676
|
+
*/
|
|
677
|
+
export function splitListItems(listSource: string): string[] {
|
|
678
|
+
const lines = listSource.split('\n');
|
|
679
|
+
const items: string[] = [];
|
|
680
|
+
let current: string[] = [];
|
|
681
|
+
|
|
682
|
+
for (const line of lines) {
|
|
683
|
+
if (/^[-*+]\s|^\d+\.\s/.test(line) && current.length > 0) {
|
|
684
|
+
items.push(current.join('\n'));
|
|
685
|
+
current = [line];
|
|
686
|
+
} else {
|
|
687
|
+
current.push(line);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (current.length > 0) items.push(current.join('\n'));
|
|
691
|
+
return items;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function findField(
|
|
695
|
+
structure: ResolvedStructure,
|
|
696
|
+
fieldName: string,
|
|
697
|
+
zoneName?: string,
|
|
698
|
+
): { field: ResolvedField | null; fields: ResolvedField[] | null } {
|
|
699
|
+
if (structure.type === 'sequence') {
|
|
700
|
+
const field = structure.fields.find(f => f.name === fieldName) ?? null;
|
|
701
|
+
return { field, fields: structure.fields };
|
|
702
|
+
}
|
|
703
|
+
if (structure.type === 'delimited' && zoneName) {
|
|
704
|
+
const zone = structure.zones.find(z => z.name === zoneName);
|
|
705
|
+
if (!zone) return { field: null, fields: null };
|
|
706
|
+
const field = zone.fields.find(f => f.name === fieldName) ?? null;
|
|
707
|
+
return { field, fields: zone.fields };
|
|
708
|
+
}
|
|
709
|
+
return { field: null, fields: null };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Remove a node's source from content, handling surrounding whitespace */
|
|
713
|
+
function removeNodeSource(content: string, nodeSource: string): string {
|
|
714
|
+
const idx = content.indexOf(nodeSource);
|
|
715
|
+
if (idx === -1) return content;
|
|
716
|
+
|
|
717
|
+
let start = idx;
|
|
718
|
+
let end = idx + nodeSource.length;
|
|
719
|
+
|
|
720
|
+
// Extend to consume surrounding blank lines
|
|
721
|
+
while (start > 0 && content[start - 1] === '\n') start--;
|
|
722
|
+
if (start > 0) start++; // Keep one newline
|
|
723
|
+
while (end < content.length && content[end] === '\n') end++;
|
|
724
|
+
|
|
725
|
+
return content.slice(0, start) + content.slice(end);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function insertInSequence(
|
|
729
|
+
innerContent: string,
|
|
730
|
+
fields: ResolvedField[],
|
|
731
|
+
fieldName: string,
|
|
732
|
+
template: string,
|
|
733
|
+
): string {
|
|
734
|
+
const fieldIndex = fields.findIndex(f => f.name === fieldName);
|
|
735
|
+
if (fieldIndex === -1) return innerContent;
|
|
736
|
+
|
|
737
|
+
// Find the last filled field before this one to insert after
|
|
738
|
+
let insertAfterNode: ContentNode | null = null;
|
|
739
|
+
for (let i = fieldIndex - 1; i >= 0; i--) {
|
|
740
|
+
if (fields[i].filled && fields[i].nodes.length > 0) {
|
|
741
|
+
const nodes = fields[i].nodes;
|
|
742
|
+
insertAfterNode = nodes[nodes.length - 1];
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (insertAfterNode) {
|
|
748
|
+
const idx = innerContent.indexOf(insertAfterNode.source);
|
|
749
|
+
if (idx !== -1) {
|
|
750
|
+
const afterEnd = idx + insertAfterNode.source.length;
|
|
751
|
+
return innerContent.slice(0, afterEnd) + '\n\n' + template + innerContent.slice(afterEnd);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Find the first filled field after this one to insert before
|
|
756
|
+
for (let i = fieldIndex + 1; i < fields.length; i++) {
|
|
757
|
+
if (fields[i].filled && fields[i].nodes.length > 0) {
|
|
758
|
+
const beforeNode = fields[i].nodes[0];
|
|
759
|
+
const idx = innerContent.indexOf(beforeNode.source);
|
|
760
|
+
if (idx !== -1) {
|
|
761
|
+
return innerContent.slice(0, idx) + template + '\n\n' + innerContent.slice(idx);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// No adjacent filled fields — append to content
|
|
767
|
+
const trimmed = innerContent.trimEnd();
|
|
768
|
+
return trimmed + (trimmed ? '\n\n' : '') + template;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function insertInDelimited(
|
|
772
|
+
innerContent: string,
|
|
773
|
+
structure: ResolvedStructure & { type: 'delimited' },
|
|
774
|
+
zoneName: string,
|
|
775
|
+
fieldName: string,
|
|
776
|
+
fields: ResolvedField[],
|
|
777
|
+
template: string,
|
|
778
|
+
): string {
|
|
779
|
+
const zoneIndex = structure.zones.findIndex(z => z.name === zoneName);
|
|
780
|
+
if (zoneIndex === -1) return innerContent;
|
|
781
|
+
|
|
782
|
+
// Split content at delimiters (---) to find zone boundaries
|
|
783
|
+
const lines = innerContent.split('\n');
|
|
784
|
+
const delimiterIndices: number[] = [];
|
|
785
|
+
for (let i = 0; i < lines.length; i++) {
|
|
786
|
+
if (lines[i].trim() === '---') delimiterIndices.push(i);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// If the target zone doesn't exist yet (no delimiter), add one
|
|
790
|
+
if (zoneIndex > 0 && delimiterIndices.length < zoneIndex) {
|
|
791
|
+
const trimmed = innerContent.trimEnd();
|
|
792
|
+
const missingDelimiters = zoneIndex - delimiterIndices.length;
|
|
793
|
+
let insert = '';
|
|
794
|
+
for (let i = 0; i < missingDelimiters; i++) {
|
|
795
|
+
insert += '\n\n---';
|
|
796
|
+
}
|
|
797
|
+
return trimmed + insert + '\n\n' + template;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Zone exists — insert within it using sequence logic
|
|
801
|
+
// Determine the zone's content range in lines
|
|
802
|
+
const zoneStart = zoneIndex === 0 ? 0 : delimiterIndices[zoneIndex - 1] + 1;
|
|
803
|
+
const zoneEnd = zoneIndex < delimiterIndices.length ? delimiterIndices[zoneIndex] : lines.length;
|
|
804
|
+
const zoneContent = lines.slice(zoneStart, zoneEnd).join('\n');
|
|
805
|
+
|
|
806
|
+
const updatedZone = insertInSequence(zoneContent, fields, fieldName, template);
|
|
807
|
+
|
|
808
|
+
const before = lines.slice(0, zoneStart).join('\n');
|
|
809
|
+
const after = lines.slice(zoneEnd).join('\n');
|
|
810
|
+
|
|
811
|
+
return [before, updatedZone, after].filter(Boolean).join('\n');
|
|
812
|
+
}
|
|
813
|
+
|
|
351
814
|
// ── Serialization ────────────────────────────────────────────────────
|
|
352
815
|
|
|
353
816
|
/** Reconstruct source text from blocks */
|
|
@@ -9,25 +9,33 @@ type PostTransformFn = (node: SerializedTag, context: { modifiers: Record<string
|
|
|
9
9
|
|
|
10
10
|
/** Runes needing external resources or runtime data — show placeholder in editor */
|
|
11
11
|
const RUNTIME_ONLY_TYPES = new Set([
|
|
12
|
-
'
|
|
12
|
+
'nav', 'nav-group', 'nav-item', // needs RfContext.pages
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Restore
|
|
17
|
-
* The server
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Restore functions that were lost during JSON serialization.
|
|
17
|
+
* The server sends themeConfig as JSON; JSON.stringify silently drops functions.
|
|
18
|
+
* We merge postTransform and styles (which may contain transform functions)
|
|
19
|
+
* back from the statically-imported baseConfig (core runes) and from the
|
|
20
|
+
* community tags bundle (community runes).
|
|
20
21
|
*/
|
|
21
|
-
function
|
|
22
|
+
function restoreFunctions(
|
|
22
23
|
config: ThemeConfig,
|
|
23
24
|
communityPostTransforms?: Record<string, PostTransformFn>,
|
|
25
|
+
communityStyles?: Record<string, Record<string, unknown>>,
|
|
24
26
|
): ThemeConfig {
|
|
25
27
|
const runes = { ...config.runes };
|
|
28
|
+
// Core runes: restore from baseConfig
|
|
26
29
|
for (const [name, rune] of Object.entries(baseConfig.runes)) {
|
|
27
|
-
if (
|
|
28
|
-
|
|
30
|
+
if (!runes[name]) continue;
|
|
31
|
+
const patches: Record<string, unknown> = {};
|
|
32
|
+
if (rune.postTransform) patches.postTransform = rune.postTransform;
|
|
33
|
+
if (rune.styles) patches.styles = rune.styles;
|
|
34
|
+
if (Object.keys(patches).length) {
|
|
35
|
+
runes[name] = { ...runes[name], ...patches };
|
|
29
36
|
}
|
|
30
37
|
}
|
|
38
|
+
// Community runes: restore from bundle exports
|
|
31
39
|
if (communityPostTransforms) {
|
|
32
40
|
for (const [name, postTransform] of Object.entries(communityPostTransforms)) {
|
|
33
41
|
if (runes[name]) {
|
|
@@ -35,6 +43,13 @@ function restorePostTransforms(
|
|
|
35
43
|
}
|
|
36
44
|
}
|
|
37
45
|
}
|
|
46
|
+
if (communityStyles) {
|
|
47
|
+
for (const [name, styles] of Object.entries(communityStyles)) {
|
|
48
|
+
if (runes[name]) {
|
|
49
|
+
runes[name] = { ...runes[name], styles };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
38
53
|
return { ...config, runes };
|
|
39
54
|
}
|
|
40
55
|
|
|
@@ -53,9 +68,10 @@ export function renderBlockPreview(
|
|
|
53
68
|
extraTags?: Record<string, unknown>,
|
|
54
69
|
communityPostTransforms?: Record<string, PostTransformFn>,
|
|
55
70
|
aggregated?: AggregatedData,
|
|
71
|
+
communityStyles?: Record<string, Record<string, unknown>>,
|
|
56
72
|
): { html: string; isComponent: boolean } {
|
|
57
73
|
const ast = Markdoc.parse(source);
|
|
58
|
-
const fullConfig =
|
|
74
|
+
const fullConfig = restoreFunctions(themeConfig, communityPostTransforms, communityStyles);
|
|
59
75
|
const mergedTags = extraTags ? { ...tags, ...extraTags as Record<string, import('@markdoc/markdoc').Schema> } : tags;
|
|
60
76
|
const renderable = Markdoc.transform(ast, {
|
|
61
77
|
tags: mergedTags,
|
|
@@ -90,8 +106,8 @@ function checkIsRuntimeOnly(node: RendererNode): boolean {
|
|
|
90
106
|
if (node === null || node === undefined) return false;
|
|
91
107
|
if (typeof node === 'string' || typeof node === 'number') return false;
|
|
92
108
|
if (Array.isArray(node)) return node.some(checkIsRuntimeOnly);
|
|
93
|
-
if ('attributes' in node && node.attributes?.
|
|
94
|
-
return RUNTIME_ONLY_TYPES.has(node.attributes
|
|
109
|
+
if ('attributes' in node && node.attributes?.['data-rune']) {
|
|
110
|
+
return RUNTIME_ONLY_TYPES.has(node.attributes['data-rune']);
|
|
95
111
|
}
|
|
96
112
|
return false;
|
|
97
113
|
}
|
|
@@ -109,11 +125,11 @@ function injectSandboxDesignTokens(node: RendererNode, aggregated: AggregatedDat
|
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
const tag = node as SerializedTag;
|
|
112
|
-
if (tag.attributes?.
|
|
128
|
+
if (tag.attributes?.['data-rune'] === 'sandbox') {
|
|
113
129
|
const design = aggregated['design'] as { contexts?: Record<string, unknown> } | undefined;
|
|
114
130
|
const contexts = design?.contexts ?? {};
|
|
115
131
|
const contextChild = tag.children?.find(
|
|
116
|
-
c => (c as SerializedTag)?.attributes?.
|
|
132
|
+
c => (c as SerializedTag)?.attributes?.['data-field'] === 'context',
|
|
117
133
|
) as SerializedTag | undefined;
|
|
118
134
|
const scope = (contextChild?.attributes?.content as string) ?? 'default';
|
|
119
135
|
const tokens = contexts[scope];
|
|
@@ -121,7 +137,7 @@ function injectSandboxDesignTokens(node: RendererNode, aggregated: AggregatedDat
|
|
|
121
137
|
const injected: SerializedTag = {
|
|
122
138
|
$$mdtype: 'Tag',
|
|
123
139
|
name: 'meta',
|
|
124
|
-
attributes: {
|
|
140
|
+
attributes: { 'data-field': 'design-tokens', content: JSON.stringify(tokens) },
|
|
125
141
|
children: [],
|
|
126
142
|
};
|
|
127
143
|
return { ...tag, children: [...(tag.children ?? []), injected] };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"community-tags-builder.d.ts","sourceRoot":"","sources":["../src/community-tags-builder.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,wBAAwB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,wBAAwB,CAC7C,YAAY,EAAE,MAAM,EAAE,EACtB,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,wBAAwB,CAAC,
|
|
1
|
+
{"version":3,"file":"community-tags-builder.d.ts","sourceRoot":"","sources":["../src/community-tags-builder.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,wBAAwB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,wBAAwB,CAC7C,YAAY,EAAE,MAAM,EAAE,EACtB,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,wBAAwB,CAAC,CAyDnC"}
|
|
@@ -28,6 +28,7 @@ ${imports}
|
|
|
28
28
|
|
|
29
29
|
const communityTags = {};
|
|
30
30
|
const communityPostTransforms = {};
|
|
31
|
+
const communityStyles = {};
|
|
31
32
|
for (const pkg of [${pkgArray}]) {
|
|
32
33
|
for (const [name, entry] of Object.entries(pkg.runes ?? {})) {
|
|
33
34
|
if (entry.transform) {
|
|
@@ -41,9 +42,12 @@ for (const pkg of [${pkgArray}]) {
|
|
|
41
42
|
if (typeof runeConfig.postTransform === 'function') {
|
|
42
43
|
communityPostTransforms[name] = runeConfig.postTransform;
|
|
43
44
|
}
|
|
45
|
+
if (runeConfig.styles) {
|
|
46
|
+
communityStyles[name] = runeConfig.styles;
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
|
-
export { communityTags, communityPostTransforms };
|
|
50
|
+
export { communityTags, communityPostTransforms, communityStyles };
|
|
47
51
|
`;
|
|
48
52
|
try {
|
|
49
53
|
const { build } = await import('esbuild');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"community-tags-builder.js","sourceRoot":"","sources":["../src/community-tags-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAOzC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC7C,YAAsB,EACtB,WAAmB;IAEnB,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAEzE,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,uBAAuB,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvG,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;IAEnD,IAAI,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAEjE,+EAA+E;IAC/E,MAAM,OAAO,GAAG,YAAY;SAC1B,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;SAChE,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG;EACjB,OAAO
|
|
1
|
+
{"version":3,"file":"community-tags-builder.js","sourceRoot":"","sources":["../src/community-tags-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAOzC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC7C,YAAsB,EACtB,WAAmB;IAEnB,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAEzE,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,uBAAuB,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvG,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;IAEnD,IAAI,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAEjE,+EAA+E;IAC/E,MAAM,OAAO,GAAG,YAAY;SAC1B,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;SAChE,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG;EACjB,OAAO;;;;;qBAKY,QAAQ;;;;;;;;;;;;;;;;;;;CAmB5B,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;QAC1C,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,MAAM,KAAK,CAAC;YACX,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;YACrE,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,UAAU;YACnB,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,CAAC,QAAQ,CAAC;SACpB,CAAC,CAAC;QACH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,+CAA+C,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;QACtF,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC3C,CAAC;AACF,CAAC"}
|
package/dist/server.d.ts
CHANGED
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC;AAShF,MAAM,WAAW,aAAa;IAC7B,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,WAAW,EAAE,WAAW,CAAC;IACzB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,kBAAkB,EAAE,MAAM,CAAC,CAAC;IAC9D,qDAAqD;IACrD,cAAc,CAAC,EAAE,KAAK,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QACrD,WAAW,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QACvC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,OAAO,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAC;QACnF,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;CACH;AAgCD,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAsTvE"}
|