@plurnk/plurnk-schemes 0.3.0 → 0.4.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/README.md +8 -8
- package/SPEC.md +31 -21
- package/dist/ctx.d.ts +0 -9
- package/dist/ctx.d.ts.map +1 -1
- package/dist/ctx.js +7 -5
- package/dist/ctx.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -6
- package/dist/index.js.map +1 -1
- package/dist/line-marker.d.ts +8 -5
- package/dist/line-marker.d.ts.map +1 -1
- package/dist/line-marker.js +282 -280
- package/dist/line-marker.js.map +1 -1
- package/dist/matcher.d.ts +4 -1
- package/dist/matcher.d.ts.map +1 -1
- package/dist/matcher.js +52 -48
- package/dist/matcher.js.map +1 -1
- package/dist/mimetype-binary.d.ts +6 -4
- package/dist/mimetype-binary.d.ts.map +1 -1
- package/dist/mimetype-binary.js +46 -41
- package/dist/mimetype-binary.js.map +1 -1
- package/dist/path-mimetype.d.ts +4 -1
- package/dist/path-mimetype.d.ts.map +1 -1
- package/dist/path-mimetype.js +27 -25
- package/dist/path-mimetype.js.map +1 -1
- package/dist/resolveForLoop.d.ts +12 -10
- package/dist/resolveForLoop.d.ts.map +1 -1
- package/dist/resolveForLoop.js +28 -26
- package/dist/resolveForLoop.js.map +1 -1
- package/dist/results.d.ts +8 -6
- package/dist/results.d.ts.map +1 -1
- package/dist/results.js +39 -25
- package/dist/results.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/line-marker.js
CHANGED
|
@@ -10,312 +10,314 @@
|
|
|
10
10
|
// <-1> as defined sentinels. Other negatives (<-2>, <-3>) are not
|
|
11
11
|
// specified and rejected as 416. Within a range, -1 as the M endpoint
|
|
12
12
|
// means "include through the last line" (so <1,-1> is whole content).
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
const normalize = (marker, totalLines) => {
|
|
23
|
-
const { first, last } = marker;
|
|
24
|
-
if (last === null) {
|
|
25
|
-
if (first === 0)
|
|
26
|
-
return { kind: "before-first", start: 0, end: 0 };
|
|
27
|
-
if (first === -1)
|
|
28
|
-
return { kind: "after-last", start: 0, end: 0 };
|
|
29
|
-
if (first > 0 && first <= totalLines)
|
|
30
|
-
return { kind: "range", start: first, end: first };
|
|
31
|
-
return { error: `line ${first} out of range (1..${totalLines})` };
|
|
13
|
+
export default class Slicer {
|
|
14
|
+
static #splitLines(content) {
|
|
15
|
+
const trailingNewline = content.endsWith("\n");
|
|
16
|
+
if (content === "")
|
|
17
|
+
return { lines: [], trailingNewline: false };
|
|
18
|
+
const lines = content.split("\n");
|
|
19
|
+
if (trailingNewline)
|
|
20
|
+
lines.pop();
|
|
21
|
+
return { lines, trailingNewline };
|
|
32
22
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return { error: `range start ${first} > end ${last}` };
|
|
45
|
-
return { kind: "range", start: n, end: m };
|
|
46
|
-
};
|
|
47
|
-
// READ a line range. Returns the raw selected lines (no `N:\t` prefix)
|
|
48
|
-
// plus the 1-indexed position of the first selected line. The render
|
|
49
|
-
// layer adds `N:\t` per plurnk.md ("READ output prefixes every line with
|
|
50
|
-
// line numbers, N:\t") starting from `startLine` — keeps numbering as a
|
|
51
|
-
// presentation concern, prevents double-prefixing when the same content
|
|
52
|
-
// passes through the log render.
|
|
53
|
-
//
|
|
54
|
-
// Sentinel positions <0> and <-1> select no content (they're insertion
|
|
55
|
-
// points, not lines) → status 200 with empty text.
|
|
56
|
-
export const sliceLines = (content, marker) => {
|
|
57
|
-
const { lines } = splitLines(content);
|
|
58
|
-
const norm = normalize(marker, lines.length);
|
|
59
|
-
if ("error" in norm)
|
|
60
|
-
return { status: 416, error: norm.error };
|
|
61
|
-
if (norm.kind !== "range")
|
|
62
|
-
return { status: 200, text: "", startLine: undefined };
|
|
63
|
-
const selected = lines.slice(norm.start - 1, norm.end);
|
|
64
|
-
return { status: 200, text: selected.join("\n"), startLine: norm.start };
|
|
65
|
-
};
|
|
66
|
-
// Structural `<L>` slice for JSON sources (plurnk-grammar 0.13.0).
|
|
67
|
-
// "On structured entries, <L> addresses item index, not line number."
|
|
68
|
-
// Every JSON value becomes a list of top-level items:
|
|
69
|
-
// array `[a, b, c]` → items are the array elements
|
|
70
|
-
// object `{k1: v1, ...}` → items are key-value pairs (as single-key objects)
|
|
71
|
-
// scalar `"hello"` / 42 → item is the scalar itself (length-1 list)
|
|
72
|
-
// `<L>` indexes into that list (1-indexed). Result is always a JSON array.
|
|
73
|
-
// Sentinels `<0>` / `<-1>` are insertion points — empty `[]` for READ.
|
|
74
|
-
// Out-of-range positions return 416. Matches the uniform "always JSON
|
|
75
|
-
// array out" shape we settled for matcher results.
|
|
76
|
-
const jsonValueToItems = (parsed) => {
|
|
77
|
-
if (Array.isArray(parsed))
|
|
78
|
-
return parsed;
|
|
79
|
-
if (parsed !== null && typeof parsed === "object") {
|
|
80
|
-
// Object items are single-key {key: value} wrappers, in insertion
|
|
81
|
-
// order. Object.entries preserves spec-guaranteed iteration order
|
|
82
|
-
// for string keys.
|
|
83
|
-
return Object.entries(parsed).map(([k, v]) => ({ [k]: v }));
|
|
84
|
-
}
|
|
85
|
-
// Scalar (string, number, boolean, null): a length-1 list of itself.
|
|
86
|
-
return [parsed];
|
|
87
|
-
};
|
|
88
|
-
export const sliceJsonItems = (content, marker) => {
|
|
89
|
-
let parsed;
|
|
90
|
-
try {
|
|
91
|
-
parsed = JSON.parse(content);
|
|
92
|
-
}
|
|
93
|
-
catch (err) {
|
|
94
|
-
return { status: 400, error: `malformed JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
95
|
-
}
|
|
96
|
-
const items = jsonValueToItems(parsed);
|
|
97
|
-
const total = items.length;
|
|
98
|
-
const { first, last } = marker;
|
|
99
|
-
if (last === null) {
|
|
100
|
-
if (first === 0 || first === -1)
|
|
101
|
-
return { status: 200, body: "[]" };
|
|
102
|
-
if (first > 0 && first <= total)
|
|
103
|
-
return { status: 200, body: JSON.stringify([items[first - 1]], null, 2) };
|
|
104
|
-
return { status: 416, error: `item ${first} out of range (1..${total})` };
|
|
105
|
-
}
|
|
106
|
-
let n = first;
|
|
107
|
-
let m = last;
|
|
108
|
-
if (n === 0)
|
|
109
|
-
n = 1;
|
|
110
|
-
if (m === -1)
|
|
111
|
-
m = total;
|
|
112
|
-
if (n < 1 || n > total)
|
|
113
|
-
return { status: 416, error: `range start ${first} out of range (1..${total})` };
|
|
114
|
-
if (m < 1 || m > total)
|
|
115
|
-
return { status: 416, error: `range end ${last} out of range (1..${total})` };
|
|
116
|
-
if (n > m)
|
|
117
|
-
return { status: 416, error: `range start ${first} > end ${last}` };
|
|
118
|
-
return { status: 200, body: JSON.stringify(items.slice(n - 1, m), null, 2) };
|
|
119
|
-
};
|
|
120
|
-
// Structural `<L>` EDIT for JSON sources (plurnk-grammar 0.13.0/0.14.0).
|
|
121
|
-
// Source-shape rules (matches sliceJsonItems' item definition):
|
|
122
|
-
// array → items are elements
|
|
123
|
-
// object → items are key-value pairs (single-key fragments)
|
|
124
|
-
// scalar → length-1 list of itself; grow markers (<0>,<-1>) reject
|
|
125
|
-
//
|
|
126
|
-
// Body shape (Resolution B):
|
|
127
|
-
// body parses as JSON array → those are the items to splice in
|
|
128
|
-
// body parses as non-array JSON → single item to splice in
|
|
129
|
-
// empty body → delete the selection
|
|
130
|
-
// body fails JSON parse → 400 (path-extension declares intent; honor it)
|
|
131
|
-
//
|
|
132
|
-
// Marker semantics (parallel to line-EDIT):
|
|
133
|
-
// <N> replace item N with body item(s)
|
|
134
|
-
// <N,M> replace items N..M with body item(s)
|
|
135
|
-
// <0> prepend body item(s)
|
|
136
|
-
// <-1> append body item(s)
|
|
137
|
-
// <1,-1> replace whole top-level with body item(s); empty body clears
|
|
138
|
-
// Empty body on a sentinel insertion (<0> or <-1>) → no-op.
|
|
139
|
-
const itemsFromBody = (body) => {
|
|
140
|
-
if (body === "")
|
|
141
|
-
return { items: [] }; // empty body = delete
|
|
142
|
-
let parsed;
|
|
143
|
-
try {
|
|
144
|
-
parsed = JSON.parse(body);
|
|
145
|
-
}
|
|
146
|
-
catch (err) {
|
|
147
|
-
return { error: `malformed JSON body: ${err instanceof Error ? err.message : String(err)}` };
|
|
148
|
-
}
|
|
149
|
-
if (Array.isArray(parsed))
|
|
150
|
-
return { items: parsed };
|
|
151
|
-
return { items: [parsed] };
|
|
152
|
-
};
|
|
153
|
-
const applyJsonArrayEdit = (source, marker, items) => {
|
|
154
|
-
const total = source.length;
|
|
155
|
-
const { first, last } = marker;
|
|
156
|
-
let result;
|
|
157
|
-
if (last === null) {
|
|
158
|
-
if (first === 0)
|
|
159
|
-
result = [...items, ...source];
|
|
160
|
-
else if (first === -1)
|
|
161
|
-
result = [...source, ...items];
|
|
162
|
-
else if (first > 0 && first <= total)
|
|
163
|
-
result = [...source.slice(0, first - 1), ...items, ...source.slice(first)];
|
|
164
|
-
else
|
|
165
|
-
return { status: 416, error: `position ${first} out of range (1..${total})` };
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
23
|
+
static #normalize(marker, totalLines) {
|
|
24
|
+
const { first, last } = marker;
|
|
25
|
+
if (last === null) {
|
|
26
|
+
if (first === 0)
|
|
27
|
+
return { kind: "before-first", start: 0, end: 0 };
|
|
28
|
+
if (first === -1)
|
|
29
|
+
return { kind: "after-last", start: 0, end: 0 };
|
|
30
|
+
if (first > 0 && first <= totalLines)
|
|
31
|
+
return { kind: "range", start: first, end: first };
|
|
32
|
+
return { error: `line ${first} out of range (1..${totalLines})` };
|
|
33
|
+
}
|
|
168
34
|
let n = first;
|
|
169
35
|
let m = last;
|
|
170
36
|
if (n === 0)
|
|
171
37
|
n = 1;
|
|
172
38
|
if (m === -1)
|
|
173
|
-
m =
|
|
174
|
-
if (
|
|
175
|
-
return {
|
|
176
|
-
if (
|
|
177
|
-
return {
|
|
178
|
-
if (m < 1 || (total > 0 && m > total))
|
|
179
|
-
return { status: 416, error: `range end ${last} out of range (1..${total})` };
|
|
39
|
+
m = totalLines;
|
|
40
|
+
if (n < 1 || n > totalLines)
|
|
41
|
+
return { error: `range start ${first} out of range (1..${totalLines})` };
|
|
42
|
+
if (m < 1 || m > totalLines)
|
|
43
|
+
return { error: `range end ${last} out of range (1..${totalLines})` };
|
|
180
44
|
if (n > m)
|
|
181
|
-
return {
|
|
182
|
-
|
|
45
|
+
return { error: `range start ${first} > end ${last}` };
|
|
46
|
+
return { kind: "range", start: n, end: m };
|
|
183
47
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
48
|
+
// READ a line range. Returns the raw selected lines (no `N:\t` prefix)
|
|
49
|
+
// plus the 1-indexed position of the first selected line. The render
|
|
50
|
+
// layer adds `N:\t` per plurnk.md ("READ output prefixes every line with
|
|
51
|
+
// line numbers, N:\t") starting from `startLine` — keeps numbering as a
|
|
52
|
+
// presentation concern, prevents double-prefixing when the same content
|
|
53
|
+
// passes through the log render.
|
|
54
|
+
//
|
|
55
|
+
// Sentinel positions <0> and <-1> select no content (they're insertion
|
|
56
|
+
// points, not lines) → status 200 with empty text.
|
|
57
|
+
static lines(content, marker) {
|
|
58
|
+
const { lines } = Slicer.#splitLines(content);
|
|
59
|
+
const norm = Slicer.#normalize(marker, lines.length);
|
|
60
|
+
if ("error" in norm)
|
|
61
|
+
return { status: 416, error: norm.error };
|
|
62
|
+
if (norm.kind !== "range")
|
|
63
|
+
return { status: 200, text: "", startLine: undefined };
|
|
64
|
+
const selected = lines.slice(norm.start - 1, norm.end);
|
|
65
|
+
return { status: 200, text: selected.join("\n"), startLine: norm.start };
|
|
197
66
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return
|
|
67
|
+
// Structural `<L>` slice for JSON sources (plurnk-grammar 0.13.0).
|
|
68
|
+
// "On structured entries, <L> addresses item index, not line number."
|
|
69
|
+
// Every JSON value becomes a list of top-level items:
|
|
70
|
+
// array `[a, b, c]` → items are the array elements
|
|
71
|
+
// object `{k1: v1, ...}` → items are key-value pairs (as single-key objects)
|
|
72
|
+
// scalar `"hello"` / 42 → item is the scalar itself (length-1 list)
|
|
73
|
+
// `<L>` indexes into that list (1-indexed). Result is always a JSON array.
|
|
74
|
+
// Sentinels `<0>` / `<-1>` are insertion points — empty `[]` for READ.
|
|
75
|
+
// Out-of-range positions return 416. Matches the uniform "always JSON
|
|
76
|
+
// array out" shape we settled for matcher results.
|
|
77
|
+
static #jsonValueToItems(parsed) {
|
|
78
|
+
if (Array.isArray(parsed))
|
|
79
|
+
return parsed;
|
|
80
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
81
|
+
// Object items are single-key {key: value} wrappers, in insertion
|
|
82
|
+
// order. Object.entries preserves spec-guaranteed iteration order
|
|
83
|
+
// for string keys.
|
|
84
|
+
return Object.entries(parsed).map(([k, v]) => ({ [k]: v }));
|
|
85
|
+
}
|
|
86
|
+
// Scalar (string, number, boolean, null): a length-1 list of itself.
|
|
87
|
+
return [parsed];
|
|
211
88
|
}
|
|
212
|
-
|
|
89
|
+
static jsonItems(content, marker) {
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(content);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
return { status: 400, error: `malformed JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
96
|
+
}
|
|
97
|
+
const items = Slicer.#jsonValueToItems(parsed);
|
|
98
|
+
const total = items.length;
|
|
99
|
+
const { first, last } = marker;
|
|
100
|
+
if (last === null) {
|
|
101
|
+
if (first === 0 || first === -1)
|
|
102
|
+
return { status: 200, body: "[]" };
|
|
103
|
+
if (first > 0 && first <= total)
|
|
104
|
+
return { status: 200, body: JSON.stringify([items[first - 1]], null, 2) };
|
|
105
|
+
return { status: 416, error: `item ${first} out of range (1..${total})` };
|
|
106
|
+
}
|
|
213
107
|
let n = first;
|
|
214
108
|
let m = last;
|
|
215
109
|
if (n === 0)
|
|
216
110
|
n = 1;
|
|
217
111
|
if (m === -1)
|
|
218
112
|
m = total;
|
|
219
|
-
if (
|
|
220
|
-
return { status: 416, error: `range on empty object` };
|
|
221
|
-
if (n < 1 || (total > 0 && n > total))
|
|
113
|
+
if (n < 1 || n > total)
|
|
222
114
|
return { status: 416, error: `range start ${first} out of range (1..${total})` };
|
|
223
|
-
if (m < 1 ||
|
|
115
|
+
if (m < 1 || m > total)
|
|
224
116
|
return { status: 416, error: `range end ${last} out of range (1..${total})` };
|
|
225
117
|
if (n > m)
|
|
226
118
|
return { status: 416, error: `range start ${first} > end ${last}` };
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
return { status: 200, result: JSON.stringify(Object.fromEntries(result), null, 2) };
|
|
230
|
-
};
|
|
231
|
-
const applyJsonScalarEdit = (source, marker, items) => {
|
|
232
|
-
// Scalar source is a length-1 list of itself. Only `<1>` replace
|
|
233
|
-
// works cleanly; grow markers (<0>,<-1>) and ranges that imply
|
|
234
|
-
// growth/delete would require type promotion (scalar → array),
|
|
235
|
-
// which is the kind of implicit magic that bites later. Reject.
|
|
236
|
-
const { first, last } = marker;
|
|
237
|
-
if (last === null && first === 1) {
|
|
238
|
-
if (items.length === 0)
|
|
239
|
-
return { status: 200, result: "null" }; // delete the scalar
|
|
240
|
-
if (items.length === 1)
|
|
241
|
-
return { status: 200, result: JSON.stringify(items[0], null, 2) };
|
|
242
|
-
return { status: 400, error: "scalar source: <1> body must produce 0 or 1 items (no implicit promotion to array)" };
|
|
119
|
+
return { status: 200, body: JSON.stringify(items.slice(n - 1, m), null, 2) };
|
|
243
120
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
121
|
+
// Structural `<L>` EDIT for JSON sources (plurnk-grammar 0.13.0/0.14.0).
|
|
122
|
+
// Source-shape rules (matches jsonItems' item definition):
|
|
123
|
+
// array → items are elements
|
|
124
|
+
// object → items are key-value pairs (single-key fragments)
|
|
125
|
+
// scalar → length-1 list of itself; grow markers (<0>,<-1>) reject
|
|
126
|
+
//
|
|
127
|
+
// Body shape (Resolution B):
|
|
128
|
+
// body parses as JSON array → those are the items to splice in
|
|
129
|
+
// body parses as non-array JSON → single item to splice in
|
|
130
|
+
// empty body → delete the selection
|
|
131
|
+
// body fails JSON parse → 400 (path-extension declares intent; honor it)
|
|
132
|
+
//
|
|
133
|
+
// Marker semantics (parallel to line-EDIT):
|
|
134
|
+
// <N> replace item N with body item(s)
|
|
135
|
+
// <N,M> replace items N..M with body item(s)
|
|
136
|
+
// <0> prepend body item(s)
|
|
137
|
+
// <-1> append body item(s)
|
|
138
|
+
// <1,-1> replace whole top-level with body item(s); empty body clears
|
|
139
|
+
// Empty body on a sentinel insertion (<0> or <-1>) → no-op.
|
|
140
|
+
static #itemsFromBody(body) {
|
|
141
|
+
if (body === "")
|
|
142
|
+
return { items: [] }; // empty body = delete
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = JSON.parse(body);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return { error: `malformed JSON body: ${err instanceof Error ? err.message : String(err)}` };
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(parsed))
|
|
151
|
+
return { items: parsed };
|
|
152
|
+
return { items: [parsed] };
|
|
251
153
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
154
|
+
static #applyJsonArrayEdit(source, marker, items) {
|
|
155
|
+
const total = source.length;
|
|
156
|
+
const { first, last } = marker;
|
|
157
|
+
let result;
|
|
158
|
+
if (last === null) {
|
|
159
|
+
if (first === 0)
|
|
160
|
+
result = [...items, ...source];
|
|
161
|
+
else if (first === -1)
|
|
162
|
+
result = [...source, ...items];
|
|
163
|
+
else if (first > 0 && first <= total)
|
|
164
|
+
result = [...source.slice(0, first - 1), ...items, ...source.slice(first)];
|
|
165
|
+
else
|
|
166
|
+
return { status: 416, error: `position ${first} out of range (1..${total})` };
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
let n = first;
|
|
170
|
+
let m = last;
|
|
171
|
+
if (n === 0)
|
|
172
|
+
n = 1;
|
|
173
|
+
if (m === -1)
|
|
174
|
+
m = total;
|
|
175
|
+
if (total === 0 && (first !== 1 || last !== -1))
|
|
176
|
+
return { status: 416, error: `range on empty array` };
|
|
177
|
+
if (n < 1 || (total > 0 && n > total))
|
|
178
|
+
return { status: 416, error: `range start ${first} out of range (1..${total})` };
|
|
179
|
+
if (m < 1 || (total > 0 && m > total))
|
|
180
|
+
return { status: 416, error: `range end ${last} out of range (1..${total})` };
|
|
181
|
+
if (n > m)
|
|
182
|
+
return { status: 416, error: `range start ${first} > end ${last}` };
|
|
183
|
+
result = [...source.slice(0, n - 1), ...items, ...source.slice(m)];
|
|
184
|
+
}
|
|
185
|
+
return { status: 200, result: JSON.stringify(result, null, 2) };
|
|
258
186
|
}
|
|
259
|
-
|
|
260
|
-
|
|
187
|
+
static #applyJsonObjectEdit(source, marker, items) {
|
|
188
|
+
// Object items are key-value pairs. Body items must be objects;
|
|
189
|
+
// each object's entries become kv-pairs to splice in. Items that
|
|
190
|
+
// aren't single objects → 400 (model used wrong body shape for an
|
|
191
|
+
// object source).
|
|
192
|
+
const bodyEntries = [];
|
|
193
|
+
for (const item of items) {
|
|
194
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
195
|
+
return { status: 400, error: "object source requires body items to be JSON objects (key-value pairs)" };
|
|
196
|
+
}
|
|
197
|
+
bodyEntries.push(...Object.entries(item));
|
|
198
|
+
}
|
|
199
|
+
const entries = Object.entries(source);
|
|
200
|
+
const total = entries.length;
|
|
201
|
+
const { first, last } = marker;
|
|
202
|
+
let result;
|
|
203
|
+
if (last === null) {
|
|
204
|
+
if (first === 0)
|
|
205
|
+
result = [...bodyEntries, ...entries];
|
|
206
|
+
else if (first === -1)
|
|
207
|
+
result = [...entries, ...bodyEntries];
|
|
208
|
+
else if (first > 0 && first <= total)
|
|
209
|
+
result = [...entries.slice(0, first - 1), ...bodyEntries, ...entries.slice(first)];
|
|
210
|
+
else
|
|
211
|
+
return { status: 416, error: `position ${first} out of range (1..${total})` };
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
let n = first;
|
|
215
|
+
let m = last;
|
|
216
|
+
if (n === 0)
|
|
217
|
+
n = 1;
|
|
218
|
+
if (m === -1)
|
|
219
|
+
m = total;
|
|
220
|
+
if (total === 0 && (first !== 1 || last !== -1))
|
|
221
|
+
return { status: 416, error: `range on empty object` };
|
|
222
|
+
if (n < 1 || (total > 0 && n > total))
|
|
223
|
+
return { status: 416, error: `range start ${first} out of range (1..${total})` };
|
|
224
|
+
if (m < 1 || (total > 0 && m > total))
|
|
225
|
+
return { status: 416, error: `range end ${last} out of range (1..${total})` };
|
|
226
|
+
if (n > m)
|
|
227
|
+
return { status: 416, error: `range start ${first} > end ${last}` };
|
|
228
|
+
result = [...entries.slice(0, n - 1), ...bodyEntries, ...entries.slice(m)];
|
|
229
|
+
}
|
|
230
|
+
return { status: 200, result: JSON.stringify(Object.fromEntries(result), null, 2) };
|
|
261
231
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
232
|
+
static #applyJsonScalarEdit(source, marker, items) {
|
|
233
|
+
// Scalar source is a length-1 list of itself. Only `<1>` replace
|
|
234
|
+
// works cleanly; grow markers (<0>,<-1>) and ranges that imply
|
|
235
|
+
// growth/delete would require type promotion (scalar → array),
|
|
236
|
+
// which is the kind of implicit magic that bites later. Reject.
|
|
237
|
+
const { first, last } = marker;
|
|
238
|
+
if (last === null && first === 1) {
|
|
239
|
+
if (items.length === 0)
|
|
240
|
+
return { status: 200, result: "null" }; // delete the scalar
|
|
241
|
+
if (items.length === 1)
|
|
242
|
+
return { status: 200, result: JSON.stringify(items[0], null, 2) };
|
|
243
|
+
return { status: 400, error: "scalar source: <1> body must produce 0 or 1 items (no implicit promotion to array)" };
|
|
244
|
+
}
|
|
245
|
+
if (last === -1 && first === 1) {
|
|
246
|
+
// <1,-1> = whole content. Same constraints as <1> for scalars.
|
|
247
|
+
if (items.length === 0)
|
|
248
|
+
return { status: 200, result: "null" };
|
|
249
|
+
if (items.length === 1)
|
|
250
|
+
return { status: 200, result: JSON.stringify(items[0], null, 2) };
|
|
251
|
+
return { status: 400, error: "scalar source: <1,-1> body must produce 0 or 1 items (no implicit promotion to array)" };
|
|
252
|
+
}
|
|
253
|
+
return { status: 400, error: "scalar JSON source: only <1> or <1,-1> markers supported (no implicit promotion to array via grow markers)" };
|
|
270
254
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// EDIT applies body at the marker position:
|
|
294
|
-
// <0> prepend body before line 1
|
|
295
|
-
// <-1> append body after the last line
|
|
296
|
-
// <N> replace line N with body
|
|
297
|
-
// <N,M> replace lines N..M with body
|
|
298
|
-
// <1,-1> whole content (replace everything); empty body clears.
|
|
299
|
-
// Empty body with <N>/<N,M> deletes those lines.
|
|
300
|
-
export const applyLineMarkerEdit = (content, marker, body) => {
|
|
301
|
-
const { lines, trailingNewline } = splitLines(content);
|
|
302
|
-
const norm = normalize(marker, lines.length);
|
|
303
|
-
if ("error" in norm)
|
|
304
|
-
return { status: 416, error: norm.error };
|
|
305
|
-
const bodyLines = splitLines(body).lines;
|
|
306
|
-
let newLines;
|
|
307
|
-
if (norm.kind === "before-first") {
|
|
308
|
-
newLines = [...bodyLines, ...lines];
|
|
255
|
+
static jsonItemEdit(content, marker, body) {
|
|
256
|
+
let parsed;
|
|
257
|
+
try {
|
|
258
|
+
parsed = JSON.parse(content);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
return { status: 400, error: `malformed JSON source: ${err instanceof Error ? err.message : String(err)}` };
|
|
262
|
+
}
|
|
263
|
+
const bodyResult = Slicer.#itemsFromBody(body);
|
|
264
|
+
if ("error" in bodyResult)
|
|
265
|
+
return { status: 400, error: bodyResult.error };
|
|
266
|
+
const items = bodyResult.items;
|
|
267
|
+
// Empty-body sentinel insertion is a no-op (model accidentally
|
|
268
|
+
// emitted no items at an insertion point).
|
|
269
|
+
if (items.length === 0 && marker.last === null && (marker.first === 0 || marker.first === -1)) {
|
|
270
|
+
return { status: 200, result: content };
|
|
271
|
+
}
|
|
272
|
+
if (Array.isArray(parsed))
|
|
273
|
+
return Slicer.#applyJsonArrayEdit(parsed, marker, items);
|
|
274
|
+
if (parsed !== null && typeof parsed === "object")
|
|
275
|
+
return Slicer.#applyJsonObjectEdit(parsed, marker, items);
|
|
276
|
+
return Slicer.#applyJsonScalarEdit(parsed, marker, items);
|
|
309
277
|
}
|
|
310
|
-
|
|
311
|
-
|
|
278
|
+
// COPY-style raw line slice. Returns the selected lines verbatim (no line-
|
|
279
|
+
// number prefix), trailing newline appended if any lines were selected.
|
|
280
|
+
// Used for COPY/MOVE `<L>` per SPEC.md §16.9 (source range, symmetric
|
|
281
|
+
// with READ but without the READ-output prefix that's a render concern,
|
|
282
|
+
// not a data concern).
|
|
283
|
+
static linesRaw(content, marker) {
|
|
284
|
+
const { lines } = Slicer.#splitLines(content);
|
|
285
|
+
const norm = Slicer.#normalize(marker, lines.length);
|
|
286
|
+
if ("error" in norm)
|
|
287
|
+
return { status: 416, error: norm.error };
|
|
288
|
+
if (norm.kind !== "range")
|
|
289
|
+
return { status: 200, text: "" };
|
|
290
|
+
const selected = lines.slice(norm.start - 1, norm.end);
|
|
291
|
+
const result = selected.length > 0 ? `${selected.join("\n")}\n` : "";
|
|
292
|
+
return { status: 200, text: result };
|
|
312
293
|
}
|
|
313
|
-
|
|
314
|
-
|
|
294
|
+
// EDIT applies body at the marker position:
|
|
295
|
+
// <0> prepend body before line 1
|
|
296
|
+
// <-1> append body after the last line
|
|
297
|
+
// <N> replace line N with body
|
|
298
|
+
// <N,M> replace lines N..M with body
|
|
299
|
+
// <1,-1> whole content (replace everything); empty body clears.
|
|
300
|
+
// Empty body with <N>/<N,M> deletes those lines.
|
|
301
|
+
static lineMarkerEdit(content, marker, body) {
|
|
302
|
+
const { lines, trailingNewline } = Slicer.#splitLines(content);
|
|
303
|
+
const norm = Slicer.#normalize(marker, lines.length);
|
|
304
|
+
if ("error" in norm)
|
|
305
|
+
return { status: 416, error: norm.error };
|
|
306
|
+
const bodyLines = Slicer.#splitLines(body).lines;
|
|
307
|
+
let newLines;
|
|
308
|
+
if (norm.kind === "before-first") {
|
|
309
|
+
newLines = [...bodyLines, ...lines];
|
|
310
|
+
}
|
|
311
|
+
else if (norm.kind === "after-last") {
|
|
312
|
+
newLines = [...lines, ...bodyLines];
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
newLines = [...lines.slice(0, norm.start - 1), ...bodyLines, ...lines.slice(norm.end)];
|
|
316
|
+
}
|
|
317
|
+
let result = newLines.join("\n");
|
|
318
|
+
if (newLines.length > 0 && trailingNewline)
|
|
319
|
+
result += "\n";
|
|
320
|
+
return { status: 200, result };
|
|
315
321
|
}
|
|
316
|
-
|
|
317
|
-
if (newLines.length > 0 && trailingNewline)
|
|
318
|
-
result += "\n";
|
|
319
|
-
return { status: 200, result };
|
|
320
|
-
};
|
|
322
|
+
}
|
|
321
323
|
//# sourceMappingURL=line-marker.js.map
|