@cueloop/schema 0.1.0-alpha.10 → 0.1.0-alpha.100

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.
@@ -1,15 +1,95 @@
1
- /**
2
- * Working-copy block surgery: the only module that slices raw source text by
3
- * Block line ranges. Cut removes a block's lines; restore re-inserts a base
4
- * block's chunk and reports when the working copy round-trips back to the
5
- * submitted revision (pristine). Callers never touch lineStart/lineEnd.
6
- */
1
+ /** Working-copy source surgery for block and character cuts plus block restoration. */
2
+
3
+ import { parseBlocks, sourceOffsetAt, type Block } from "./markdown";
4
+ import type { TextCut } from "./types";
5
+
6
+ /** Add one source range and normalize overlaps without copying on a no-op. */
7
+ export function mergeTextCut(
8
+ source: string,
9
+ cuts: readonly TextCut[],
10
+ start: number,
11
+ end: number,
12
+ ): TextCut[] {
13
+ const boundedStart = Math.max(0, Math.min(start, source.length));
14
+ const boundedEnd = Math.max(boundedStart, Math.min(end, source.length));
15
+
16
+ if (boundedEnd === boundedStart) {
17
+ // SAFETY: a no-op returns the caller's array unchanged; callers never mutate the result.
18
+ return cuts as TextCut[];
19
+ }
20
+ const ranges = [
21
+ ...cuts.map((cut) => ({ start: cut.start, end: cut.end })),
22
+ { start: boundedStart, end: boundedEnd },
23
+ ]
24
+ .map((range) => ({
25
+ start: Math.max(0, Math.min(range.start, source.length)),
26
+ end: Math.max(0, Math.min(range.end, source.length)),
27
+ }))
28
+ .filter((range) => range.end > range.start)
29
+ .toSorted((left, right) => left.start - right.start || left.end - right.end);
30
+ const merged: Array<{ start: number; end: number }> = [];
31
+
32
+ for (const range of ranges) {
33
+ const previous = merged.at(-1);
34
+
35
+ if (previous && range.start <= previous.end) previous.end = Math.max(previous.end, range.end);
36
+ else merged.push(range);
37
+ }
38
+
39
+ return merged.map((range) => ({
40
+ ...range,
41
+ quote: source.slice(range.start, range.end),
42
+ }));
43
+ }
44
+
45
+ /** Restore a selected part of one contiguous character Cut. */
46
+ export function restoreTextCut(
47
+ source: string,
48
+ cuts: readonly TextCut[],
49
+ start: number,
50
+ end: number,
51
+ ): TextCut[] | null {
52
+ const cutIndex = cuts.findIndex((cut) => cut.start <= start && end <= cut.end);
7
53
 
8
- import { parseBlocks, type Block } from "./markdown";
54
+ if (cutIndex === -1 || end <= start) return null;
55
+ const cut = cuts[cutIndex]!;
56
+ const replacements: TextCut[] = [];
57
+
58
+ if (cut.start < start) {
59
+ replacements.push({ start: cut.start, end: start, quote: source.slice(cut.start, start) });
60
+ }
61
+ if (end < cut.end) {
62
+ replacements.push({ start: end, end: cut.end, quote: source.slice(end, cut.end) });
63
+ }
64
+
65
+ return [...cuts.slice(0, cutIndex), ...replacements, ...cuts.slice(cutIndex + 1)];
66
+ }
67
+
68
+ /** Apply valid source ranges in source order, ignoring stale or overlapping records. */
69
+ export function applyTextCuts(source: string, cuts: readonly TextCut[]): string {
70
+ if (cuts.length === 0) return source;
71
+ let cursor = 0;
72
+ const pieces: string[] = [];
73
+
74
+ for (const cut of cuts.toSorted(
75
+ (left, right) => left.start - right.start || left.end - right.end,
76
+ )) {
77
+ if (cut.start < cursor || cut.start < 0 || cut.end > source.length || cut.end <= cut.start)
78
+ continue;
79
+ if (source.slice(cut.start, cut.end) !== cut.quote) continue;
80
+
81
+ pieces.push(source.slice(cursor, cut.start));
82
+ cursor = cut.end;
83
+ }
84
+ pieces.push(source.slice(cursor));
85
+
86
+ return pieces.join("");
87
+ }
9
88
 
10
89
  /** Chunk of the base source a block occupies (for restore and display). */
11
90
  export function sourceChunk(base: string, block: Block): string {
12
91
  const lines = base.split("\n");
92
+
13
93
  return lines.slice(block.lineStart, block.lineEnd + 1).join("\n");
14
94
  }
15
95
 
@@ -18,10 +98,50 @@ export function cutBlock(working: string, block: Block): string {
18
98
  const lines = working.split("\n");
19
99
  const before = lines.slice(0, block.lineStart);
20
100
  const after = lines.slice(block.lineEnd + 1);
101
+
21
102
  while (before.length && before[before.length - 1]!.trim() === "") before.pop();
103
+
22
104
  return [...before, ...after].join("\n");
23
105
  }
24
106
 
107
+ /** Source interval removed by a whole-block Cut, including its leading blank-line padding. */
108
+ export function blockCutSourceRange(source: string, block: Block) {
109
+ const lines = source.split("\n");
110
+ let startLine = block.lineStart;
111
+
112
+ while (startLine > 0 && lines[startLine - 1]!.trim() === "") startLine--;
113
+ const lineStarts = [0];
114
+
115
+ for (let index = 0; index < source.length; index++) {
116
+ if (source.charCodeAt(index) === 10) lineStarts.push(index + 1);
117
+ }
118
+
119
+ return {
120
+ start: lineStarts[startLine] ?? source.length,
121
+ end: lineStarts[block.lineEnd + 1] ?? source.length,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Remove exactly the selected block-text range while retaining surrounding
127
+ * Markdown markers. A cross-block range also removes the source between its
128
+ * endpoint characters.
129
+ */
130
+ export function cutTextRange(
131
+ working: string,
132
+ startBlock: Block,
133
+ start: number,
134
+ endBlock: Block,
135
+ end: number,
136
+ ): string {
137
+ const sourceStart = sourceOffsetAt(working, startBlock, start) ?? -1;
138
+ const sourceEnd = sourceOffsetAt(working, endBlock, end) ?? -1;
139
+
140
+ if (sourceStart < 0 || sourceEnd <= sourceStart) return working;
141
+
142
+ return working.slice(0, sourceStart) + working.slice(sourceEnd);
143
+ }
144
+
25
145
  /** The line a restored block re-enters at: the next surviving block's start. */
26
146
  export function restoreLine(next: Block | undefined, workingLineCount: number): number {
27
147
  return next ? next.lineStart : workingLineCount;
@@ -34,10 +154,24 @@ export function restoreLine(next: Block | undefined, workingLineCount: number):
34
154
  */
35
155
  function signature(text: string): string {
36
156
  return parseBlocks(text)
37
- .map((b) => b.kind + " " + b.text)
157
+ .map((block) => block.kind + " " + block.text)
38
158
  .join("\0");
39
159
  }
40
160
 
161
+ /**
162
+ * Whether a base block is missing from the working source: the working copy
163
+ * holds fewer blocks with its kind and text than the base does. Only such a
164
+ * block can be restored without duplicating content.
165
+ */
166
+ export function isBlockCut(base: string, working: string, block: Block): boolean {
167
+ const matches = (text: string) =>
168
+ parseBlocks(text).filter(
169
+ (candidate) => candidate.kind === block.kind && candidate.text === block.text,
170
+ ).length;
171
+
172
+ return matches(working) < matches(base);
173
+ }
174
+
41
175
  /**
42
176
  * Re-insert a cut base block into the working source before `beforeLine`.
43
177
  * Returns undefined when the block structure round-trips to the base content
@@ -53,9 +187,11 @@ export function restoreBlock(
53
187
  const lines = working.split("\n");
54
188
  const before = lines.slice(0, beforeLine);
55
189
  const after = lines.slice(beforeLine);
56
- const ins = sourceChunk(base, block).split("\n");
57
- if (before.length && before[before.length - 1]!.trim() !== "") ins.unshift("");
58
- if (after.length && after[0]!.trim() !== "") ins.push("");
59
- const restored = [...before, ...ins, ...after].join("\n");
190
+ const insertedLines = sourceChunk(base, block).split("\n");
191
+
192
+ if (before.length && before[before.length - 1]!.trim() !== "") insertedLines.unshift("");
193
+ if (after.length && after[0]!.trim() !== "") insertedLines.push("");
194
+ const restored = [...before, ...insertedLines, ...after].join("\n");
195
+
60
196
  return signature(restored) === signature(base) ? undefined : restored;
61
197
  }