@refrakt-md/editor 0.8.0 → 0.8.2
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-Ca-wW6uw.js → index-80NtMar1.js} +1 -1
- package/app/dist/assets/index-B6H6LF1M.css +1 -0
- package/app/dist/assets/{index-Dg4A5Pez.js → index-BDj1XPol.js} +1 -1
- package/app/dist/assets/{index-BfYWp0QC.js → index-BXe1fKaT.js} +1 -1
- package/app/dist/assets/{index-Cq0Maciq.js → index-BfxTGrHB.js} +1 -1
- package/app/dist/assets/{index-BsSUa0GD.js → index-Bn8ajfVl.js} +1 -1
- package/app/dist/assets/{index-D6vnTt4b.js → index-CCkzIGTi.js} +2 -2
- package/app/dist/assets/{index-BehCztSl.js → index-CXeK-dZx.js} +1 -1
- package/app/dist/assets/{index-iGDqoXj_.js → index-CaRBCHaX.js} +1 -1
- package/app/dist/assets/index-Cd12jZId.js +479 -0
- package/app/dist/assets/{index-D5pMhPrg.js → index-Cgbvx23V.js} +1 -1
- package/app/dist/assets/{index-IU6QYZAa.js → index-D5ucdUTo.js} +1 -1
- package/app/dist/assets/{index-CdpS6tGk.js → index-DGYxLhpR.js} +1 -1
- package/app/dist/assets/{index-RKEq45V5.js → index-DNJBunzP.js} +1 -1
- package/app/dist/assets/{index-Cgaw2jCE.js → index-DNtuldOx.js} +1 -1
- package/app/dist/assets/{index-BEPqnnsd.js → index-DQUOY-pF.js} +1 -1
- package/app/dist/assets/{index-2hOoPFOR.js → index-DskvyNKT.js} +1 -1
- package/app/dist/assets/{index-CLZfwYyS.js → index-aPeHMqUX.js} +1 -1
- package/app/dist/assets/{index-BobjskUl.js → index-dGztG-54.js} +1 -1
- package/app/dist/assets/{index-DHALjxX5.js → index-xo7v6nRB.js} +1 -1
- package/app/dist/index.html +2 -2
- package/app/src/lib/api/client.ts +81 -0
- package/app/src/lib/components/ActionEditPopover.svelte +267 -0
- package/app/src/lib/components/BlockCard.svelte +285 -0
- package/app/src/lib/components/BlockEditPanel.svelte +640 -260
- package/app/src/lib/components/BlockEditor.svelte +513 -52
- package/app/src/lib/components/CodeEditPopover.svelte +444 -0
- package/app/src/lib/components/ContentModelTree.svelte +835 -0
- package/app/src/lib/components/EditorLayout.svelte +1 -6
- package/app/src/lib/components/FrontmatterEditPanel.svelte +0 -1
- package/app/src/lib/components/IconPickerPopover.svelte +389 -0
- package/app/src/lib/components/ImageEditPopover.svelte +519 -0
- package/app/src/lib/components/InlineEditPopover.svelte +616 -0
- package/app/src/lib/components/RuneAttributes.svelte +51 -0
- package/app/src/lib/editor/block-parser.ts +424 -6
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +189 -2
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/app/dist/assets/index-98ylvoBO.css +0 -1
- package/app/dist/assets/index-CVzOx0nV.js +0 -372
|
@@ -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.
|
|
@@ -149,8 +150,11 @@ export function parseBlocks(source: string): ParsedBlock[] {
|
|
|
149
150
|
|
|
150
151
|
// ── Fenced code blocks ───────────────────────────────────
|
|
151
152
|
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
152
|
-
const
|
|
153
|
-
|
|
153
|
+
const fenceChar = trimmed[0];
|
|
154
|
+
let fenceLen = 3;
|
|
155
|
+
while (fenceLen < trimmed.length && trimmed[fenceLen] === fenceChar) fenceLen++;
|
|
156
|
+
const fence = trimmed.slice(0, fenceLen);
|
|
157
|
+
const lang = trimmed.slice(fenceLen).trim();
|
|
154
158
|
const start = i;
|
|
155
159
|
i++;
|
|
156
160
|
const codeLines: string[] = [];
|
|
@@ -395,8 +399,11 @@ export function parseContentTree(content: string): ContentNode[] {
|
|
|
395
399
|
|
|
396
400
|
// Fenced code
|
|
397
401
|
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
398
|
-
const
|
|
399
|
-
|
|
402
|
+
const fenceChar = trimmed[0];
|
|
403
|
+
let fenceLen = 3;
|
|
404
|
+
while (fenceLen < trimmed.length && trimmed[fenceLen] === fenceChar) fenceLen++;
|
|
405
|
+
const fence = trimmed.slice(0, fenceLen);
|
|
406
|
+
const lang = trimmed.slice(fenceLen).trim();
|
|
400
407
|
const start = i;
|
|
401
408
|
i++;
|
|
402
409
|
const codeStart = i;
|
|
@@ -504,13 +511,26 @@ export function parseContentTree(content: string): ContentNode[] {
|
|
|
504
511
|
// List
|
|
505
512
|
if (/^(\d+\.|[-*+])\s/.test(trimmed)) {
|
|
506
513
|
const start = i;
|
|
514
|
+
const ordered = /^\d+\./.test(trimmed);
|
|
507
515
|
while (i < lines.length) {
|
|
508
516
|
const lt = lines[i].trimStart();
|
|
509
|
-
if (/^(\d+\.|[-*+])\s/.test(lt)
|
|
517
|
+
if (/^(\d+\.|[-*+])\s/.test(lt)) {
|
|
518
|
+
// Stop if the list marker type switches (e.g. unordered → ordered)
|
|
519
|
+
const nextOrdered = /^\d+\./.test(lt);
|
|
520
|
+
if (nextOrdered !== ordered && i > start) break;
|
|
521
|
+
i++;
|
|
522
|
+
} else if (lt === '' && i + 1 < lines.length && /^(\s+|\d+\.|[-*+]\s)/.test(lines[i + 1])) {
|
|
523
|
+
// Blank line followed by continuation — but stop if the next item switches type
|
|
524
|
+
const nextNonBlank = lines[i + 1].trimStart();
|
|
525
|
+
if (/^(\d+\.|[-*+])\s/.test(nextNonBlank)) {
|
|
526
|
+
const nextOrdered = /^\d+\./.test(nextNonBlank);
|
|
527
|
+
if (nextOrdered !== ordered) break;
|
|
528
|
+
}
|
|
529
|
+
i++;
|
|
530
|
+
} else if (lt !== '' && lines[i].startsWith(' ')) {
|
|
510
531
|
i++;
|
|
511
532
|
} else { break; }
|
|
512
533
|
}
|
|
513
|
-
const ordered = /^\d+\./.test(trimmed);
|
|
514
534
|
nodes.push({ type: 'list', label: ordered ? 'Ordered list' : 'List', source: lines.slice(start, i).join('\n'), listOrdered: ordered });
|
|
515
535
|
continue;
|
|
516
536
|
}
|
|
@@ -538,6 +558,404 @@ export function replaceNodeSource(parentContent: string, oldSource: string, newS
|
|
|
538
558
|
return parentContent.slice(0, idx) + newSource + parentContent.slice(idx + oldSource.length);
|
|
539
559
|
}
|
|
540
560
|
|
|
561
|
+
// ── Content model field insertion / removal ──────────────────────────
|
|
562
|
+
|
|
563
|
+
/** Default markdown template for a field's match type */
|
|
564
|
+
function defaultTemplate(match: string): string {
|
|
565
|
+
const primary = match.includes('|') ? match.split('|')[0] : match;
|
|
566
|
+
switch (primary) {
|
|
567
|
+
case 'heading': return '## Heading';
|
|
568
|
+
case 'heading:1': return '# Heading';
|
|
569
|
+
case 'heading:2': return '## Heading';
|
|
570
|
+
case 'heading:3': return '### Heading';
|
|
571
|
+
case 'heading:4': return '#### Heading';
|
|
572
|
+
case 'heading:5': return '##### Heading';
|
|
573
|
+
case 'heading:6': return '###### Heading';
|
|
574
|
+
case 'paragraph': return 'Text content';
|
|
575
|
+
case 'list': case 'list:unordered': return '- Item 1\n- Item 2';
|
|
576
|
+
case 'list:ordered': return '1. Item 1\n2. Item 2';
|
|
577
|
+
case 'fence': return '```\ncode\n```';
|
|
578
|
+
case 'image': return '';
|
|
579
|
+
case 'blockquote': case 'quote': return '> Quote';
|
|
580
|
+
case 'any': return 'Content';
|
|
581
|
+
default: return 'Content';
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Insert content for an empty field in a resolved structure.
|
|
587
|
+
* Returns the updated innerContent string.
|
|
588
|
+
*/
|
|
589
|
+
export function insertFieldContent(
|
|
590
|
+
innerContent: string,
|
|
591
|
+
structure: ResolvedStructure,
|
|
592
|
+
fieldName: string,
|
|
593
|
+
zoneName?: string,
|
|
594
|
+
): string {
|
|
595
|
+
const { field, fields } = findField(structure, fieldName, zoneName);
|
|
596
|
+
if (!field || field.filled) return innerContent;
|
|
597
|
+
|
|
598
|
+
const template = field.template || defaultTemplate(field.match);
|
|
599
|
+
|
|
600
|
+
if (structure.type === 'delimited' && zoneName) {
|
|
601
|
+
return insertInDelimited(innerContent, structure, zoneName, fieldName, fields!, template);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (structure.type === 'sequence') {
|
|
605
|
+
return insertInSequence(innerContent, structure.fields, fieldName, template);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return innerContent;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Remove content for a filled field in a resolved structure.
|
|
613
|
+
* Returns the updated innerContent string.
|
|
614
|
+
*/
|
|
615
|
+
export function removeFieldContent(
|
|
616
|
+
innerContent: string,
|
|
617
|
+
structure: ResolvedStructure,
|
|
618
|
+
fieldName: string,
|
|
619
|
+
zoneName?: string,
|
|
620
|
+
): string {
|
|
621
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
622
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
623
|
+
|
|
624
|
+
let result = innerContent;
|
|
625
|
+
// Remove all matched nodes' source text (reverse order to preserve indices)
|
|
626
|
+
for (let i = field.nodes.length - 1; i >= 0; i--) {
|
|
627
|
+
const nodeSource = field.nodes[i].source;
|
|
628
|
+
result = removeNodeSource(result, nodeSource);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Clean up double blank lines left by removal
|
|
632
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
633
|
+
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Append a new item to a filled list field.
|
|
639
|
+
* Returns the updated innerContent string.
|
|
640
|
+
*/
|
|
641
|
+
export function appendListItem(
|
|
642
|
+
innerContent: string,
|
|
643
|
+
structure: ResolvedStructure,
|
|
644
|
+
fieldName: string,
|
|
645
|
+
zoneName?: string,
|
|
646
|
+
): string {
|
|
647
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
648
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
649
|
+
|
|
650
|
+
const template = field.template || '- New item';
|
|
651
|
+
const lastNode = field.nodes[field.nodes.length - 1];
|
|
652
|
+
const idx = innerContent.indexOf(lastNode.source);
|
|
653
|
+
if (idx === -1) return innerContent;
|
|
654
|
+
|
|
655
|
+
// For ordered lists, adjust the number to follow the last item
|
|
656
|
+
let finalTemplate = template;
|
|
657
|
+
if (field.match === 'list:ordered') {
|
|
658
|
+
const numMatch = lastNode.source.match(/^(\d+)\./);
|
|
659
|
+
const nextNum = numMatch ? parseInt(numMatch[1], 10) + 1 : field.nodes.length + 1;
|
|
660
|
+
finalTemplate = template.replace(/^\d+\./, `${nextNum}.`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const afterEnd = idx + lastNode.source.length;
|
|
664
|
+
return innerContent.slice(0, afterEnd) + '\n' + finalTemplate + innerContent.slice(afterEnd);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Remove a single item from a list field by index.
|
|
669
|
+
* If the last item is removed, removes the entire list node.
|
|
670
|
+
*/
|
|
671
|
+
export function removeListItem(
|
|
672
|
+
innerContent: string,
|
|
673
|
+
structure: ResolvedStructure,
|
|
674
|
+
fieldName: string,
|
|
675
|
+
itemIndex: number,
|
|
676
|
+
zoneName?: string,
|
|
677
|
+
): string {
|
|
678
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
679
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
680
|
+
|
|
681
|
+
// Get the list source from the first list node
|
|
682
|
+
const listNode = field.nodes[0];
|
|
683
|
+
const items = splitListItems(listNode.source);
|
|
684
|
+
if (itemIndex < 0 || itemIndex >= items.length) return innerContent;
|
|
685
|
+
|
|
686
|
+
if (items.length === 1) {
|
|
687
|
+
// Removing the last item — remove the entire list node
|
|
688
|
+
return removeNodeSource(innerContent, listNode.source);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Remove the item and rejoin
|
|
692
|
+
const remaining = items.filter((_, i) => i !== itemIndex);
|
|
693
|
+
const newListSource = remaining.join('\n\n');
|
|
694
|
+
const idx = innerContent.indexOf(listNode.source);
|
|
695
|
+
if (idx === -1) return innerContent;
|
|
696
|
+
return innerContent.slice(0, idx) + newListSource + innerContent.slice(idx + listNode.source.length);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Reorder a list item within a list field by moving it from one index to another.
|
|
701
|
+
* For ordered lists, renumbers the items after reordering.
|
|
702
|
+
*/
|
|
703
|
+
export function reorderListItem(
|
|
704
|
+
innerContent: string,
|
|
705
|
+
structure: ResolvedStructure,
|
|
706
|
+
fieldName: string,
|
|
707
|
+
fromIndex: number,
|
|
708
|
+
toIndex: number,
|
|
709
|
+
zoneName?: string,
|
|
710
|
+
): string {
|
|
711
|
+
if (fromIndex === toIndex) return innerContent;
|
|
712
|
+
|
|
713
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
714
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
715
|
+
|
|
716
|
+
const listNode = field.nodes[0];
|
|
717
|
+
const items = splitListItems(listNode.source);
|
|
718
|
+
if (fromIndex < 0 || fromIndex >= items.length || toIndex < 0 || toIndex >= items.length) return innerContent;
|
|
719
|
+
|
|
720
|
+
// Move item from fromIndex to toIndex
|
|
721
|
+
const reordered = [...items];
|
|
722
|
+
const [moved] = reordered.splice(fromIndex, 1);
|
|
723
|
+
reordered.splice(toIndex, 0, moved);
|
|
724
|
+
|
|
725
|
+
// For ordered lists, renumber items
|
|
726
|
+
const isOrdered = field.match === 'list:ordered' || /^\d+\.\s/.test(items[0]);
|
|
727
|
+
if (isOrdered) {
|
|
728
|
+
for (let i = 0; i < reordered.length; i++) {
|
|
729
|
+
reordered[i] = reordered[i].replace(/^\d+\./, `${i + 1}.`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const newListSource = reordered.join('\n');
|
|
734
|
+
const idx = innerContent.indexOf(listNode.source);
|
|
735
|
+
if (idx === -1) return innerContent;
|
|
736
|
+
return innerContent.slice(0, idx) + newListSource + innerContent.slice(idx + listNode.source.length);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Append a new item to a filled greedy field.
|
|
741
|
+
* Inserts a default template after the last node in field.nodes[].
|
|
742
|
+
*/
|
|
743
|
+
export function appendGreedyItem(
|
|
744
|
+
innerContent: string,
|
|
745
|
+
structure: ResolvedStructure,
|
|
746
|
+
fieldName: string,
|
|
747
|
+
zoneName?: string,
|
|
748
|
+
): string {
|
|
749
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
750
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
751
|
+
|
|
752
|
+
const template = field.template || defaultTemplate(field.match);
|
|
753
|
+
const lastNode = field.nodes[field.nodes.length - 1];
|
|
754
|
+
const idx = innerContent.indexOf(lastNode.source);
|
|
755
|
+
if (idx === -1) return innerContent;
|
|
756
|
+
|
|
757
|
+
const afterEnd = idx + lastNode.source.length;
|
|
758
|
+
return innerContent.slice(0, afterEnd) + '\n\n' + template + innerContent.slice(afterEnd);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Remove a single node from a greedy field by index.
|
|
763
|
+
*/
|
|
764
|
+
export function removeGreedyItem(
|
|
765
|
+
innerContent: string,
|
|
766
|
+
structure: ResolvedStructure,
|
|
767
|
+
fieldName: string,
|
|
768
|
+
itemIndex: number,
|
|
769
|
+
zoneName?: string,
|
|
770
|
+
): string {
|
|
771
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
772
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
773
|
+
if (itemIndex < 0 || itemIndex >= field.nodes.length) return innerContent;
|
|
774
|
+
|
|
775
|
+
const nodeSource = field.nodes[itemIndex].source;
|
|
776
|
+
let result = removeNodeSource(innerContent, nodeSource);
|
|
777
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Reorder a node within a greedy field by moving it from one index to another.
|
|
783
|
+
*/
|
|
784
|
+
export function reorderGreedyItem(
|
|
785
|
+
innerContent: string,
|
|
786
|
+
structure: ResolvedStructure,
|
|
787
|
+
fieldName: string,
|
|
788
|
+
fromIndex: number,
|
|
789
|
+
toIndex: number,
|
|
790
|
+
zoneName?: string,
|
|
791
|
+
): string {
|
|
792
|
+
if (fromIndex === toIndex) return innerContent;
|
|
793
|
+
|
|
794
|
+
const { field } = findField(structure, fieldName, zoneName);
|
|
795
|
+
if (!field || !field.filled || field.nodes.length === 0) return innerContent;
|
|
796
|
+
if (fromIndex < 0 || fromIndex >= field.nodes.length) return innerContent;
|
|
797
|
+
if (toIndex < 0 || toIndex >= field.nodes.length) return innerContent;
|
|
798
|
+
|
|
799
|
+
const nodesSources = field.nodes.map(n => n.source);
|
|
800
|
+
|
|
801
|
+
const reordered = [...nodesSources];
|
|
802
|
+
const [moved] = reordered.splice(fromIndex, 1);
|
|
803
|
+
reordered.splice(toIndex, 0, moved);
|
|
804
|
+
|
|
805
|
+
// Find the span from the first node to the last node in innerContent
|
|
806
|
+
const firstSource = field.nodes[0].source;
|
|
807
|
+
const lastSource = field.nodes[field.nodes.length - 1].source;
|
|
808
|
+
const spanStart = innerContent.indexOf(firstSource);
|
|
809
|
+
const lastIdx = innerContent.indexOf(lastSource);
|
|
810
|
+
if (spanStart === -1 || lastIdx === -1) return innerContent;
|
|
811
|
+
const spanEnd = lastIdx + lastSource.length;
|
|
812
|
+
|
|
813
|
+
const newSpan = reordered.join('\n\n');
|
|
814
|
+
return innerContent.slice(0, spanStart) + newSpan + innerContent.slice(spanEnd);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Split a list source into individual item sources.
|
|
819
|
+
* Each item starts with a list marker (-, *, +, or 1.) and may have
|
|
820
|
+
* indented continuation lines or blank-line separated paragraphs.
|
|
821
|
+
*/
|
|
822
|
+
export function splitListItems(listSource: string): string[] {
|
|
823
|
+
const lines = listSource.split('\n');
|
|
824
|
+
const items: string[] = [];
|
|
825
|
+
let current: string[] = [];
|
|
826
|
+
|
|
827
|
+
for (const line of lines) {
|
|
828
|
+
if (/^[-*+]\s|^\d+\.\s/.test(line) && current.length > 0) {
|
|
829
|
+
items.push(current.join('\n'));
|
|
830
|
+
current = [line];
|
|
831
|
+
} else {
|
|
832
|
+
current.push(line);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (current.length > 0) items.push(current.join('\n'));
|
|
836
|
+
return items;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function findField(
|
|
840
|
+
structure: ResolvedStructure,
|
|
841
|
+
fieldName: string,
|
|
842
|
+
zoneName?: string,
|
|
843
|
+
): { field: ResolvedField | null; fields: ResolvedField[] | null } {
|
|
844
|
+
if (structure.type === 'sequence') {
|
|
845
|
+
const field = structure.fields.find(f => f.name === fieldName) ?? null;
|
|
846
|
+
return { field, fields: structure.fields };
|
|
847
|
+
}
|
|
848
|
+
if (structure.type === 'delimited' && zoneName) {
|
|
849
|
+
const zone = structure.zones.find(z => z.name === zoneName);
|
|
850
|
+
if (!zone) return { field: null, fields: null };
|
|
851
|
+
const field = zone.fields.find(f => f.name === fieldName) ?? null;
|
|
852
|
+
return { field, fields: zone.fields };
|
|
853
|
+
}
|
|
854
|
+
return { field: null, fields: null };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** Remove a node's source from content, handling surrounding whitespace */
|
|
858
|
+
function removeNodeSource(content: string, nodeSource: string): string {
|
|
859
|
+
const idx = content.indexOf(nodeSource);
|
|
860
|
+
if (idx === -1) return content;
|
|
861
|
+
|
|
862
|
+
let start = idx;
|
|
863
|
+
let end = idx + nodeSource.length;
|
|
864
|
+
|
|
865
|
+
// Extend to consume surrounding blank lines
|
|
866
|
+
while (start > 0 && content[start - 1] === '\n') start--;
|
|
867
|
+
if (start > 0) start++; // Keep one newline
|
|
868
|
+
while (end < content.length && content[end] === '\n') end++;
|
|
869
|
+
|
|
870
|
+
return content.slice(0, start) + content.slice(end);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function insertInSequence(
|
|
874
|
+
innerContent: string,
|
|
875
|
+
fields: ResolvedField[],
|
|
876
|
+
fieldName: string,
|
|
877
|
+
template: string,
|
|
878
|
+
): string {
|
|
879
|
+
const fieldIndex = fields.findIndex(f => f.name === fieldName);
|
|
880
|
+
if (fieldIndex === -1) return innerContent;
|
|
881
|
+
|
|
882
|
+
// Find the last filled field before this one to insert after
|
|
883
|
+
let insertAfterNode: ContentNode | null = null;
|
|
884
|
+
for (let i = fieldIndex - 1; i >= 0; i--) {
|
|
885
|
+
if (fields[i].filled && fields[i].nodes.length > 0) {
|
|
886
|
+
const nodes = fields[i].nodes;
|
|
887
|
+
insertAfterNode = nodes[nodes.length - 1];
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (insertAfterNode) {
|
|
893
|
+
const idx = innerContent.indexOf(insertAfterNode.source);
|
|
894
|
+
if (idx !== -1) {
|
|
895
|
+
const afterEnd = idx + insertAfterNode.source.length;
|
|
896
|
+
return innerContent.slice(0, afterEnd) + '\n\n' + template + innerContent.slice(afterEnd);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Find the first filled field after this one to insert before
|
|
901
|
+
for (let i = fieldIndex + 1; i < fields.length; i++) {
|
|
902
|
+
if (fields[i].filled && fields[i].nodes.length > 0) {
|
|
903
|
+
const beforeNode = fields[i].nodes[0];
|
|
904
|
+
const idx = innerContent.indexOf(beforeNode.source);
|
|
905
|
+
if (idx !== -1) {
|
|
906
|
+
return innerContent.slice(0, idx) + template + '\n\n' + innerContent.slice(idx);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// No adjacent filled fields — append to content
|
|
912
|
+
const trimmed = innerContent.trimEnd();
|
|
913
|
+
return trimmed + (trimmed ? '\n\n' : '') + template;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function insertInDelimited(
|
|
917
|
+
innerContent: string,
|
|
918
|
+
structure: ResolvedStructure & { type: 'delimited' },
|
|
919
|
+
zoneName: string,
|
|
920
|
+
fieldName: string,
|
|
921
|
+
fields: ResolvedField[],
|
|
922
|
+
template: string,
|
|
923
|
+
): string {
|
|
924
|
+
const zoneIndex = structure.zones.findIndex(z => z.name === zoneName);
|
|
925
|
+
if (zoneIndex === -1) return innerContent;
|
|
926
|
+
|
|
927
|
+
// Split content at delimiters (---) to find zone boundaries
|
|
928
|
+
const lines = innerContent.split('\n');
|
|
929
|
+
const delimiterIndices: number[] = [];
|
|
930
|
+
for (let i = 0; i < lines.length; i++) {
|
|
931
|
+
if (lines[i].trim() === '---') delimiterIndices.push(i);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// If the target zone doesn't exist yet (no delimiter), add one
|
|
935
|
+
if (zoneIndex > 0 && delimiterIndices.length < zoneIndex) {
|
|
936
|
+
const trimmed = innerContent.trimEnd();
|
|
937
|
+
const missingDelimiters = zoneIndex - delimiterIndices.length;
|
|
938
|
+
let insert = '';
|
|
939
|
+
for (let i = 0; i < missingDelimiters; i++) {
|
|
940
|
+
insert += '\n\n---';
|
|
941
|
+
}
|
|
942
|
+
return trimmed + insert + '\n\n' + template;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Zone exists — insert within it using sequence logic
|
|
946
|
+
// Determine the zone's content range in lines
|
|
947
|
+
const zoneStart = zoneIndex === 0 ? 0 : delimiterIndices[zoneIndex - 1] + 1;
|
|
948
|
+
const zoneEnd = zoneIndex < delimiterIndices.length ? delimiterIndices[zoneIndex] : lines.length;
|
|
949
|
+
const zoneContent = lines.slice(zoneStart, zoneEnd).join('\n');
|
|
950
|
+
|
|
951
|
+
const updatedZone = insertInSequence(zoneContent, fields, fieldName, template);
|
|
952
|
+
|
|
953
|
+
const before = lines.slice(0, zoneStart).join('\n');
|
|
954
|
+
const after = lines.slice(zoneEnd).join('\n');
|
|
955
|
+
|
|
956
|
+
return [before, updatedZone, after].filter(Boolean).join('\n');
|
|
957
|
+
}
|
|
958
|
+
|
|
541
959
|
// ── Serialization ────────────────────────────────────────────────────
|
|
542
960
|
|
|
543
961
|
/** Reconstruct source text from blocks */
|
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;AAkCD,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAoUvE"}
|