@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.
Files changed (50) hide show
  1. package/app/dist/assets/{index-4SP4_AaD.js → index-BBinZAiy.js} +1 -1
  2. package/app/dist/assets/index-BD2EBUrQ.css +1 -0
  3. package/app/dist/assets/{index-D77rckeh.js → index-BLuaHLN3.js} +1 -1
  4. package/app/dist/assets/{index-30gAspk8.js → index-BgCNqcSo.js} +1 -1
  5. package/app/dist/assets/index-BlAOhWAQ.js +453 -0
  6. package/app/dist/assets/{index-BZ4adnS0.js → index-BwFn9q4x.js} +1 -1
  7. package/app/dist/assets/{index-DFkteo0w.js → index-C72UC2ga.js} +1 -1
  8. package/app/dist/assets/{index-x67KGOIr.js → index-COIPZ34u.js} +1 -1
  9. package/app/dist/assets/{index-BEFUVB_B.js → index-CW02bulk.js} +1 -1
  10. package/app/dist/assets/{index-CI5PewQM.js → index-CXFMPmtf.js} +1 -1
  11. package/app/dist/assets/{index-ByHhigzw.js → index-CeU_s7BB.js} +1 -1
  12. package/app/dist/assets/{index-DvgOtlru.js → index-CqHjo2YT.js} +1 -1
  13. package/app/dist/assets/{index-DKnhR16N.js → index-D3TQo8gu.js} +1 -1
  14. package/app/dist/assets/{index-Baf7ZSct.js → index-DVM3uoxc.js} +1 -1
  15. package/app/dist/assets/{index-C9w1RpYY.js → index-DW2zI-Ss.js} +1 -1
  16. package/app/dist/assets/{index--rGC9bba.js → index-D_Y6J00B.js} +1 -1
  17. package/app/dist/assets/{index-kPhFxtn-.js → index-DgIg-QAA.js} +2 -2
  18. package/app/dist/assets/{index-DIuFNfTc.js → index-DmY6uqAw.js} +1 -1
  19. package/app/dist/assets/{index-D1WOi3EN.js → index-DzHt8ZRh.js} +1 -1
  20. package/app/dist/assets/{index-BwWzfQVn.js → index-ZLvRNfLb.js} +1 -1
  21. package/app/dist/index.html +2 -2
  22. package/app/src/lib/api/client.ts +49 -0
  23. package/app/src/lib/components/ActionEditPopover.svelte +245 -0
  24. package/app/src/lib/components/BlockCard.svelte +255 -1
  25. package/app/src/lib/components/BlockEditPanel.svelte +697 -138
  26. package/app/src/lib/components/BlockEditor.svelte +467 -389
  27. package/app/src/lib/components/CodeEditPopover.svelte +226 -0
  28. package/app/src/lib/components/ContentModelTree.svelte +562 -0
  29. package/app/src/lib/components/ContentTree.svelte +181 -0
  30. package/app/src/lib/components/EditorLayout.svelte +1 -6
  31. package/app/src/lib/components/FrontmatterEditPanel.svelte +0 -1
  32. package/app/src/lib/components/HeaderBar.svelte +38 -0
  33. package/app/src/lib/components/InlineEditPopover.svelte +593 -0
  34. package/app/src/lib/components/InsertBlockDialog.svelte +429 -0
  35. package/app/src/lib/components/PageCard.svelte +3 -4
  36. package/app/src/lib/components/PreviewPane.svelte +19 -1
  37. package/app/src/lib/components/RuneAttributes.svelte +249 -100
  38. package/app/src/lib/editor/block-parser.ts +463 -0
  39. package/app/src/lib/preview/block-renderer.ts +30 -14
  40. package/dist/community-tags-builder.d.ts.map +1 -1
  41. package/dist/community-tags-builder.js +5 -1
  42. package/dist/community-tags-builder.js.map +1 -1
  43. package/dist/server.d.ts +1 -0
  44. package/dist/server.d.ts.map +1 -1
  45. package/dist/server.js +92 -6
  46. package/dist/server.js.map +1 -1
  47. package/package.json +6 -6
  48. package/preview-runtime/App.svelte +2 -0
  49. package/app/dist/assets/index-DlrXwdpb.css +0 -1
  50. 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 '![alt](url)';
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
- 'Nav', 'NavGroup', 'NavItem', // needs RfContext.pages
12
+ 'nav', 'nav-group', 'nav-item', // needs RfContext.pages
13
13
  ]);
14
14
 
15
15
  /**
16
- * Restore postTransform hooks that were stripped during JSON serialization.
17
- * The server strips functions from the themeConfig before sending it as JSON;
18
- * we merge them back from the statically-imported baseConfig (core runes)
19
- * and from the community tags bundle (community runes).
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 restorePostTransforms(
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 (rune.postTransform && runes[name]) {
28
- runes[name] = { ...runes[name], postTransform: rune.postTransform };
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 = restorePostTransforms(themeConfig, communityPostTransforms);
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?.typeof) {
94
- return RUNTIME_ONLY_TYPES.has(node.attributes.typeof);
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?.typeof === 'Sandbox') {
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?.property === 'context',
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: { property: 'design-tokens', content: JSON.stringify(tokens) },
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,CAqDnC"}
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;;;;qBAIY,QAAQ;;;;;;;;;;;;;;;;CAgB5B,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"}
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
@@ -40,6 +40,7 @@ export interface EditorOptions {
40
40
  values?: string[];
41
41
  }>;
42
42
  example?: string;
43
+ contentModel?: object;
43
44
  }>;
44
45
  }
45
46
  export declare function startEditor(options: EditorOptions): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AASA,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;KACjB,CAAC,CAAC;CACH;AAgCD,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAsTvE"}
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"}