@pieceful/ravel-projection 0.2.0
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/LICENSE +21 -0
- package/README.md +156 -0
- package/package.json +40 -0
- package/src/builder.js +533 -0
- package/src/context.js +108 -0
- package/src/index.d.ts +331 -0
- package/src/index.js +68 -0
- package/src/internal.js +163 -0
- package/src/line-index.js +130 -0
- package/src/mapping.js +369 -0
- package/src/service.js +384 -0
- package/src/transforms.d.ts +29 -0
- package/src/transforms.js +563 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import { createProjectionId, createVirtualUri } from "./builder.js";
|
|
2
|
+
import { createLineIndex, offsetAt } from "./line-index.js";
|
|
3
|
+
import { buildProjectionIndexes, coalesceProjectionSegments } from "./mapping.js";
|
|
4
|
+
import {
|
|
5
|
+
clone,
|
|
6
|
+
deepFreeze,
|
|
7
|
+
defaultCapabilities,
|
|
8
|
+
isInteger,
|
|
9
|
+
projectionStages,
|
|
10
|
+
sourceLocation,
|
|
11
|
+
sourceRangeOffsets,
|
|
12
|
+
stableHash,
|
|
13
|
+
throwIfAborted,
|
|
14
|
+
validOffsetRange
|
|
15
|
+
} from "./internal.js";
|
|
16
|
+
|
|
17
|
+
const modes = new Set(["copy", "mapped", "inserted", "removed"]);
|
|
18
|
+
|
|
19
|
+
const span = (inputStart, inputEnd, outputStart, outputEnd, mode = "copy") => ({
|
|
20
|
+
input: { start: inputStart, end: inputEnd },
|
|
21
|
+
output: { start: outputStart, end: outputEnd },
|
|
22
|
+
mode
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const appendSpan = (spans, entry) => {
|
|
26
|
+
if (!validOffsetRange(entry.input) || !validOffsetRange(entry.output) || !modes.has(entry.mode)) return;
|
|
27
|
+
const previous = spans.at(-1);
|
|
28
|
+
if (previous && previous.mode === entry.mode &&
|
|
29
|
+
previous.input.end === entry.input.start && previous.output.end === entry.output.start &&
|
|
30
|
+
// Do not merge a deletion and insertion pair at the same anchor.
|
|
31
|
+
(entry.mode === "copy" || entry.mode === "mapped")) {
|
|
32
|
+
previous.input.end = entry.input.end;
|
|
33
|
+
previous.output.end = entry.output.end;
|
|
34
|
+
} else {
|
|
35
|
+
spans.push(entry);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const finalizeMap = (name, inputLength, outputLength, spans) => deepFreeze({
|
|
40
|
+
kind: "offset",
|
|
41
|
+
name,
|
|
42
|
+
inputLength,
|
|
43
|
+
outputLength,
|
|
44
|
+
spans
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const identityTransformMap = (input) => {
|
|
48
|
+
const length = typeof input === "string" ? input.length : input;
|
|
49
|
+
if (!isInteger(length) || length < 0) return deepFreeze({ kind: "invalid", reason: "invalid-length" });
|
|
50
|
+
return finalizeMap("identity", length, length, length ? [span(0, length, 0, length)] : []);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const createIndentOffsetMap = (input, count = 2) => {
|
|
54
|
+
if (typeof input !== "string" || !isInteger(count) || count < 0) {
|
|
55
|
+
return deepFreeze({ ok: false, reason: "invalid-indent" });
|
|
56
|
+
}
|
|
57
|
+
if (count === 0) return deepFreeze({ ok: true, text: input, map: identityTransformMap(input) });
|
|
58
|
+
const padding = " ".repeat(count);
|
|
59
|
+
const spans = [];
|
|
60
|
+
let output = "";
|
|
61
|
+
let inputCursor = 0;
|
|
62
|
+
for (const line of input.split("\n")) {
|
|
63
|
+
const lineStart = inputCursor;
|
|
64
|
+
const lineEnd = lineStart + line.length;
|
|
65
|
+
if (line.length) {
|
|
66
|
+
const outputStart = output.length;
|
|
67
|
+
output += padding;
|
|
68
|
+
appendSpan(spans, span(lineStart, lineStart, outputStart, output.length, "inserted"));
|
|
69
|
+
}
|
|
70
|
+
if (line.length) {
|
|
71
|
+
const outputStart = output.length;
|
|
72
|
+
output += line;
|
|
73
|
+
appendSpan(spans, span(lineStart, lineEnd, outputStart, output.length));
|
|
74
|
+
}
|
|
75
|
+
inputCursor = lineEnd;
|
|
76
|
+
if (inputCursor < input.length) {
|
|
77
|
+
const outputStart = output.length;
|
|
78
|
+
output += "\n";
|
|
79
|
+
appendSpan(spans, span(inputCursor, inputCursor + 1, outputStart, output.length));
|
|
80
|
+
inputCursor += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return deepFreeze({ ok: true, text: output, map: finalizeMap("indent", input.length, output.length, spans) });
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const createDedentOffsetMap = (input) => {
|
|
87
|
+
if (typeof input !== "string") return deepFreeze({ ok: false, reason: "invalid-input" });
|
|
88
|
+
const lines = input.split("\n");
|
|
89
|
+
const indents = lines.filter((line) => /\S/.test(line)).map((line) => /^\s*/.exec(line)[0].length);
|
|
90
|
+
const amount = indents.length ? Math.min(...indents) : 0;
|
|
91
|
+
if (amount === 0) return deepFreeze({ ok: true, text: input, amount, map: identityTransformMap(input) });
|
|
92
|
+
const spans = [];
|
|
93
|
+
let output = "";
|
|
94
|
+
let inputCursor = 0;
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const lineStart = inputCursor;
|
|
97
|
+
const lineEnd = lineStart + line.length;
|
|
98
|
+
const removed = Math.min(amount, line.length);
|
|
99
|
+
if (removed) appendSpan(spans, span(lineStart, lineStart + removed, output.length, output.length, "removed"));
|
|
100
|
+
if (line.length > removed) {
|
|
101
|
+
const outputStart = output.length;
|
|
102
|
+
output += line.slice(removed);
|
|
103
|
+
appendSpan(spans, span(lineStart + removed, lineEnd, outputStart, output.length));
|
|
104
|
+
}
|
|
105
|
+
inputCursor = lineEnd;
|
|
106
|
+
if (inputCursor < input.length) {
|
|
107
|
+
const outputStart = output.length;
|
|
108
|
+
output += "\n";
|
|
109
|
+
appendSpan(spans, span(inputCursor, inputCursor + 1, outputStart, output.length));
|
|
110
|
+
inputCursor += 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return deepFreeze({ ok: true, text: output, amount, map: finalizeMap("dedent", input.length, output.length, spans) });
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const createEolOffsetMap = (input, eol = "\n") => {
|
|
117
|
+
if (typeof input !== "string" || (eol !== "\n" && eol !== "\r\n")) {
|
|
118
|
+
return deepFreeze({ ok: false, reason: "invalid-eol" });
|
|
119
|
+
}
|
|
120
|
+
const spans = [];
|
|
121
|
+
let output = "";
|
|
122
|
+
let index = 0;
|
|
123
|
+
while (index < input.length) {
|
|
124
|
+
if (input[index] !== "\r" && input[index] !== "\n") {
|
|
125
|
+
const start = index;
|
|
126
|
+
while (index < input.length && input[index] !== "\r" && input[index] !== "\n") index += 1;
|
|
127
|
+
const outputStart = output.length;
|
|
128
|
+
output += input.slice(start, index);
|
|
129
|
+
appendSpan(spans, span(start, index, outputStart, output.length));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const newlineStart = index;
|
|
133
|
+
const consumed = input[index] === "\r" && input[index + 1] === "\n" ? 2 : 1;
|
|
134
|
+
const original = input.slice(index, index + consumed);
|
|
135
|
+
const outputStart = output.length;
|
|
136
|
+
output += eol;
|
|
137
|
+
if (original === eol) {
|
|
138
|
+
appendSpan(spans, span(index, index + consumed, outputStart, output.length));
|
|
139
|
+
} else if (consumed === 2 && eol === "\n") {
|
|
140
|
+
appendSpan(spans, span(index, index + 1, outputStart, outputStart, "removed"));
|
|
141
|
+
appendSpan(spans, span(index + 1, index + 2, outputStart, output.length));
|
|
142
|
+
} else if (consumed === 1 && eol === "\r\n") {
|
|
143
|
+
appendSpan(spans, span(index, index, outputStart, outputStart + 1, "inserted"));
|
|
144
|
+
appendSpan(spans, span(index, index + 1, outputStart + 1, output.length, "mapped"));
|
|
145
|
+
} else {
|
|
146
|
+
appendSpan(spans, span(index, index + consumed, outputStart, output.length, "mapped"));
|
|
147
|
+
}
|
|
148
|
+
index = newlineStart + consumed;
|
|
149
|
+
}
|
|
150
|
+
return deepFreeze({ ok: true, text: output, map: finalizeMap("normalize-eol", input.length, output.length, spans) });
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const validateTransformMapping = (mapping) => {
|
|
154
|
+
if (mapping?.kind === "identity") return deepFreeze({ ok: true });
|
|
155
|
+
if (mapping?.kind === "opaque") return deepFreeze({ ok: true });
|
|
156
|
+
if (mapping?.kind === "source-map" && Array.isArray(mapping.entries)) return deepFreeze({ ok: true });
|
|
157
|
+
if (mapping?.kind !== "offset" || !isInteger(mapping.inputLength) || mapping.inputLength < 0 ||
|
|
158
|
+
!isInteger(mapping.outputLength) || mapping.outputLength < 0 || !Array.isArray(mapping.spans)) {
|
|
159
|
+
return deepFreeze({ ok: false, reason: "invalid-transform-map" });
|
|
160
|
+
}
|
|
161
|
+
let lastInput = 0;
|
|
162
|
+
let lastOutput = 0;
|
|
163
|
+
for (const [index, entry] of mapping.spans.entries()) {
|
|
164
|
+
if (!validOffsetRange(entry.input) || !validOffsetRange(entry.output) || !modes.has(entry.mode) ||
|
|
165
|
+
entry.input.end > mapping.inputLength || entry.output.end > mapping.outputLength ||
|
|
166
|
+
entry.input.start < lastInput || entry.output.start < lastOutput ||
|
|
167
|
+
((entry.mode === "copy" || entry.mode === "mapped") &&
|
|
168
|
+
entry.input.end - entry.input.start !== entry.output.end - entry.output.start) ||
|
|
169
|
+
(entry.mode === "inserted" && entry.input.start !== entry.input.end) ||
|
|
170
|
+
(entry.mode === "removed" && entry.output.start !== entry.output.end)) {
|
|
171
|
+
return deepFreeze({ ok: false, reason: "invalid-transform-span", index });
|
|
172
|
+
}
|
|
173
|
+
lastInput = entry.input.end;
|
|
174
|
+
lastOutput = entry.output.end;
|
|
175
|
+
}
|
|
176
|
+
return deepFreeze({ ok: true });
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const characterOrigins = (mapping) => {
|
|
180
|
+
const origins = Array(mapping.outputLength).fill(null);
|
|
181
|
+
const mapped = Array(mapping.outputLength).fill(false);
|
|
182
|
+
for (const entry of mapping.spans) {
|
|
183
|
+
if (entry.mode !== "copy" && entry.mode !== "mapped") continue;
|
|
184
|
+
for (let offset = 0; offset < entry.output.end - entry.output.start; offset += 1) {
|
|
185
|
+
origins[entry.output.start + offset] = entry.input.start + offset;
|
|
186
|
+
mapped[entry.output.start + offset] = entry.mode === "mapped";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { origins, mapped };
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const mapFromOrigins = (name, inputLength, origins, mappedFlags) => {
|
|
193
|
+
const spans = [];
|
|
194
|
+
let output = 0;
|
|
195
|
+
while (output < origins.length) {
|
|
196
|
+
const origin = origins[output];
|
|
197
|
+
const mode = origin === null ? "inserted" : mappedFlags[output] ? "mapped" : "copy";
|
|
198
|
+
let end = output + 1;
|
|
199
|
+
while (end < origins.length) {
|
|
200
|
+
const nextOrigin = origins[end];
|
|
201
|
+
const nextMode = nextOrigin === null ? "inserted" : mappedFlags[end] ? "mapped" : "copy";
|
|
202
|
+
if (nextMode !== mode || (mode !== "inserted" && nextOrigin !== origin + end - output)) break;
|
|
203
|
+
end += 1;
|
|
204
|
+
}
|
|
205
|
+
let anchor = origin;
|
|
206
|
+
if (mode === "inserted") {
|
|
207
|
+
const previous = output > 0 ? origins[output - 1] : null;
|
|
208
|
+
const next = origins.slice(end).find((candidate) => candidate !== null);
|
|
209
|
+
anchor = previous !== null ? previous + 1 : next ?? inputLength;
|
|
210
|
+
}
|
|
211
|
+
appendSpan(spans, mode === "inserted"
|
|
212
|
+
? span(anchor, anchor, output, end, mode)
|
|
213
|
+
: span(origin, origin + end - output, output, end, mode));
|
|
214
|
+
output = end;
|
|
215
|
+
}
|
|
216
|
+
return finalizeMap(name, inputLength, origins.length, spans);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const composeOffsetMaps = (...inputMaps) => {
|
|
220
|
+
const maps = inputMaps.length === 1 && Array.isArray(inputMaps[0]) ? inputMaps[0] : inputMaps;
|
|
221
|
+
if (!maps.length) return deepFreeze({ ok: false, reason: "no-transform-maps" });
|
|
222
|
+
if (maps.some((mapping) => !validateTransformMapping(mapping).ok || mapping.kind !== "offset")) {
|
|
223
|
+
return deepFreeze({ ok: false, reason: "incompatible-transform-map" });
|
|
224
|
+
}
|
|
225
|
+
let current = maps[0];
|
|
226
|
+
for (const next of maps.slice(1)) {
|
|
227
|
+
if (current.outputLength !== next.inputLength) {
|
|
228
|
+
return deepFreeze({ ok: false, reason: "transform-length-mismatch" });
|
|
229
|
+
}
|
|
230
|
+
const first = characterOrigins(current);
|
|
231
|
+
const second = characterOrigins(next);
|
|
232
|
+
const origins = [];
|
|
233
|
+
const mapped = [];
|
|
234
|
+
for (let output = 0; output < next.outputLength; output += 1) {
|
|
235
|
+
const middle = second.origins[output];
|
|
236
|
+
const origin = middle === null ? null : first.origins[middle];
|
|
237
|
+
origins.push(origin);
|
|
238
|
+
mapped.push(second.mapped[output] || (middle !== null && first.mapped[middle]));
|
|
239
|
+
}
|
|
240
|
+
current = mapFromOrigins(`${current.name}+${next.name}`, current.inputLength, origins, mapped);
|
|
241
|
+
}
|
|
242
|
+
return deepFreeze({ ok: true, map: current });
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const pointFromSpan = (entry, offset, direction, fromOutput) => {
|
|
246
|
+
const from = fromOutput ? entry.output : entry.input;
|
|
247
|
+
const to = fromOutput ? entry.input : entry.output;
|
|
248
|
+
if (entry.mode === "inserted" || entry.mode === "removed") return undefined;
|
|
249
|
+
if (offset > from.start && offset < from.end) return to.start + offset - from.start;
|
|
250
|
+
if (offset === from.start && direction !== "left") return to.start;
|
|
251
|
+
if (offset === from.end && direction !== "right") return to.end;
|
|
252
|
+
return undefined;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export const mapTransformOffset = (mapping, offset, { direction = "output-to-input", affinity = "none" } = {}) => {
|
|
256
|
+
if (!validateTransformMapping(mapping).ok || mapping.kind !== "offset" || !isInteger(offset) || offset < 0) {
|
|
257
|
+
return deepFreeze({ ok: false, reason: "invalid-position", matches: [] });
|
|
258
|
+
}
|
|
259
|
+
const fromOutput = direction === "output-to-input";
|
|
260
|
+
const length = fromOutput ? mapping.outputLength : mapping.inputLength;
|
|
261
|
+
if (offset > length) return deepFreeze({ ok: false, reason: "invalid-position", matches: [] });
|
|
262
|
+
const matches = [];
|
|
263
|
+
for (const entry of mapping.spans) {
|
|
264
|
+
const mapped = pointFromSpan(entry, offset, affinity, fromOutput);
|
|
265
|
+
if (mapped !== undefined && !matches.includes(mapped)) matches.push(mapped);
|
|
266
|
+
}
|
|
267
|
+
return deepFreeze({ ok: true, matches });
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export const opaqueTransformMap = (anchor) => deepFreeze({
|
|
271
|
+
kind: "opaque",
|
|
272
|
+
anchor: sourceLocation(anchor)
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
export const stageCapabilities = (stage, overrides = {}) => {
|
|
276
|
+
if (!projectionStages.has(stage)) {
|
|
277
|
+
throw new TypeError(`Unknown projection stage: ${String(stage)}`);
|
|
278
|
+
}
|
|
279
|
+
return deepFreeze({
|
|
280
|
+
...defaultCapabilities(stage),
|
|
281
|
+
...overrides
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const validateAnalysisTransform = (descriptor) => {
|
|
286
|
+
if (!descriptor || typeof descriptor !== "object") return deepFreeze({ ok: false, reason: "invalid-transform" });
|
|
287
|
+
if (descriptor.pure !== true) return deepFreeze({ ok: false, reason: "transform-not-declared-pure" });
|
|
288
|
+
if ((descriptor.effects?.length ?? 0) > 0 || (descriptor.authorities?.length ?? 0) > 0 || descriptor.effect) {
|
|
289
|
+
return deepFreeze({ ok: false, reason: "analysis-transform-requested-effect" });
|
|
290
|
+
}
|
|
291
|
+
const mapping = descriptor.mapping ?? descriptor.map;
|
|
292
|
+
return validateTransformMapping(mapping?.kind === "identity" ? mapping : mapping);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
296
|
+
const decodeVlq = (text, cursor) => {
|
|
297
|
+
let result = 0;
|
|
298
|
+
let shift = 0;
|
|
299
|
+
let continuation;
|
|
300
|
+
do {
|
|
301
|
+
const digit = base64.indexOf(text[cursor.index++]);
|
|
302
|
+
if (digit < 0) return undefined;
|
|
303
|
+
continuation = digit & 32;
|
|
304
|
+
result += (digit & 31) << shift;
|
|
305
|
+
shift += 5;
|
|
306
|
+
} while (continuation);
|
|
307
|
+
const negative = result & 1;
|
|
308
|
+
result >>>= 1;
|
|
309
|
+
return negative ? -result : result;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/** Decode a version-3 source map into presentation-neutral mapping points. */
|
|
313
|
+
export const normalizeSourceMap = (sourceMap) => {
|
|
314
|
+
if (!sourceMap || sourceMap.version !== 3 || !Array.isArray(sourceMap.sources) ||
|
|
315
|
+
typeof sourceMap.mappings !== "string") {
|
|
316
|
+
return deepFreeze({ ok: false, reason: "invalid-source-map" });
|
|
317
|
+
}
|
|
318
|
+
const entries = [];
|
|
319
|
+
let generatedLine = 0;
|
|
320
|
+
let generatedColumn = 0;
|
|
321
|
+
let sourceIndex = 0;
|
|
322
|
+
let originalLine = 0;
|
|
323
|
+
let originalColumn = 0;
|
|
324
|
+
let nameIndex = 0;
|
|
325
|
+
const lines = sourceMap.mappings.split(";");
|
|
326
|
+
for (const line of lines) {
|
|
327
|
+
generatedColumn = 0;
|
|
328
|
+
for (const encoded of line.split(",")) {
|
|
329
|
+
if (!encoded) continue;
|
|
330
|
+
const cursor = { index: 0 };
|
|
331
|
+
const generatedDelta = decodeVlq(encoded, cursor);
|
|
332
|
+
if (generatedDelta === undefined) return deepFreeze({ ok: false, reason: "invalid-source-map-vlq" });
|
|
333
|
+
generatedColumn += generatedDelta;
|
|
334
|
+
if (cursor.index >= encoded.length) {
|
|
335
|
+
entries.push({ generated: { line: generatedLine, column: generatedColumn } });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const sourceDelta = decodeVlq(encoded, cursor);
|
|
339
|
+
const lineDelta = decodeVlq(encoded, cursor);
|
|
340
|
+
const columnDelta = decodeVlq(encoded, cursor);
|
|
341
|
+
if ([sourceDelta, lineDelta, columnDelta].some((value) => value === undefined)) {
|
|
342
|
+
return deepFreeze({ ok: false, reason: "invalid-source-map-vlq" });
|
|
343
|
+
}
|
|
344
|
+
sourceIndex += sourceDelta;
|
|
345
|
+
originalLine += lineDelta;
|
|
346
|
+
originalColumn += columnDelta;
|
|
347
|
+
const entry = {
|
|
348
|
+
generated: { line: generatedLine, column: generatedColumn },
|
|
349
|
+
source: sourceMap.sources[sourceIndex],
|
|
350
|
+
original: { line: originalLine, column: originalColumn }
|
|
351
|
+
};
|
|
352
|
+
if (cursor.index < encoded.length) {
|
|
353
|
+
const nameDelta = decodeVlq(encoded, cursor);
|
|
354
|
+
nameIndex += nameDelta;
|
|
355
|
+
entry.name = sourceMap.names?.[nameIndex];
|
|
356
|
+
}
|
|
357
|
+
entries.push(entry);
|
|
358
|
+
}
|
|
359
|
+
generatedLine += 1;
|
|
360
|
+
}
|
|
361
|
+
return deepFreeze({
|
|
362
|
+
ok: true,
|
|
363
|
+
map: {
|
|
364
|
+
kind: "source-map",
|
|
365
|
+
sourceRoot: sourceMap.sourceRoot,
|
|
366
|
+
sources: clone(sourceMap.sources),
|
|
367
|
+
sourcesContent: clone(sourceMap.sourcesContent ?? []),
|
|
368
|
+
entries
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const mappingForInputOffset = (document, offset, affinity = "right") => {
|
|
374
|
+
const candidates = document.mappings.filter((segment) => {
|
|
375
|
+
if (segment.virtual.start === segment.virtual.end) return segment.virtual.start === offset;
|
|
376
|
+
return affinity === "right"
|
|
377
|
+
? segment.virtual.start <= offset && offset < segment.virtual.end
|
|
378
|
+
: segment.virtual.start < offset && offset <= segment.virtual.end;
|
|
379
|
+
});
|
|
380
|
+
return candidates.find((segment) => segment.kind !== "synthetic") ?? candidates[0];
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const outputRangeForInputRange = (mapping, inputRange) => {
|
|
384
|
+
const start = mapTransformOffset(mapping, inputRange.start, { direction: "input-to-output", affinity: "right" });
|
|
385
|
+
const end = mapTransformOffset(mapping, inputRange.end, { direction: "input-to-output", affinity: "left" });
|
|
386
|
+
return start.matches.length && end.matches.length
|
|
387
|
+
? { start: Math.min(...start.matches), end: Math.max(...end.matches) }
|
|
388
|
+
: undefined;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const transformedSource = (document, segment, inputStart, inputEnd) => {
|
|
392
|
+
const source = sourceLocation(segment.source);
|
|
393
|
+
const offsets = sourceRangeOffsets(source);
|
|
394
|
+
const inputLength = segment.virtual.end - segment.virtual.start;
|
|
395
|
+
if (!source || !offsets || offsets.end - offsets.start !== inputLength) return source;
|
|
396
|
+
const relativeStart = inputStart - segment.virtual.start;
|
|
397
|
+
const relativeEnd = inputEnd - segment.virtual.start;
|
|
398
|
+
const text = document.text.slice(segment.virtual.start, segment.virtual.end);
|
|
399
|
+
const advance = (count) => {
|
|
400
|
+
const result = clone(source.range.start);
|
|
401
|
+
for (let index = 0; index < count; index += 1) {
|
|
402
|
+
if (text[index] === "\n") { result.line += 1; result.column = 0; }
|
|
403
|
+
else result.column += 1;
|
|
404
|
+
result.offset += 1;
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
};
|
|
408
|
+
return { uri: source.uri, range: { start: advance(relativeStart), end: advance(relativeEnd) } };
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const sourceMapToOffsetMap = (document, outputText, mapping, inputSource) => {
|
|
412
|
+
const generatedIndex = createLineIndex(outputText);
|
|
413
|
+
const inputIndex = document.lineIndex;
|
|
414
|
+
const sourceName = inputSource ?? document.uri;
|
|
415
|
+
const points = mapping.entries.filter((entry) => entry.source === sourceName)
|
|
416
|
+
.map((entry) => {
|
|
417
|
+
const generated = offsetAt(generatedIndex, {
|
|
418
|
+
line: entry.generated.line,
|
|
419
|
+
character: entry.generated.column
|
|
420
|
+
});
|
|
421
|
+
const original = offsetAt(inputIndex, {
|
|
422
|
+
line: entry.original.line,
|
|
423
|
+
character: entry.original.column
|
|
424
|
+
});
|
|
425
|
+
return generated.ok && original.ok ? { output: generated.offset, input: original.offset } : undefined;
|
|
426
|
+
}).filter(Boolean).sort((left, right) => left.output - right.output);
|
|
427
|
+
if (!points.length) return undefined;
|
|
428
|
+
const spans = [];
|
|
429
|
+
for (const [index, point] of points.entries()) {
|
|
430
|
+
const next = points[index + 1];
|
|
431
|
+
const outputEnd = next?.output ?? outputText.length;
|
|
432
|
+
const inputEnd = next?.input ?? document.text.length;
|
|
433
|
+
const length = Math.min(outputEnd - point.output, inputEnd - point.input);
|
|
434
|
+
if (length > 0) appendSpan(spans, span(point.input, point.input + length, point.output, point.output + length, "mapped"));
|
|
435
|
+
}
|
|
436
|
+
return finalizeMap("source-map", document.text.length, outputText.length, spans);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/** Apply a pure transform map to a virtual document while retaining provenance. */
|
|
440
|
+
export const applyTransformMap = (document, outputText, capability, options = {}) => {
|
|
441
|
+
throwIfAborted(options.signal);
|
|
442
|
+
if (!document || typeof outputText !== "string") return deepFreeze({ ok: false, reason: "invalid-transform-input" });
|
|
443
|
+
if (!projectionStages.has(options.stage ?? "transformed")) {
|
|
444
|
+
return deepFreeze({ ok: false, reason: "invalid-stage" });
|
|
445
|
+
}
|
|
446
|
+
let mapping = capability?.map ?? capability;
|
|
447
|
+
if (mapping?.kind === "identity") mapping = identityTransformMap(document.text);
|
|
448
|
+
if (mapping?.kind === "source-map") mapping = sourceMapToOffsetMap(document, outputText, mapping, options.inputSource);
|
|
449
|
+
if (capability?.kind === "opaque" || mapping?.kind === "opaque" || !mapping) {
|
|
450
|
+
const root = document.occurrences.find((occurrence) => !occurrence.parentOccurrenceId);
|
|
451
|
+
const source = sourceLocation(capability?.anchor ?? mapping?.anchor ?? root?.definitionSource ?? document.artifactSource);
|
|
452
|
+
const occurrenceId = root?.id;
|
|
453
|
+
const mappings = outputText.length ? [{
|
|
454
|
+
virtual: { start: 0, end: outputText.length },
|
|
455
|
+
source,
|
|
456
|
+
pieceId: root?.pieceId,
|
|
457
|
+
occurrenceId,
|
|
458
|
+
expansionPath: clone(root?.expansionPath ?? []),
|
|
459
|
+
kind: "opaque",
|
|
460
|
+
role: "transform-output",
|
|
461
|
+
startAffinity: "right",
|
|
462
|
+
endAffinity: "left",
|
|
463
|
+
transformChain: [...(options.transformChain ?? []), { name: options.name ?? "opaque" }]
|
|
464
|
+
}] : [];
|
|
465
|
+
const occurrences = document.occurrences.map((occurrence) => ({
|
|
466
|
+
...clone(occurrence),
|
|
467
|
+
virtual: occurrence.id === occurrenceId
|
|
468
|
+
? { start: 0, end: outputText.length }
|
|
469
|
+
: { start: 0, end: 0 }
|
|
470
|
+
}));
|
|
471
|
+
return transformedDocument(document, outputText, mappings, occurrences, options);
|
|
472
|
+
}
|
|
473
|
+
const validation = validateTransformMapping(mapping);
|
|
474
|
+
if (!validation.ok || mapping.kind !== "offset" || mapping.inputLength !== document.text.length ||
|
|
475
|
+
mapping.outputLength !== outputText.length) {
|
|
476
|
+
return deepFreeze({ ok: false, reason: "transform-map-does-not-match-text" });
|
|
477
|
+
}
|
|
478
|
+
const resultSegments = [];
|
|
479
|
+
for (const transformSpan of mapping.spans) {
|
|
480
|
+
throwIfAborted(options.signal);
|
|
481
|
+
if (transformSpan.mode === "removed" || transformSpan.output.start === transformSpan.output.end) continue;
|
|
482
|
+
if (transformSpan.mode === "inserted") {
|
|
483
|
+
const responsible = mappingForInputOffset(document, transformSpan.input.start);
|
|
484
|
+
resultSegments.push({
|
|
485
|
+
virtual: clone(transformSpan.output),
|
|
486
|
+
source: sourceLocation(options.transformSource),
|
|
487
|
+
pieceId: responsible?.pieceId,
|
|
488
|
+
occurrenceId: responsible?.occurrenceId,
|
|
489
|
+
expansionPath: clone(responsible?.expansionPath ?? []),
|
|
490
|
+
kind: options.transformSource ? "anchored" : "synthetic",
|
|
491
|
+
role: "transform-insert",
|
|
492
|
+
startAffinity: "right",
|
|
493
|
+
endAffinity: "left",
|
|
494
|
+
transformChain: [...(responsible?.transformChain ?? []), { name: options.name ?? mapping.name }]
|
|
495
|
+
});
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
for (const segment of document.mappings) {
|
|
499
|
+
if (segment.virtual.start === segment.virtual.end) continue;
|
|
500
|
+
const start = Math.max(segment.virtual.start, transformSpan.input.start);
|
|
501
|
+
const end = Math.min(segment.virtual.end, transformSpan.input.end);
|
|
502
|
+
if (start >= end) continue;
|
|
503
|
+
const outputStart = transformSpan.output.start + start - transformSpan.input.start;
|
|
504
|
+
const outputEnd = outputStart + end - start;
|
|
505
|
+
const precise = segment.kind === "exact" || segment.kind === "transformed";
|
|
506
|
+
resultSegments.push({
|
|
507
|
+
virtual: { start: outputStart, end: outputEnd },
|
|
508
|
+
source: transformedSource(document, segment, start, end),
|
|
509
|
+
pieceId: segment.pieceId,
|
|
510
|
+
occurrenceId: segment.occurrenceId,
|
|
511
|
+
expansionPath: clone(segment.expansionPath ?? []),
|
|
512
|
+
kind: precise ? (mapping.name === "identity" ? segment.kind : "transformed") : segment.kind,
|
|
513
|
+
role: segment.role,
|
|
514
|
+
startAffinity: segment.startAffinity,
|
|
515
|
+
endAffinity: segment.endAffinity,
|
|
516
|
+
transformChain: [...(segment.transformChain ?? []), { name: options.name ?? mapping.name }]
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const mappings = coalesceProjectionSegments(resultSegments);
|
|
521
|
+
const occurrences = document.occurrences.map((occurrence) => ({
|
|
522
|
+
...clone(occurrence),
|
|
523
|
+
virtual: outputRangeForInputRange(mapping, occurrence.virtual) ?? { start: 0, end: 0 }
|
|
524
|
+
}));
|
|
525
|
+
return transformedDocument(document, outputText, mappings, occurrences, options);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const transformedDocument = (document, text, mappings, occurrences, options) => {
|
|
529
|
+
const stage = options.stage ?? "transformed";
|
|
530
|
+
const languageId = options.languageId ?? document.languageId;
|
|
531
|
+
const id = options.projectionId ?? createProjectionId({
|
|
532
|
+
workspaceId: document.workspaceId,
|
|
533
|
+
targetId: document.targetId,
|
|
534
|
+
artifactId: document.artifactId,
|
|
535
|
+
stage,
|
|
536
|
+
languageId
|
|
537
|
+
});
|
|
538
|
+
const uri = options.uri ?? createVirtualUri({
|
|
539
|
+
workspaceId: document.workspaceId,
|
|
540
|
+
targetId: document.targetId,
|
|
541
|
+
artifactId: document.artifactId,
|
|
542
|
+
stage,
|
|
543
|
+
path: options.path ?? document.artifactId
|
|
544
|
+
});
|
|
545
|
+
const finalMappings = coalesceProjectionSegments(mappings);
|
|
546
|
+
const result = {
|
|
547
|
+
...document,
|
|
548
|
+
id,
|
|
549
|
+
uri,
|
|
550
|
+
version: options.version ?? 1,
|
|
551
|
+
stage,
|
|
552
|
+
languageId,
|
|
553
|
+
text,
|
|
554
|
+
mappings: finalMappings,
|
|
555
|
+
occurrences,
|
|
556
|
+
lineIndex: createLineIndex(text),
|
|
557
|
+
indexes: buildProjectionIndexes(finalMappings, occurrences),
|
|
558
|
+
contentHash: stableHash(text),
|
|
559
|
+
inputHash: stableHash({ previous: document.inputHash, text, stage, languageId, mappings: finalMappings }),
|
|
560
|
+
capabilities: stageCapabilities(stage, options.capabilities)
|
|
561
|
+
};
|
|
562
|
+
return deepFreeze({ ok: true, document: deepFreeze(result) });
|
|
563
|
+
};
|