yamlover 0.3.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 +214 -0
- package/bin/yamlover.js +202 -0
- package/dist/server.js +4681 -0
- package/index.html +12 -0
- package/package.json +72 -0
- package/src/client/App.tsx +372 -0
- package/src/client/NodeView.tsx +422 -0
- package/src/client/TaskStrip.tsx +34 -0
- package/src/client/Tree.tsx +97 -0
- package/src/client/api.ts +186 -0
- package/src/client/icons.ts +91 -0
- package/src/client/links.tsx +108 -0
- package/src/client/live.ts +42 -0
- package/src/client/main.tsx +10 -0
- package/src/client/paste-html.ts +228 -0
- package/src/client/paste-links.ts +42 -0
- package/src/client/paths.ts +109 -0
- package/src/client/render.tsx +326 -0
- package/src/client/renderers/annotate.tsx +507 -0
- package/src/client/renderers/asciidoc.tsx +35 -0
- package/src/client/renderers/chapter.tsx +138 -0
- package/src/client/renderers/csv.tsx +233 -0
- package/src/client/renderers/decoded.tsx +72 -0
- package/src/client/renderers/djvu.tsx +97 -0
- package/src/client/renderers/doc.tsx +40 -0
- package/src/client/renderers/docx.tsx +49 -0
- package/src/client/renderers/epub.tsx +147 -0
- package/src/client/renderers/explorer.tsx +209 -0
- package/src/client/renderers/fb2.tsx +149 -0
- package/src/client/renderers/headings.ts +69 -0
- package/src/client/renderers/heic.tsx +23 -0
- package/src/client/renderers/imagemap.tsx +157 -0
- package/src/client/renderers/kml.ts +46 -0
- package/src/client/renderers/latex.tsx +36 -0
- package/src/client/renderers/map.tsx +205 -0
- package/src/client/renderers/marklower.tsx +119 -0
- package/src/client/renderers/markup.tsx +64 -0
- package/src/client/renderers/media.tsx +19 -0
- package/src/client/renderers/panzoom.ts +101 -0
- package/src/client/renderers/pdf.tsx +176 -0
- package/src/client/renderers/plaintext.tsx +120 -0
- package/src/client/renderers/plantuml.tsx +82 -0
- package/src/client/renderers/psd.tsx +25 -0
- package/src/client/renderers/registry.tsx +389 -0
- package/src/client/renderers/rtf.tsx +210 -0
- package/src/client/renderers/spreadsheet.tsx +105 -0
- package/src/client/renderers/tag.tsx +113 -0
- package/src/client/renderers/text.tsx +41 -0
- package/src/client/renderers/tiff.tsx +33 -0
- package/src/client/styles.css +1115 -0
- package/src/client/vendor/README.md +30 -0
- package/src/client/vendor/djvu.js +15535 -0
- package/src/client/vite-env.d.ts +31 -0
- package/src/server/api.ts +147 -0
- package/src/server/engine-api.ts +1442 -0
- package/src/server/gitignore.ts +81 -0
- package/src/server/node-kind.ts +48 -0
- package/src/server/tasks.ts +83 -0
- package/src/server/yamlover.ts +1133 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,4681 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// node_modules/ignore/index.js
|
|
28
|
+
var require_ignore = __commonJS({
|
|
29
|
+
"node_modules/ignore/index.js"(exports, module) {
|
|
30
|
+
function makeArray(subject) {
|
|
31
|
+
return Array.isArray(subject) ? subject : [subject];
|
|
32
|
+
}
|
|
33
|
+
var EMPTY = "";
|
|
34
|
+
var SPACE = " ";
|
|
35
|
+
var ESCAPE = "\\";
|
|
36
|
+
var REGEX_TEST_BLANK_LINE = /^\s+$/;
|
|
37
|
+
var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
|
|
38
|
+
var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
|
|
39
|
+
var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
|
|
40
|
+
var REGEX_SPLITALL_CRLF = /\r?\n/g;
|
|
41
|
+
var REGEX_TEST_INVALID_PATH = /^\.*\/|^\.+$/;
|
|
42
|
+
var SLASH = "/";
|
|
43
|
+
var TMP_KEY_IGNORE = "node-ignore";
|
|
44
|
+
if (typeof Symbol !== "undefined") {
|
|
45
|
+
TMP_KEY_IGNORE = Symbol.for("node-ignore");
|
|
46
|
+
}
|
|
47
|
+
var KEY_IGNORE = TMP_KEY_IGNORE;
|
|
48
|
+
var define = (object, key, value) => Object.defineProperty(object, key, { value });
|
|
49
|
+
var REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
|
|
50
|
+
var RETURN_FALSE = () => false;
|
|
51
|
+
var sanitizeRange = (range) => range.replace(
|
|
52
|
+
REGEX_REGEXP_RANGE,
|
|
53
|
+
(match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match : EMPTY
|
|
54
|
+
);
|
|
55
|
+
var cleanRangeBackSlash = (slashes) => {
|
|
56
|
+
const { length } = slashes;
|
|
57
|
+
return slashes.slice(0, length - length % 2);
|
|
58
|
+
};
|
|
59
|
+
var REPLACERS = [
|
|
60
|
+
[
|
|
61
|
+
// remove BOM
|
|
62
|
+
// TODO:
|
|
63
|
+
// Other similar zero-width characters?
|
|
64
|
+
/^\uFEFF/,
|
|
65
|
+
() => EMPTY
|
|
66
|
+
],
|
|
67
|
+
// > Trailing spaces are ignored unless they are quoted with backslash ("\")
|
|
68
|
+
[
|
|
69
|
+
// (a\ ) -> (a )
|
|
70
|
+
// (a ) -> (a)
|
|
71
|
+
// (a ) -> (a)
|
|
72
|
+
// (a \ ) -> (a )
|
|
73
|
+
/((?:\\\\)*?)(\\?\s+)$/,
|
|
74
|
+
(_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY)
|
|
75
|
+
],
|
|
76
|
+
// replace (\ ) with ' '
|
|
77
|
+
// (\ ) -> ' '
|
|
78
|
+
// (\\ ) -> '\\ '
|
|
79
|
+
// (\\\ ) -> '\\ '
|
|
80
|
+
[
|
|
81
|
+
/(\\+?)\s/g,
|
|
82
|
+
(_, m1) => {
|
|
83
|
+
const { length } = m1;
|
|
84
|
+
return m1.slice(0, length - length % 2) + SPACE;
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
// Escape metacharacters
|
|
88
|
+
// which is written down by users but means special for regular expressions.
|
|
89
|
+
// > There are 12 characters with special meanings:
|
|
90
|
+
// > - the backslash \,
|
|
91
|
+
// > - the caret ^,
|
|
92
|
+
// > - the dollar sign $,
|
|
93
|
+
// > - the period or dot .,
|
|
94
|
+
// > - the vertical bar or pipe symbol |,
|
|
95
|
+
// > - the question mark ?,
|
|
96
|
+
// > - the asterisk or star *,
|
|
97
|
+
// > - the plus sign +,
|
|
98
|
+
// > - the opening parenthesis (,
|
|
99
|
+
// > - the closing parenthesis ),
|
|
100
|
+
// > - and the opening square bracket [,
|
|
101
|
+
// > - the opening curly brace {,
|
|
102
|
+
// > These special characters are often called "metacharacters".
|
|
103
|
+
[
|
|
104
|
+
/[\\$.|*+(){^]/g,
|
|
105
|
+
(match) => `\\${match}`
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
// > a question mark (?) matches a single character
|
|
109
|
+
/(?!\\)\?/g,
|
|
110
|
+
() => "[^/]"
|
|
111
|
+
],
|
|
112
|
+
// leading slash
|
|
113
|
+
[
|
|
114
|
+
// > A leading slash matches the beginning of the pathname.
|
|
115
|
+
// > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
|
|
116
|
+
// A leading slash matches the beginning of the pathname
|
|
117
|
+
/^\//,
|
|
118
|
+
() => "^"
|
|
119
|
+
],
|
|
120
|
+
// replace special metacharacter slash after the leading slash
|
|
121
|
+
[
|
|
122
|
+
/\//g,
|
|
123
|
+
() => "\\/"
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
// > A leading "**" followed by a slash means match in all directories.
|
|
127
|
+
// > For example, "**/foo" matches file or directory "foo" anywhere,
|
|
128
|
+
// > the same as pattern "foo".
|
|
129
|
+
// > "**/foo/bar" matches file or directory "bar" anywhere that is directly
|
|
130
|
+
// > under directory "foo".
|
|
131
|
+
// Notice that the '*'s have been replaced as '\\*'
|
|
132
|
+
/^\^*\\\*\\\*\\\//,
|
|
133
|
+
// '**/foo' <-> 'foo'
|
|
134
|
+
() => "^(?:.*\\/)?"
|
|
135
|
+
],
|
|
136
|
+
// starting
|
|
137
|
+
[
|
|
138
|
+
// there will be no leading '/'
|
|
139
|
+
// (which has been replaced by section "leading slash")
|
|
140
|
+
// If starts with '**', adding a '^' to the regular expression also works
|
|
141
|
+
/^(?=[^^])/,
|
|
142
|
+
function startingReplacer() {
|
|
143
|
+
return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^";
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
// two globstars
|
|
147
|
+
[
|
|
148
|
+
// Use lookahead assertions so that we could match more than one `'/**'`
|
|
149
|
+
/\\\/\\\*\\\*(?=\\\/|$)/g,
|
|
150
|
+
// Zero, one or several directories
|
|
151
|
+
// should not use '*', or it will be replaced by the next replacer
|
|
152
|
+
// Check if it is not the last `'/**'`
|
|
153
|
+
(_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
|
|
154
|
+
],
|
|
155
|
+
// normal intermediate wildcards
|
|
156
|
+
[
|
|
157
|
+
// Never replace escaped '*'
|
|
158
|
+
// ignore rule '\*' will match the path '*'
|
|
159
|
+
// 'abc.*/' -> go
|
|
160
|
+
// 'abc.*' -> skip this rule,
|
|
161
|
+
// coz trailing single wildcard will be handed by [trailing wildcard]
|
|
162
|
+
/(^|[^\\]+)(\\\*)+(?=.+)/g,
|
|
163
|
+
// '*.js' matches '.js'
|
|
164
|
+
// '*.js' doesn't match 'abc'
|
|
165
|
+
(_, p1, p2) => {
|
|
166
|
+
const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
|
|
167
|
+
return p1 + unescaped;
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
// unescape, revert step 3 except for back slash
|
|
172
|
+
// For example, if a user escape a '\\*',
|
|
173
|
+
// after step 3, the result will be '\\\\\\*'
|
|
174
|
+
/\\\\\\(?=[$.|*+(){^])/g,
|
|
175
|
+
() => ESCAPE
|
|
176
|
+
],
|
|
177
|
+
[
|
|
178
|
+
// '\\\\' -> '\\'
|
|
179
|
+
/\\\\/g,
|
|
180
|
+
() => ESCAPE
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
// > The range notation, e.g. [a-zA-Z],
|
|
184
|
+
// > can be used to match one of the characters in a range.
|
|
185
|
+
// `\` is escaped by step 3
|
|
186
|
+
/(\\)?\[([^\]/]*?)(\\*)($|\])/g,
|
|
187
|
+
(match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]"
|
|
188
|
+
],
|
|
189
|
+
// ending
|
|
190
|
+
[
|
|
191
|
+
// 'js' will not match 'js.'
|
|
192
|
+
// 'ab' will not match 'abc'
|
|
193
|
+
/(?:[^*])$/,
|
|
194
|
+
// WTF!
|
|
195
|
+
// https://git-scm.com/docs/gitignore
|
|
196
|
+
// changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
|
|
197
|
+
// which re-fixes #24, #38
|
|
198
|
+
// > If there is a separator at the end of the pattern then the pattern
|
|
199
|
+
// > will only match directories, otherwise the pattern can match both
|
|
200
|
+
// > files and directories.
|
|
201
|
+
// 'js*' will not match 'a.js'
|
|
202
|
+
// 'js/' will not match 'a.js'
|
|
203
|
+
// 'js' will match 'a.js' and 'a.js/'
|
|
204
|
+
(match) => /\/$/.test(match) ? `${match}$` : `${match}(?=$|\\/$)`
|
|
205
|
+
],
|
|
206
|
+
// trailing wildcard
|
|
207
|
+
[
|
|
208
|
+
/(\^|\\\/)?\\\*$/,
|
|
209
|
+
(_, p1) => {
|
|
210
|
+
const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
|
|
211
|
+
return `${prefix}(?=$|\\/$)`;
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
];
|
|
215
|
+
var regexCache = /* @__PURE__ */ Object.create(null);
|
|
216
|
+
var makeRegex = (pattern, ignoreCase) => {
|
|
217
|
+
let source = regexCache[pattern];
|
|
218
|
+
if (!source) {
|
|
219
|
+
source = REPLACERS.reduce(
|
|
220
|
+
(prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)),
|
|
221
|
+
pattern
|
|
222
|
+
);
|
|
223
|
+
regexCache[pattern] = source;
|
|
224
|
+
}
|
|
225
|
+
return ignoreCase ? new RegExp(source, "i") : new RegExp(source);
|
|
226
|
+
};
|
|
227
|
+
var isString = (subject) => typeof subject === "string";
|
|
228
|
+
var checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0;
|
|
229
|
+
var splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF);
|
|
230
|
+
var IgnoreRule = class {
|
|
231
|
+
constructor(origin, pattern, negative, regex) {
|
|
232
|
+
this.origin = origin;
|
|
233
|
+
this.pattern = pattern;
|
|
234
|
+
this.negative = negative;
|
|
235
|
+
this.regex = regex;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
var createRule = (pattern, ignoreCase) => {
|
|
239
|
+
const origin = pattern;
|
|
240
|
+
let negative = false;
|
|
241
|
+
if (pattern.indexOf("!") === 0) {
|
|
242
|
+
negative = true;
|
|
243
|
+
pattern = pattern.substr(1);
|
|
244
|
+
}
|
|
245
|
+
pattern = pattern.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
|
|
246
|
+
const regex = makeRegex(pattern, ignoreCase);
|
|
247
|
+
return new IgnoreRule(
|
|
248
|
+
origin,
|
|
249
|
+
pattern,
|
|
250
|
+
negative,
|
|
251
|
+
regex
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
var throwError = (message, Ctor) => {
|
|
255
|
+
throw new Ctor(message);
|
|
256
|
+
};
|
|
257
|
+
var checkPath = (path8, originalPath, doThrow) => {
|
|
258
|
+
if (!isString(path8)) {
|
|
259
|
+
return doThrow(
|
|
260
|
+
`path must be a string, but got \`${originalPath}\``,
|
|
261
|
+
TypeError
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (!path8) {
|
|
265
|
+
return doThrow(`path must not be empty`, TypeError);
|
|
266
|
+
}
|
|
267
|
+
if (checkPath.isNotRelative(path8)) {
|
|
268
|
+
const r = "`path.relative()`d";
|
|
269
|
+
return doThrow(
|
|
270
|
+
`path should be a ${r} string, but got "${originalPath}"`,
|
|
271
|
+
RangeError
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
};
|
|
276
|
+
var isNotRelative = (path8) => REGEX_TEST_INVALID_PATH.test(path8);
|
|
277
|
+
checkPath.isNotRelative = isNotRelative;
|
|
278
|
+
checkPath.convert = (p) => p;
|
|
279
|
+
var Ignore = class {
|
|
280
|
+
constructor({
|
|
281
|
+
ignorecase = true,
|
|
282
|
+
ignoreCase = ignorecase,
|
|
283
|
+
allowRelativePaths = false
|
|
284
|
+
} = {}) {
|
|
285
|
+
define(this, KEY_IGNORE, true);
|
|
286
|
+
this._rules = [];
|
|
287
|
+
this._ignoreCase = ignoreCase;
|
|
288
|
+
this._allowRelativePaths = allowRelativePaths;
|
|
289
|
+
this._initCache();
|
|
290
|
+
}
|
|
291
|
+
_initCache() {
|
|
292
|
+
this._ignoreCache = /* @__PURE__ */ Object.create(null);
|
|
293
|
+
this._testCache = /* @__PURE__ */ Object.create(null);
|
|
294
|
+
}
|
|
295
|
+
_addPattern(pattern) {
|
|
296
|
+
if (pattern && pattern[KEY_IGNORE]) {
|
|
297
|
+
this._rules = this._rules.concat(pattern._rules);
|
|
298
|
+
this._added = true;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (checkPattern(pattern)) {
|
|
302
|
+
const rule = createRule(pattern, this._ignoreCase);
|
|
303
|
+
this._added = true;
|
|
304
|
+
this._rules.push(rule);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// @param {Array<string> | string | Ignore} pattern
|
|
308
|
+
add(pattern) {
|
|
309
|
+
this._added = false;
|
|
310
|
+
makeArray(
|
|
311
|
+
isString(pattern) ? splitPattern(pattern) : pattern
|
|
312
|
+
).forEach(this._addPattern, this);
|
|
313
|
+
if (this._added) {
|
|
314
|
+
this._initCache();
|
|
315
|
+
}
|
|
316
|
+
return this;
|
|
317
|
+
}
|
|
318
|
+
// legacy
|
|
319
|
+
addPattern(pattern) {
|
|
320
|
+
return this.add(pattern);
|
|
321
|
+
}
|
|
322
|
+
// | ignored : unignored
|
|
323
|
+
// negative | 0:0 | 0:1 | 1:0 | 1:1
|
|
324
|
+
// -------- | ------- | ------- | ------- | --------
|
|
325
|
+
// 0 | TEST | TEST | SKIP | X
|
|
326
|
+
// 1 | TESTIF | SKIP | TEST | X
|
|
327
|
+
// - SKIP: always skip
|
|
328
|
+
// - TEST: always test
|
|
329
|
+
// - TESTIF: only test if checkUnignored
|
|
330
|
+
// - X: that never happen
|
|
331
|
+
// @param {boolean} whether should check if the path is unignored,
|
|
332
|
+
// setting `checkUnignored` to `false` could reduce additional
|
|
333
|
+
// path matching.
|
|
334
|
+
// @returns {TestResult} true if a file is ignored
|
|
335
|
+
_testOne(path8, checkUnignored) {
|
|
336
|
+
let ignored = false;
|
|
337
|
+
let unignored = false;
|
|
338
|
+
this._rules.forEach((rule) => {
|
|
339
|
+
const { negative } = rule;
|
|
340
|
+
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const matched = rule.regex.test(path8);
|
|
344
|
+
if (matched) {
|
|
345
|
+
ignored = !negative;
|
|
346
|
+
unignored = negative;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
ignored,
|
|
351
|
+
unignored
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
// @returns {TestResult}
|
|
355
|
+
_test(originalPath, cache, checkUnignored, slices) {
|
|
356
|
+
const path8 = originalPath && checkPath.convert(originalPath);
|
|
357
|
+
checkPath(
|
|
358
|
+
path8,
|
|
359
|
+
originalPath,
|
|
360
|
+
this._allowRelativePaths ? RETURN_FALSE : throwError
|
|
361
|
+
);
|
|
362
|
+
return this._t(path8, cache, checkUnignored, slices);
|
|
363
|
+
}
|
|
364
|
+
_t(path8, cache, checkUnignored, slices) {
|
|
365
|
+
if (path8 in cache) {
|
|
366
|
+
return cache[path8];
|
|
367
|
+
}
|
|
368
|
+
if (!slices) {
|
|
369
|
+
slices = path8.split(SLASH);
|
|
370
|
+
}
|
|
371
|
+
slices.pop();
|
|
372
|
+
if (!slices.length) {
|
|
373
|
+
return cache[path8] = this._testOne(path8, checkUnignored);
|
|
374
|
+
}
|
|
375
|
+
const parent = this._t(
|
|
376
|
+
slices.join(SLASH) + SLASH,
|
|
377
|
+
cache,
|
|
378
|
+
checkUnignored,
|
|
379
|
+
slices
|
|
380
|
+
);
|
|
381
|
+
return cache[path8] = parent.ignored ? parent : this._testOne(path8, checkUnignored);
|
|
382
|
+
}
|
|
383
|
+
ignores(path8) {
|
|
384
|
+
return this._test(path8, this._ignoreCache, false).ignored;
|
|
385
|
+
}
|
|
386
|
+
createFilter() {
|
|
387
|
+
return (path8) => !this.ignores(path8);
|
|
388
|
+
}
|
|
389
|
+
filter(paths) {
|
|
390
|
+
return makeArray(paths).filter(this.createFilter());
|
|
391
|
+
}
|
|
392
|
+
// @returns {TestResult}
|
|
393
|
+
test(path8) {
|
|
394
|
+
return this._test(path8, this._testCache, true);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
var factory = (options) => new Ignore(options);
|
|
398
|
+
var isPathValid = (path8) => checkPath(path8 && checkPath.convert(path8), path8, RETURN_FALSE);
|
|
399
|
+
factory.isPathValid = isPathValid;
|
|
400
|
+
factory.default = factory;
|
|
401
|
+
module.exports = factory;
|
|
402
|
+
if (
|
|
403
|
+
// Detect `process` so that it can run in browsers.
|
|
404
|
+
typeof process !== "undefined" && (process.env && process.env.IGNORE_TEST_WIN32 || process.platform === "win32")
|
|
405
|
+
) {
|
|
406
|
+
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
407
|
+
checkPath.convert = makePosix;
|
|
408
|
+
const REGIX_IS_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
409
|
+
checkPath.isNotRelative = (path8) => REGIX_IS_WINDOWS_PATH_ABSOLUTE.test(path8) || isNotRelative(path8);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// src/server/engine-api.ts
|
|
415
|
+
import path7 from "node:path";
|
|
416
|
+
import fs6 from "node:fs";
|
|
417
|
+
|
|
418
|
+
// ../parser/ts/src/ir.ts
|
|
419
|
+
function isPointer(v) {
|
|
420
|
+
return v.kind === "pointer";
|
|
421
|
+
}
|
|
422
|
+
function toPlain(node) {
|
|
423
|
+
const ents = node.entries ?? [];
|
|
424
|
+
if (ents.length === 0) {
|
|
425
|
+
if (node.kind === "scalar") return node.value;
|
|
426
|
+
if (node.kind === "blob") throw new Error("toPlain: a blob has no plain JSON form");
|
|
427
|
+
return node.array ? [] : {};
|
|
428
|
+
}
|
|
429
|
+
if (node.kind === "mapping" && (node.array ?? ents.every((e2) => e2.key === null))) {
|
|
430
|
+
return ents.map(entryPlain);
|
|
431
|
+
}
|
|
432
|
+
const o = {};
|
|
433
|
+
if (node.kind === "scalar") o.$value = node.value;
|
|
434
|
+
ents.forEach((e2, i) => {
|
|
435
|
+
o[e2.key ?? String(i)] = entryPlain(e2);
|
|
436
|
+
});
|
|
437
|
+
return o;
|
|
438
|
+
}
|
|
439
|
+
function entryPlain(e2) {
|
|
440
|
+
if (isPointer(e2.value)) throw new Error("toPlain: unresolved pointer has no plain form");
|
|
441
|
+
return toPlain(e2.value);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ../engine/ts/src/resolve.ts
|
|
445
|
+
function resolveDocument(doc) {
|
|
446
|
+
const chains = buildChains(doc.root);
|
|
447
|
+
const { edges: anchorEdges, keys } = realizeAnchors(doc, chains);
|
|
448
|
+
const out = [];
|
|
449
|
+
const walk = (node, docRoot) => {
|
|
450
|
+
if (!node.entries && !node.meta?.anchors) return;
|
|
451
|
+
const chain = chains.get(node);
|
|
452
|
+
const base = pathOf(chain);
|
|
453
|
+
const dr = node.meta?.documentRoot ? base : docRoot;
|
|
454
|
+
const prefix = base === ":" ? "" : base;
|
|
455
|
+
node.entries?.forEach((e2, i) => {
|
|
456
|
+
const seg = e2.key != null ? ":" + e2.key : "[" + i + "]";
|
|
457
|
+
if (isPointer(e2.value)) {
|
|
458
|
+
const target = resolve(doc, chains, chain, e2.value, /* @__PURE__ */ new Set([e2.value]), keys);
|
|
459
|
+
out.push({ from: prefix + seg, holder: base, label: e2.key, pos: i, raw: e2.value.raw, edge: e2.edge, target, ptr: e2.value, docRoot: dr });
|
|
460
|
+
} else {
|
|
461
|
+
walk(e2.value, dr);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
for (const ae of anchorEdges) {
|
|
465
|
+
if (ae.node !== node) continue;
|
|
466
|
+
const raw = "&" + ae.anchor.path.raw + (ae.anchor.ordinal ? "[]" : "");
|
|
467
|
+
out.push({
|
|
468
|
+
from: base,
|
|
469
|
+
holder: base,
|
|
470
|
+
label: anchorKeyOf(ae.anchor),
|
|
471
|
+
pos: (node.entries?.length ?? 0) + ae.idx,
|
|
472
|
+
raw,
|
|
473
|
+
edge: "back",
|
|
474
|
+
target: ae.target,
|
|
475
|
+
ptr: ae.anchor.path,
|
|
476
|
+
docRoot: dr,
|
|
477
|
+
anchor: true
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
walk(doc.root, ":");
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
function anchorKeyOf(a) {
|
|
485
|
+
return a.ordinal ? null : lastKey(a.path);
|
|
486
|
+
}
|
|
487
|
+
function lastKey(p) {
|
|
488
|
+
const last = p.steps[p.steps.length - 1];
|
|
489
|
+
return last && last.sel === "key" ? last.name : null;
|
|
490
|
+
}
|
|
491
|
+
function realizeAnchors(doc, chains) {
|
|
492
|
+
const edges = [];
|
|
493
|
+
const keys = /* @__PURE__ */ new Map();
|
|
494
|
+
for (const [node, chain] of chains) {
|
|
495
|
+
const anchors = node.meta?.anchors;
|
|
496
|
+
if (!anchors) continue;
|
|
497
|
+
anchors.forEach((a, idx) => {
|
|
498
|
+
const fromChain = chain.slice(0, -1);
|
|
499
|
+
const containerPtr = a.ordinal ? a.path : { ...a.path, steps: a.path.steps.slice(0, -1) };
|
|
500
|
+
let target;
|
|
501
|
+
if (fromChain.length === 0 && (containerPtr.base.scope === "current" || containerPtr.base.scope === "parent")) {
|
|
502
|
+
target = { kind: "unresolved", reason: "a relative anchor on the document root has no container" };
|
|
503
|
+
} else {
|
|
504
|
+
target = resolve(doc, chains, fromChain.length > 0 ? fromChain : [doc.root], containerPtr, /* @__PURE__ */ new Set([containerPtr]));
|
|
505
|
+
}
|
|
506
|
+
edges.push({ node, chain, anchor: a, idx, target });
|
|
507
|
+
if (!a.ordinal && target.kind === "node") {
|
|
508
|
+
const k = lastKey(a.path);
|
|
509
|
+
if (k !== null) {
|
|
510
|
+
let m = keys.get(target.node);
|
|
511
|
+
if (!m) {
|
|
512
|
+
m = /* @__PURE__ */ new Map();
|
|
513
|
+
keys.set(target.node, m);
|
|
514
|
+
}
|
|
515
|
+
if (!m.has(k)) m.set(k, node);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return { edges, keys };
|
|
521
|
+
}
|
|
522
|
+
function resolve(doc, chains, fromChain, ptr, visited, anchorKeys) {
|
|
523
|
+
const root = doc.root;
|
|
524
|
+
let steps = ptr.steps;
|
|
525
|
+
let chain;
|
|
526
|
+
const linkAuthority = ptr.base.scope === "link" ? ptr.base.authority : "";
|
|
527
|
+
let isLink = false;
|
|
528
|
+
const external = () => ({ kind: "external", authority: linkAuthority, steps: ptr.steps });
|
|
529
|
+
switch (ptr.base.scope) {
|
|
530
|
+
case "link": {
|
|
531
|
+
isLink = true;
|
|
532
|
+
chain = chains.get(root) ?? [root];
|
|
533
|
+
steps = [{ sel: "key", name: ptr.base.authority }, ...ptr.steps];
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case "document": {
|
|
537
|
+
let docRoot = root;
|
|
538
|
+
for (let k = fromChain.length - 1; k >= 0; k--) {
|
|
539
|
+
if (fromChain[k].meta?.documentRoot) {
|
|
540
|
+
docRoot = fromChain[k];
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
chain = chains.get(docRoot) ?? [docRoot];
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "parent":
|
|
548
|
+
chain = fromChain.slice(0, -1);
|
|
549
|
+
break;
|
|
550
|
+
case "current":
|
|
551
|
+
chain = fromChain;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
for (const st of steps) {
|
|
555
|
+
if (st.sel === "parent") {
|
|
556
|
+
if (chain.length <= 1) return isLink ? external() : { kind: "unresolved", reason: '".." above the document root' };
|
|
557
|
+
chain = chain.slice(0, -1);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const node = chain[chain.length - 1];
|
|
561
|
+
if (node === void 0) return { kind: "unresolved", reason: "empty resolution scope" };
|
|
562
|
+
const entry = st.sel === "key" ? node.entries?.find((e2) => e2.key === st.name) : node.entries?.[st.n];
|
|
563
|
+
if (!entry) {
|
|
564
|
+
if (st.sel === "key") {
|
|
565
|
+
const via = anchorKeys?.get(node)?.get(st.name);
|
|
566
|
+
if (via) {
|
|
567
|
+
chain = chains.get(via) ?? [via];
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (!node.entries) return isLink ? external() : { kind: "unresolved", reason: "step into a node with no fields" };
|
|
572
|
+
return isLink ? external() : { kind: "unresolved", reason: `no ${st.sel === "key" ? `key "${st.name}"` : `index [${st.n}]`}` };
|
|
573
|
+
}
|
|
574
|
+
if (isPointer(entry.value)) {
|
|
575
|
+
if (visited.has(entry.value)) return { kind: "unresolved", reason: "pointer cycle" };
|
|
576
|
+
const next = new Set(visited);
|
|
577
|
+
next.add(entry.value);
|
|
578
|
+
const r = resolve(doc, chains, chain, entry.value, next, anchorKeys);
|
|
579
|
+
if (r.kind !== "node") return r;
|
|
580
|
+
chain = chains.get(r.node) ?? [r.node];
|
|
581
|
+
} else {
|
|
582
|
+
chain = chains.get(entry.value) ?? [...chain, entry.value];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const target = chain[chain.length - 1];
|
|
586
|
+
return { kind: "node", node: target, path: pathOf(chain) };
|
|
587
|
+
}
|
|
588
|
+
function buildChains(root) {
|
|
589
|
+
const m = /* @__PURE__ */ new Map();
|
|
590
|
+
const walk = (node, chain) => {
|
|
591
|
+
const c = [...chain, node];
|
|
592
|
+
m.set(node, c);
|
|
593
|
+
if (node.entries) {
|
|
594
|
+
for (const e2 of node.entries) if (!isPointer(e2.value)) walk(e2.value, c);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
walk(root, []);
|
|
598
|
+
return m;
|
|
599
|
+
}
|
|
600
|
+
function pathOf(chain) {
|
|
601
|
+
let s = "";
|
|
602
|
+
for (let i = 1; i < chain.length; i++) {
|
|
603
|
+
const parent = chain[i - 1];
|
|
604
|
+
if (!parent.entries) {
|
|
605
|
+
s += ":?";
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const idx = parent.entries.findIndex((e3) => e3.value === chain[i]);
|
|
609
|
+
const e2 = parent.entries[idx];
|
|
610
|
+
s += e2 && e2.key != null ? ":" + e2.key : "[" + idx + "]";
|
|
611
|
+
}
|
|
612
|
+
return s === "" ? ":" : s;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ../engine/ts/src/store.ts
|
|
616
|
+
import { DatabaseSync } from "node:sqlite";
|
|
617
|
+
var SCHEMA_VERSION = 4;
|
|
618
|
+
var SCHEMA = `
|
|
619
|
+
CREATE TABLE IF NOT EXISTS node (
|
|
620
|
+
path TEXT PRIMARY KEY,
|
|
621
|
+
type TEXT NOT NULL, -- mapping | scalar | blob
|
|
622
|
+
format TEXT, -- blob/meta format (e.g. image/png, text/markdown)
|
|
623
|
+
value TEXT, -- scalar self-value, JSON-encoded (scalar/omni only)
|
|
624
|
+
content_hash TEXT, -- blob bytes hash (xxh3/BLAKE3 later)
|
|
625
|
+
size INTEGER, -- blob byte size
|
|
626
|
+
is_array INTEGER NOT NULL DEFAULT 0, -- projection hint: 1 \u21D2 all-keyless (pure sequence)
|
|
627
|
+
meta TEXT -- JSON: schema ref, span, \u2026
|
|
628
|
+
);
|
|
629
|
+
CREATE TABLE IF NOT EXISTS edge (
|
|
630
|
+
from_path TEXT NOT NULL,
|
|
631
|
+
to_path TEXT NOT NULL,
|
|
632
|
+
label TEXT, -- relation name (entry key); null for keyless
|
|
633
|
+
kind TEXT NOT NULL, -- contain | ref | back | derived
|
|
634
|
+
pos INTEGER -- order within parent (contain) for stable TOC order
|
|
635
|
+
);
|
|
636
|
+
CREATE INDEX IF NOT EXISTS edge_from ON edge (from_path);
|
|
637
|
+
CREATE INDEX IF NOT EXISTS edge_to ON edge (to_path);
|
|
638
|
+
CREATE TABLE IF NOT EXISTS file (
|
|
639
|
+
path TEXT PRIMARY KEY, -- filesystem path relative to the indexed root (POSIX)
|
|
640
|
+
hash TEXT, -- xxh64:\u2026 of the bytes; NULL until the hasher reaches a large blob
|
|
641
|
+
size INTEGER NOT NULL,
|
|
642
|
+
mtime_ms REAL NOT NULL -- (size, mtime) match \u21D2 reuse hash without re-reading
|
|
643
|
+
);
|
|
644
|
+
CREATE TABLE IF NOT EXISTS dangling (
|
|
645
|
+
from_path TEXT NOT NULL, -- the entry holding the pointer
|
|
646
|
+
raw TEXT NOT NULL, -- the pointer text as authored
|
|
647
|
+
reason TEXT NOT NULL -- why it did not resolve
|
|
648
|
+
);
|
|
649
|
+
`;
|
|
650
|
+
var Store = class {
|
|
651
|
+
db;
|
|
652
|
+
/** True while the DB holds no usable index (a new file, or a schema-version mismatch dropped
|
|
653
|
+
* it) — the caller must run a full re-index before serving from it. Cleared by the first
|
|
654
|
+
* successful {@link indexDocument}. */
|
|
655
|
+
_stale;
|
|
656
|
+
get stale() {
|
|
657
|
+
return this._stale;
|
|
658
|
+
}
|
|
659
|
+
constructor(dbPath) {
|
|
660
|
+
this.db = new DatabaseSync(dbPath);
|
|
661
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
662
|
+
const ver = this.db.prepare("PRAGMA user_version").get().user_version;
|
|
663
|
+
if (ver !== SCHEMA_VERSION) {
|
|
664
|
+
this.db.exec("DROP TABLE IF EXISTS node; DROP TABLE IF EXISTS edge; DROP TABLE IF EXISTS file; DROP TABLE IF EXISTS dangling;");
|
|
665
|
+
this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION};`);
|
|
666
|
+
}
|
|
667
|
+
this.db.exec(SCHEMA);
|
|
668
|
+
this._stale = ver !== SCHEMA_VERSION;
|
|
669
|
+
}
|
|
670
|
+
close() {
|
|
671
|
+
this.db.close();
|
|
672
|
+
}
|
|
673
|
+
/** Rebuild the whole index from one resolved document: clear, then insert nodes + edges in
|
|
674
|
+
* a single transaction. (v1 = one document per DB; the directory walker feeds it; ENGINE.md
|
|
675
|
+
* derived/cache contract — re-runnable from scratch at any time.) `files` is the walk's file
|
|
676
|
+
* manifest (hash cache + diff base — replaced wholesale); pointers that fail to resolve are
|
|
677
|
+
* recorded in `dangling` instead of being silently dropped. */
|
|
678
|
+
indexDocument(doc, files) {
|
|
679
|
+
const insNode = this.db.prepare(
|
|
680
|
+
`INSERT OR REPLACE INTO node (path, type, format, value, content_hash, size, is_array, meta)
|
|
681
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
682
|
+
);
|
|
683
|
+
const insEdge = this.db.prepare(
|
|
684
|
+
`INSERT INTO edge (from_path, to_path, label, kind, pos) VALUES (?, ?, ?, ?, ?)`
|
|
685
|
+
);
|
|
686
|
+
this.db.exec("BEGIN");
|
|
687
|
+
try {
|
|
688
|
+
this.db.exec("DELETE FROM node; DELETE FROM edge; DELETE FROM dangling;");
|
|
689
|
+
if (files) this.db.exec("DELETE FROM file;");
|
|
690
|
+
walkNodes(doc.root, ":", (path8, node, parent, label, pos) => {
|
|
691
|
+
const meta = node.meta ? JSON.stringify(node.meta) : null;
|
|
692
|
+
const owned = node.entries?.filter((e2) => e2.edge !== "back") ?? [];
|
|
693
|
+
const isArray = node.array || node.kind === "mapping" && owned.length > 0 && owned.every((e2) => e2.key === null);
|
|
694
|
+
const value = node.kind === "scalar" ? JSON.stringify(node.value) : null;
|
|
695
|
+
const format = node.kind === "blob" ? node.format : formatFromMeta(node);
|
|
696
|
+
const hash = node.kind === "blob" ? node.contentHash : null;
|
|
697
|
+
const size = node.kind === "blob" ? node.size : null;
|
|
698
|
+
insNode.run(path8, node.kind, format, value, hash, size, isArray ? 1 : 0, meta);
|
|
699
|
+
if (parent !== null) insEdge.run(parent, path8, label, "contain", pos);
|
|
700
|
+
});
|
|
701
|
+
const insDangling = this.db.prepare("INSERT INTO dangling (from_path, raw, reason) VALUES (?, ?, ?)");
|
|
702
|
+
for (const r of resolveDocument(doc)) {
|
|
703
|
+
if (r.target.kind === "node") insEdge.run(r.holder, r.target.path, r.label, r.edge, r.pos);
|
|
704
|
+
else if (r.target.kind === "unresolved") insDangling.run(r.from, r.raw, r.target.reason);
|
|
705
|
+
}
|
|
706
|
+
if (files) {
|
|
707
|
+
const insFile = this.db.prepare("INSERT OR REPLACE INTO file (path, hash, size, mtime_ms) VALUES (?, ?, ?, ?)");
|
|
708
|
+
for (const f of files) insFile.run(f.path, f.hash, f.size, f.mtimeMs);
|
|
709
|
+
}
|
|
710
|
+
this.db.exec("COMMIT");
|
|
711
|
+
this._stale = false;
|
|
712
|
+
} catch (e2) {
|
|
713
|
+
this.db.exec("ROLLBACK");
|
|
714
|
+
throw e2;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/** Incrementally add ONE annotation document at `annStorePath`, with a forward `target` ref edge
|
|
718
|
+
* to `targetStorePath` and (when given) a keyless `back` edge to `tagStorePath` — the applied
|
|
719
|
+
* tag's `~-` membership. This avoids a full re-walk/rebuild (which re-reads and re-hashes every
|
|
720
|
+
* blob in the served tree, blocking the server) on every save. The root is FORCED to the
|
|
721
|
+
* annotation format so the material's backlink lookup ({@link relationships} `.in`) finds it; the
|
|
722
|
+
* `target`/`~-` pointers can't resolve in isolation (they are project-scoped `//…`), so their
|
|
723
|
+
* edges are added directly from the known targets. */
|
|
724
|
+
addAnnotation(annStorePath, targetStorePath, doc, tagStorePath) {
|
|
725
|
+
const insNode = this.db.prepare(
|
|
726
|
+
`INSERT OR REPLACE INTO node (path, type, format, value, content_hash, size, is_array, meta)
|
|
727
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
728
|
+
);
|
|
729
|
+
const insEdge = this.db.prepare(
|
|
730
|
+
`INSERT INTO edge (from_path, to_path, label, kind, pos) VALUES (?, ?, ?, ?, ?)`
|
|
731
|
+
);
|
|
732
|
+
this.db.exec("BEGIN");
|
|
733
|
+
try {
|
|
734
|
+
walkNodes(doc.root, annStorePath, (p, node, parent, label, pos) => {
|
|
735
|
+
const meta = node.meta ? JSON.stringify(node.meta) : null;
|
|
736
|
+
const isArray = node.array || node.kind === "mapping" && (node.entries?.every((e2) => e2.key === null) ?? false);
|
|
737
|
+
const value = node.kind === "scalar" ? JSON.stringify(node.value) : null;
|
|
738
|
+
const format = p === annStorePath ? "x-yamlover-annotation" : node.kind === "blob" ? node.format : formatFromMeta(node);
|
|
739
|
+
const hash = node.kind === "blob" ? node.contentHash : null;
|
|
740
|
+
const size = node.kind === "blob" ? node.size : null;
|
|
741
|
+
insNode.run(p, node.kind, format, value, hash, size, isArray ? 1 : 0, meta);
|
|
742
|
+
if (parent !== null) insEdge.run(parent, p, label, "contain", pos);
|
|
743
|
+
});
|
|
744
|
+
insEdge.run(annStorePath, targetStorePath, "target", "ref", 0);
|
|
745
|
+
if (tagStorePath) {
|
|
746
|
+
const pos = (doc.root.entries ?? []).findIndex((e2) => e2.edge === "back" && e2.key === null);
|
|
747
|
+
insEdge.run(annStorePath, tagStorePath, null, "back", pos >= 0 ? pos : 0);
|
|
748
|
+
}
|
|
749
|
+
this.db.exec("COMMIT");
|
|
750
|
+
} catch (e2) {
|
|
751
|
+
this.db.exec("ROLLBACK");
|
|
752
|
+
throw e2;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/** Incrementally remove one annotation: its node subtree and every edge touching it. */
|
|
756
|
+
removeAnnotation(annStorePath) {
|
|
757
|
+
const like = annStorePath + ":%";
|
|
758
|
+
this.db.exec("BEGIN");
|
|
759
|
+
try {
|
|
760
|
+
this.db.prepare("DELETE FROM node WHERE path = ? OR path LIKE ?").run(annStorePath, like);
|
|
761
|
+
this.db.prepare("DELETE FROM edge WHERE from_path = ? OR from_path LIKE ? OR to_path = ? OR to_path LIKE ?").run(annStorePath, like, annStorePath, like);
|
|
762
|
+
this.db.exec("COMMIT");
|
|
763
|
+
} catch (e2) {
|
|
764
|
+
this.db.exec("ROLLBACK");
|
|
765
|
+
throw e2;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/** Incrementally add ONE named tag under a taxonomy node (tag creation's write path — the
|
|
769
|
+
* {@link addAnnotation} of tags): upsert any missing taxonomy ancestors as plain mappings (a
|
|
770
|
+
* fresh `/tags` has no rows yet), then the tag's subtree and its containment edge. Approximate
|
|
771
|
+
* by design — the next reconcile re-walks the edited taxonomy body and trues everything up;
|
|
772
|
+
* these rows only have to answer queries until then. */
|
|
773
|
+
addTag(taxonomyStorePath, name, pos, node) {
|
|
774
|
+
const insNode = this.db.prepare(
|
|
775
|
+
`INSERT OR REPLACE INTO node (path, type, format, value, content_hash, size, is_array, meta)
|
|
776
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
777
|
+
);
|
|
778
|
+
const insEdge = this.db.prepare(
|
|
779
|
+
`INSERT INTO edge (from_path, to_path, label, kind, pos) VALUES (?, ?, ?, ?, ?)`
|
|
780
|
+
);
|
|
781
|
+
const hasNode = this.db.prepare("SELECT 1 FROM node WHERE path = ?");
|
|
782
|
+
this.db.exec("BEGIN");
|
|
783
|
+
try {
|
|
784
|
+
const segs = taxonomyStorePath === ":" ? [] : taxonomyStorePath.slice(1).split(":");
|
|
785
|
+
for (let i = 1; i <= segs.length; i++) {
|
|
786
|
+
const p = ":" + segs.slice(0, i).join(":");
|
|
787
|
+
if (hasNode.get(p)) continue;
|
|
788
|
+
insNode.run(p, "mapping", null, null, null, null, 0, null);
|
|
789
|
+
insEdge.run(i === 1 ? ":" : ":" + segs.slice(0, i - 1).join(":"), p, segs[i - 1], "contain", null);
|
|
790
|
+
}
|
|
791
|
+
const tagPath = (taxonomyStorePath === ":" ? "" : taxonomyStorePath) + ":" + name;
|
|
792
|
+
walkNodes(node, tagPath, (p, n, parent, label, ps) => {
|
|
793
|
+
const meta = n.meta ? JSON.stringify(n.meta) : null;
|
|
794
|
+
const isArray = n.array || n.kind === "mapping" && (n.entries?.every((e2) => e2.key === null) ?? false);
|
|
795
|
+
const value = n.kind === "scalar" ? JSON.stringify(n.value) : null;
|
|
796
|
+
const format = n.kind === "blob" ? n.format : formatFromMeta(n);
|
|
797
|
+
insNode.run(p, n.kind, format, value, n.kind === "blob" ? n.contentHash : null, n.kind === "blob" ? n.size : null, isArray ? 1 : 0, meta);
|
|
798
|
+
if (parent !== null) insEdge.run(parent, p, label, "contain", ps);
|
|
799
|
+
});
|
|
800
|
+
insEdge.run(taxonomyStorePath, tagPath, name, "contain", pos);
|
|
801
|
+
this.db.exec("COMMIT");
|
|
802
|
+
} catch (e2) {
|
|
803
|
+
this.db.exec("ROLLBACK");
|
|
804
|
+
throw e2;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
/** The persisted file manifest, keyed by root-relative path — the previous walk's view of the
|
|
808
|
+
* filesystem. Feeds the walker's hash cache and the change diff. */
|
|
809
|
+
manifest() {
|
|
810
|
+
const out = /* @__PURE__ */ new Map();
|
|
811
|
+
for (const r of this.db.prepare("SELECT * FROM file").all()) {
|
|
812
|
+
out.set(r.path, { path: r.path, hash: r.hash ?? null, size: r.size, mtimeMs: r.mtime_ms });
|
|
813
|
+
}
|
|
814
|
+
return out;
|
|
815
|
+
}
|
|
816
|
+
/** Manifest entries still lacking a content hash (large blobs the walk never read), smallest
|
|
817
|
+
* first so the background hasher shows progress early. */
|
|
818
|
+
unhashedFiles(limit = -1) {
|
|
819
|
+
return this.db.prepare("SELECT * FROM file WHERE hash IS NULL ORDER BY size ASC, path ASC LIMIT ?").all(limit).map((r) => ({ path: r.path, hash: null, size: r.size, mtimeMs: r.mtime_ms }));
|
|
820
|
+
}
|
|
821
|
+
/** Fill in one file's content hash after the fact (the background hasher): updates the
|
|
822
|
+
* manifest row and the matching blob node, guarded by (size, mtimeMs) — returns false
|
|
823
|
+
* (writing nothing) when the file changed since it was queued. */
|
|
824
|
+
setFileHash(relPath, hash, size, mtimeMs) {
|
|
825
|
+
this.db.exec("BEGIN");
|
|
826
|
+
try {
|
|
827
|
+
const r = this.db.prepare("UPDATE file SET hash = ? WHERE path = ? AND size = ? AND mtime_ms = ?").run(hash, relPath, size, mtimeMs);
|
|
828
|
+
if (Number(r.changes) === 0) {
|
|
829
|
+
this.db.exec("ROLLBACK");
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
this.db.prepare("UPDATE node SET content_hash = ? WHERE path = ? AND type = 'blob'").run(hash, ":" + relPath.split("/").join(":"));
|
|
833
|
+
this.db.exec("COMMIT");
|
|
834
|
+
return true;
|
|
835
|
+
} catch (e2) {
|
|
836
|
+
this.db.exec("ROLLBACK");
|
|
837
|
+
throw e2;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/** The pointers that failed to resolve at index time (ENGINE.md: reported, never dropped). */
|
|
841
|
+
dangling() {
|
|
842
|
+
return this.db.prepare("SELECT * FROM dangling").all().map((r) => ({
|
|
843
|
+
from: r.from_path,
|
|
844
|
+
raw: r.raw,
|
|
845
|
+
reason: r.reason
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
/** A node's attributes (null if no such path). */
|
|
849
|
+
node(path8) {
|
|
850
|
+
const row = this.db.prepare("SELECT * FROM node WHERE path = ?").get(path8);
|
|
851
|
+
return row ? rowToNode(row) : null;
|
|
852
|
+
}
|
|
853
|
+
/** Direct containment children of a node, in source order (by `pos`). */
|
|
854
|
+
children(path8) {
|
|
855
|
+
return this.db.prepare("SELECT to_path, label, pos FROM edge WHERE from_path = ? AND kind = 'contain' ORDER BY pos").all(path8).map((r) => ({ to: r.to_path, label: r.label ?? null, pos: r.pos ?? null }));
|
|
856
|
+
}
|
|
857
|
+
/** ALL of a node's entries in source order — containment children AND `*`/`~` pointer
|
|
858
|
+
* entries (ref/back). Unlike {@link children} this keeps positional pointers (`- *file`)
|
|
859
|
+
* in place, so an array that mixes inline values and pointers (e.g. a chapter's `chunks`)
|
|
860
|
+
* projects in full. `kind` says whether `to` is an owned child (contain) or a pointer target. */
|
|
861
|
+
entries(path8) {
|
|
862
|
+
return this.db.prepare("SELECT to_path, label, pos, kind FROM edge WHERE from_path = ? AND kind IN ('contain','ref','back') ORDER BY pos").all(path8).map((r) => ({ to: r.to_path, label: r.label ?? null, pos: r.pos ?? null, kind: r.kind }));
|
|
863
|
+
}
|
|
864
|
+
/** Whether a node has any containment children (expandable in the TOC). */
|
|
865
|
+
hasChildren(path8) {
|
|
866
|
+
const r = this.db.prepare("SELECT 1 FROM edge WHERE from_path = ? AND kind = 'contain' LIMIT 1").get(path8);
|
|
867
|
+
return r != null;
|
|
868
|
+
}
|
|
869
|
+
/** The containment subtree rooted at `path`, up to `depth` levels (TOC). A recursive CTE
|
|
870
|
+
* over `contain` edges — the acyclic spine; ordered by `pos` at each level. */
|
|
871
|
+
toc(path8 = ":", depth = Infinity) {
|
|
872
|
+
const rows = this.db.prepare(
|
|
873
|
+
`WITH RECURSIVE sub(from_path, to_path, label, pos, lvl) AS (
|
|
874
|
+
SELECT from_path, to_path, label, pos, 1 FROM edge
|
|
875
|
+
WHERE kind = 'contain' AND from_path = ?
|
|
876
|
+
UNION ALL
|
|
877
|
+
SELECT e.from_path, e.to_path, e.label, e.pos, sub.lvl + 1 FROM edge e
|
|
878
|
+
JOIN sub ON e.from_path = sub.to_path
|
|
879
|
+
WHERE e.kind = 'contain' AND sub.lvl < ?
|
|
880
|
+
)
|
|
881
|
+
SELECT s.to_path AS path, s.from_path AS parent, s.label, s.pos, n.type, n.format
|
|
882
|
+
FROM sub s JOIN node n ON n.path = s.to_path
|
|
883
|
+
ORDER BY s.lvl, s.from_path, s.pos`
|
|
884
|
+
).all(path8, depth === Infinity ? Number.MAX_SAFE_INTEGER : depth);
|
|
885
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
886
|
+
const root = { path: path8, label: null, type: this.node(path8)?.type ?? "mapping", format: null, children: [] };
|
|
887
|
+
byPath.set(path8, root);
|
|
888
|
+
for (const r of rows) {
|
|
889
|
+
const tn = {
|
|
890
|
+
path: r.path,
|
|
891
|
+
label: r.label ?? null,
|
|
892
|
+
type: r.type,
|
|
893
|
+
format: r.format ?? null,
|
|
894
|
+
children: []
|
|
895
|
+
};
|
|
896
|
+
byPath.set(tn.path, tn);
|
|
897
|
+
(byPath.get(r.parent) ?? root).children.push(tn);
|
|
898
|
+
}
|
|
899
|
+
return root.children;
|
|
900
|
+
}
|
|
901
|
+
/** Edges in/out of a node, by kind. `derived` inverses are computed here on demand (never
|
|
902
|
+
* stored) per ENGINE.md — for each ref/back into/out of the node, the reverse direction. */
|
|
903
|
+
relationships(path8) {
|
|
904
|
+
const out = this.db.prepare("SELECT * FROM edge WHERE from_path = ? AND kind != 'contain'").all(path8).map(rowToEdge);
|
|
905
|
+
const inc = this.db.prepare("SELECT * FROM edge WHERE to_path = ? AND kind != 'contain'").all(path8).map(rowToEdge);
|
|
906
|
+
const derivedIn = inc.filter((e2) => e2.kind === "ref" || e2.kind === "back").map((e2) => ({ from: e2.to, to: e2.from, label: e2.label, kind: "derived", pos: null }));
|
|
907
|
+
return { out, in: [...inc, ...derivedIn] };
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
function walkNodes(node, path8, visit, parent = null, label = null, pos = null) {
|
|
911
|
+
visit(path8, node, parent, label, pos);
|
|
912
|
+
if (!node.entries) return;
|
|
913
|
+
node.entries.forEach((e2, i) => {
|
|
914
|
+
if (isPointer(e2.value)) return;
|
|
915
|
+
const childPath = (path8 === ":" ? "" : path8) + (e2.key != null ? ":" + e2.key : "[" + i + "]");
|
|
916
|
+
walkNodes(e2.value, childPath, visit, path8, e2.key, i);
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
function formatFromMeta(node) {
|
|
920
|
+
const s = node.meta?.schema;
|
|
921
|
+
if (!s) return null;
|
|
922
|
+
if (isPointer(s)) {
|
|
923
|
+
const last = s.steps[s.steps.length - 1];
|
|
924
|
+
return last?.sel === "key" ? `x-yamlover-${last.name}` : null;
|
|
925
|
+
}
|
|
926
|
+
if (s.kind !== "mapping") return null;
|
|
927
|
+
const f = s.entries?.find((e2) => e2.key === "format");
|
|
928
|
+
if (f && !isPointer(f.value) && f.value.kind === "scalar") return String(f.value.value);
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
function rowToNode(r) {
|
|
932
|
+
return {
|
|
933
|
+
path: r.path,
|
|
934
|
+
type: r.type,
|
|
935
|
+
format: r.format ?? null,
|
|
936
|
+
value: r.value != null ? JSON.parse(r.value) : null,
|
|
937
|
+
content_hash: r.content_hash ?? null,
|
|
938
|
+
size: r.size ?? null,
|
|
939
|
+
is_array: !!r.is_array,
|
|
940
|
+
meta: r.meta != null ? JSON.parse(r.meta) : null
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function rowToEdge(r) {
|
|
944
|
+
return {
|
|
945
|
+
from: r.from_path,
|
|
946
|
+
to: r.to_path,
|
|
947
|
+
label: r.label ?? null,
|
|
948
|
+
kind: r.kind,
|
|
949
|
+
pos: r.pos ?? null
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ../engine/ts/src/walk.ts
|
|
954
|
+
import fs from "node:fs";
|
|
955
|
+
import path from "node:path";
|
|
956
|
+
import { setImmediate as yieldLoop } from "node:timers/promises";
|
|
957
|
+
|
|
958
|
+
// ../engine/ts/node_modules/xxhash-wasm/esm/xxhash-wasm.js
|
|
959
|
+
var t = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 48, 8, 96, 3, 127, 127, 127, 1, 127, 96, 3, 127, 127, 127, 0, 96, 2, 127, 127, 0, 96, 1, 127, 1, 127, 96, 3, 127, 127, 126, 1, 126, 96, 3, 126, 127, 127, 1, 126, 96, 2, 127, 126, 0, 96, 1, 127, 1, 126, 3, 11, 10, 0, 0, 2, 1, 3, 4, 5, 6, 1, 7, 5, 3, 1, 0, 1, 7, 85, 9, 3, 109, 101, 109, 2, 0, 5, 120, 120, 104, 51, 50, 0, 0, 6, 105, 110, 105, 116, 51, 50, 0, 2, 8, 117, 112, 100, 97, 116, 101, 51, 50, 0, 3, 8, 100, 105, 103, 101, 115, 116, 51, 50, 0, 4, 5, 120, 120, 104, 54, 52, 0, 5, 6, 105, 110, 105, 116, 54, 52, 0, 7, 8, 117, 112, 100, 97, 116, 101, 54, 52, 0, 8, 8, 100, 105, 103, 101, 115, 116, 54, 52, 0, 9, 10, 251, 22, 10, 242, 1, 1, 4, 127, 32, 0, 32, 1, 106, 33, 3, 32, 1, 65, 16, 79, 4, 127, 32, 3, 65, 16, 107, 33, 6, 32, 2, 65, 168, 136, 141, 161, 2, 106, 33, 3, 32, 2, 65, 137, 235, 208, 208, 7, 107, 33, 4, 32, 2, 65, 207, 140, 162, 142, 6, 106, 33, 5, 3, 64, 32, 3, 32, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 3, 32, 4, 32, 0, 65, 4, 106, 34, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 4, 32, 2, 32, 0, 65, 4, 106, 34, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 2, 32, 5, 32, 0, 65, 4, 106, 34, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 5, 32, 6, 32, 0, 65, 4, 106, 34, 0, 79, 13, 0, 11, 32, 2, 65, 12, 119, 32, 5, 65, 18, 119, 106, 32, 4, 65, 7, 119, 106, 32, 3, 65, 1, 119, 106, 5, 32, 2, 65, 177, 207, 217, 178, 1, 106, 11, 32, 1, 106, 32, 0, 32, 1, 65, 15, 113, 16, 1, 11, 146, 1, 0, 32, 1, 32, 2, 106, 33, 2, 3, 64, 32, 1, 65, 4, 106, 32, 2, 75, 69, 4, 64, 32, 0, 32, 1, 40, 2, 0, 65, 189, 220, 202, 149, 124, 108, 106, 65, 17, 119, 65, 175, 214, 211, 190, 2, 108, 33, 0, 32, 1, 65, 4, 106, 33, 1, 12, 1, 11, 11, 3, 64, 32, 1, 32, 2, 79, 69, 4, 64, 32, 0, 32, 1, 45, 0, 0, 65, 177, 207, 217, 178, 1, 108, 106, 65, 11, 119, 65, 177, 243, 221, 241, 121, 108, 33, 0, 32, 1, 65, 1, 106, 33, 1, 12, 1, 11, 11, 32, 0, 32, 0, 65, 15, 118, 115, 65, 247, 148, 175, 175, 120, 108, 34, 0, 65, 13, 118, 32, 0, 115, 65, 189, 220, 202, 149, 124, 108, 34, 0, 65, 16, 118, 32, 0, 115, 11, 63, 0, 32, 0, 65, 8, 106, 32, 1, 65, 168, 136, 141, 161, 2, 106, 54, 2, 0, 32, 0, 65, 12, 106, 32, 1, 65, 137, 235, 208, 208, 7, 107, 54, 2, 0, 32, 0, 65, 16, 106, 32, 1, 54, 2, 0, 32, 0, 65, 20, 106, 32, 1, 65, 207, 140, 162, 142, 6, 106, 54, 2, 0, 11, 195, 4, 1, 6, 127, 32, 1, 32, 2, 106, 33, 6, 32, 0, 65, 24, 106, 33, 4, 32, 0, 65, 40, 106, 40, 2, 0, 33, 3, 32, 0, 32, 0, 40, 2, 0, 32, 2, 106, 54, 2, 0, 32, 0, 65, 4, 106, 34, 5, 32, 5, 40, 2, 0, 32, 2, 65, 16, 79, 32, 0, 40, 2, 0, 65, 16, 79, 114, 114, 54, 2, 0, 32, 2, 32, 3, 106, 65, 16, 73, 4, 64, 32, 3, 32, 4, 106, 32, 1, 32, 2, 252, 10, 0, 0, 32, 0, 65, 40, 106, 32, 2, 32, 3, 106, 54, 2, 0, 15, 11, 32, 3, 4, 64, 32, 3, 32, 4, 106, 32, 1, 65, 16, 32, 3, 107, 34, 2, 252, 10, 0, 0, 32, 0, 65, 8, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 12, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 65, 4, 106, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 16, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 65, 8, 106, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 20, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 65, 12, 106, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 40, 106, 65, 0, 54, 2, 0, 32, 1, 32, 2, 106, 33, 1, 11, 32, 1, 32, 6, 65, 16, 107, 77, 4, 64, 32, 6, 65, 16, 107, 33, 8, 32, 0, 65, 8, 106, 40, 2, 0, 33, 2, 32, 0, 65, 12, 106, 40, 2, 0, 33, 3, 32, 0, 65, 16, 106, 40, 2, 0, 33, 5, 32, 0, 65, 20, 106, 40, 2, 0, 33, 7, 3, 64, 32, 2, 32, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 2, 32, 3, 32, 1, 65, 4, 106, 34, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 3, 32, 5, 32, 1, 65, 4, 106, 34, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 5, 32, 7, 32, 1, 65, 4, 106, 34, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 7, 32, 8, 32, 1, 65, 4, 106, 34, 1, 79, 13, 0, 11, 32, 0, 65, 8, 106, 32, 2, 54, 2, 0, 32, 0, 65, 12, 106, 32, 3, 54, 2, 0, 32, 0, 65, 16, 106, 32, 5, 54, 2, 0, 32, 0, 65, 20, 106, 32, 7, 54, 2, 0, 11, 32, 1, 32, 6, 73, 4, 64, 32, 4, 32, 1, 32, 6, 32, 1, 107, 34, 1, 252, 10, 0, 0, 32, 0, 65, 40, 106, 32, 1, 54, 2, 0, 11, 11, 97, 1, 1, 127, 32, 0, 65, 16, 106, 40, 2, 0, 33, 1, 32, 0, 65, 4, 106, 40, 2, 0, 4, 127, 32, 1, 65, 12, 119, 32, 0, 65, 20, 106, 40, 2, 0, 65, 18, 119, 106, 32, 0, 65, 12, 106, 40, 2, 0, 65, 7, 119, 106, 32, 0, 65, 8, 106, 40, 2, 0, 65, 1, 119, 106, 5, 32, 1, 65, 177, 207, 217, 178, 1, 106, 11, 32, 0, 40, 2, 0, 106, 32, 0, 65, 24, 106, 32, 0, 65, 40, 106, 40, 2, 0, 16, 1, 11, 255, 3, 2, 3, 126, 1, 127, 32, 0, 32, 1, 106, 33, 6, 32, 1, 65, 32, 79, 4, 126, 32, 6, 65, 32, 107, 33, 6, 32, 2, 66, 214, 235, 130, 238, 234, 253, 137, 245, 224, 0, 124, 33, 3, 32, 2, 66, 177, 169, 172, 193, 173, 184, 212, 166, 61, 125, 33, 4, 32, 2, 66, 249, 234, 208, 208, 231, 201, 161, 228, 225, 0, 124, 33, 5, 3, 64, 32, 3, 32, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 3, 32, 4, 32, 0, 65, 8, 106, 34, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 4, 32, 2, 32, 0, 65, 8, 106, 34, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 2, 32, 5, 32, 0, 65, 8, 106, 34, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 5, 32, 6, 32, 0, 65, 8, 106, 34, 0, 79, 13, 0, 11, 32, 2, 66, 12, 137, 32, 5, 66, 18, 137, 124, 32, 4, 66, 7, 137, 124, 32, 3, 66, 1, 137, 124, 32, 3, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 4, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 2, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 5, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 5, 32, 2, 66, 197, 207, 217, 178, 241, 229, 186, 234, 39, 124, 11, 32, 1, 173, 124, 32, 0, 32, 1, 65, 31, 113, 16, 6, 11, 134, 2, 0, 32, 1, 32, 2, 106, 33, 2, 3, 64, 32, 2, 32, 1, 65, 8, 106, 79, 4, 64, 32, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 32, 0, 133, 66, 27, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 33, 0, 32, 1, 65, 8, 106, 33, 1, 12, 1, 11, 11, 32, 1, 65, 4, 106, 32, 2, 77, 4, 64, 32, 0, 32, 1, 53, 2, 0, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 23, 137, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 249, 243, 221, 241, 153, 246, 153, 171, 22, 124, 33, 0, 32, 1, 65, 4, 106, 33, 1, 11, 3, 64, 32, 1, 32, 2, 73, 4, 64, 32, 0, 32, 1, 49, 0, 0, 66, 197, 207, 217, 178, 241, 229, 186, 234, 39, 126, 133, 66, 11, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 0, 32, 1, 65, 1, 106, 33, 1, 12, 1, 11, 11, 32, 0, 32, 0, 66, 33, 136, 133, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 34, 0, 32, 0, 66, 29, 136, 133, 66, 249, 243, 221, 241, 153, 246, 153, 171, 22, 126, 34, 0, 32, 0, 66, 32, 136, 133, 11, 77, 0, 32, 0, 65, 8, 106, 32, 1, 66, 214, 235, 130, 238, 234, 253, 137, 245, 224, 0, 124, 55, 3, 0, 32, 0, 65, 16, 106, 32, 1, 66, 177, 169, 172, 193, 173, 184, 212, 166, 61, 125, 55, 3, 0, 32, 0, 65, 24, 106, 32, 1, 55, 3, 0, 32, 0, 65, 32, 106, 32, 1, 66, 249, 234, 208, 208, 231, 201, 161, 228, 225, 0, 124, 55, 3, 0, 11, 244, 4, 2, 3, 127, 4, 126, 32, 1, 32, 2, 106, 33, 5, 32, 0, 65, 40, 106, 33, 4, 32, 0, 65, 200, 0, 106, 40, 2, 0, 33, 3, 32, 0, 32, 0, 41, 3, 0, 32, 2, 173, 124, 55, 3, 0, 32, 2, 32, 3, 106, 65, 32, 73, 4, 64, 32, 3, 32, 4, 106, 32, 1, 32, 2, 252, 10, 0, 0, 32, 0, 65, 200, 0, 106, 32, 2, 32, 3, 106, 54, 2, 0, 15, 11, 32, 3, 4, 64, 32, 3, 32, 4, 106, 32, 1, 65, 32, 32, 3, 107, 34, 2, 252, 10, 0, 0, 32, 0, 65, 8, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 16, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 65, 8, 106, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 24, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 65, 16, 106, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 32, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 65, 24, 106, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 200, 0, 106, 65, 0, 54, 2, 0, 32, 1, 32, 2, 106, 33, 1, 11, 32, 1, 65, 32, 106, 32, 5, 77, 4, 64, 32, 5, 65, 32, 107, 33, 2, 32, 0, 65, 8, 106, 41, 3, 0, 33, 6, 32, 0, 65, 16, 106, 41, 3, 0, 33, 7, 32, 0, 65, 24, 106, 41, 3, 0, 33, 8, 32, 0, 65, 32, 106, 41, 3, 0, 33, 9, 3, 64, 32, 6, 32, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 6, 32, 7, 32, 1, 65, 8, 106, 34, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 7, 32, 8, 32, 1, 65, 8, 106, 34, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 8, 32, 9, 32, 1, 65, 8, 106, 34, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 9, 32, 2, 32, 1, 65, 8, 106, 34, 1, 79, 13, 0, 11, 32, 0, 65, 8, 106, 32, 6, 55, 3, 0, 32, 0, 65, 16, 106, 32, 7, 55, 3, 0, 32, 0, 65, 24, 106, 32, 8, 55, 3, 0, 32, 0, 65, 32, 106, 32, 9, 55, 3, 0, 11, 32, 1, 32, 5, 73, 4, 64, 32, 4, 32, 1, 32, 5, 32, 1, 107, 34, 1, 252, 10, 0, 0, 32, 0, 65, 200, 0, 106, 32, 1, 54, 2, 0, 11, 11, 188, 2, 1, 5, 126, 32, 0, 65, 24, 106, 41, 3, 0, 33, 1, 32, 0, 41, 3, 0, 34, 2, 66, 32, 90, 4, 126, 32, 0, 65, 8, 106, 41, 3, 0, 34, 3, 66, 1, 137, 32, 0, 65, 16, 106, 41, 3, 0, 34, 4, 66, 7, 137, 124, 32, 1, 66, 12, 137, 32, 0, 65, 32, 106, 41, 3, 0, 34, 5, 66, 18, 137, 124, 124, 32, 3, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 4, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 1, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 5, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 5, 32, 1, 66, 197, 207, 217, 178, 241, 229, 186, 234, 39, 124, 11, 32, 2, 124, 32, 0, 65, 40, 106, 32, 2, 66, 31, 131, 167, 16, 6, 11]);
|
|
960
|
+
async function e() {
|
|
961
|
+
return function(t2) {
|
|
962
|
+
const { exports: { mem: e2, xxh32: n, xxh64: r, init32: i, update32: a, digest32: o, init64: s, update64: u, digest64: c } } = t2;
|
|
963
|
+
let h = new Uint8Array(e2.buffer);
|
|
964
|
+
function g(t3, n2) {
|
|
965
|
+
if (e2.buffer.byteLength < t3 + n2) {
|
|
966
|
+
const r2 = Math.ceil((t3 + n2 - e2.buffer.byteLength) / 65536);
|
|
967
|
+
e2.grow(r2), h = new Uint8Array(e2.buffer);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function f(t3, e3, n2, r2, i2, a2) {
|
|
971
|
+
g(t3);
|
|
972
|
+
const o2 = new Uint8Array(t3);
|
|
973
|
+
return h.set(o2), n2(0, e3), o2.set(h.subarray(0, t3)), { update(e4) {
|
|
974
|
+
let n3;
|
|
975
|
+
return h.set(o2), "string" == typeof e4 ? (g(3 * e4.length, t3), n3 = w.encodeInto(e4, h.subarray(t3)).written) : (g(e4.byteLength, t3), h.set(e4, t3), n3 = e4.byteLength), r2(0, t3, n3), o2.set(h.subarray(0, t3)), this;
|
|
976
|
+
}, digest: () => (h.set(o2), a2(i2(0))) };
|
|
977
|
+
}
|
|
978
|
+
function y(t3) {
|
|
979
|
+
return t3 >>> 0;
|
|
980
|
+
}
|
|
981
|
+
const b = 2n ** 64n - 1n;
|
|
982
|
+
function d(t3) {
|
|
983
|
+
return t3 & b;
|
|
984
|
+
}
|
|
985
|
+
const w = new TextEncoder(), l = 0, p = 0n;
|
|
986
|
+
function x(t3, e3 = l) {
|
|
987
|
+
return g(3 * t3.length, 0), y(n(0, w.encodeInto(t3, h).written, e3));
|
|
988
|
+
}
|
|
989
|
+
function L(t3, e3 = p) {
|
|
990
|
+
return g(3 * t3.length, 0), d(r(0, w.encodeInto(t3, h).written, e3));
|
|
991
|
+
}
|
|
992
|
+
return { h32: x, h32ToString: (t3, e3 = l) => x(t3, e3).toString(16).padStart(8, "0"), h32Raw: (t3, e3 = l) => (g(t3.byteLength, 0), h.set(t3), y(n(0, t3.byteLength, e3))), create32: (t3 = l) => f(48, t3, i, a, o, y), h64: L, h64ToString: (t3, e3 = p) => L(t3, e3).toString(16).padStart(16, "0"), h64Raw: (t3, e3 = p) => (g(t3.byteLength, 0), h.set(t3), d(r(0, t3.byteLength, e3))), create64: (t3 = p) => f(88, t3, s, u, c, d) };
|
|
993
|
+
}((await WebAssembly.instantiate(t)).instance);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ../parser/ts/src/pointer.ts
|
|
997
|
+
var LINK_RE = /^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\//;
|
|
998
|
+
function parsePointer(raw) {
|
|
999
|
+
if (looksColon(raw)) return parseColon(raw);
|
|
1000
|
+
const lm = LINK_RE.exec(raw);
|
|
1001
|
+
if (lm) {
|
|
1002
|
+
const after = raw.slice(lm[0].length);
|
|
1003
|
+
const { seg, rest } = readUntilSlash(after);
|
|
1004
|
+
const base2 = { scope: "link", authority: unescape(seg) };
|
|
1005
|
+
return { kind: "pointer", base: base2, steps: parsePath(rest), raw };
|
|
1006
|
+
}
|
|
1007
|
+
if (raw[0] === "/") {
|
|
1008
|
+
return { kind: "pointer", base: { scope: "document" }, steps: parsePath(raw), raw };
|
|
1009
|
+
}
|
|
1010
|
+
const segs = splitSlash(raw);
|
|
1011
|
+
let base = { scope: "current" };
|
|
1012
|
+
if (segs.length > 0 && segs[0] === "..") {
|
|
1013
|
+
base = { scope: "parent" };
|
|
1014
|
+
segs.shift();
|
|
1015
|
+
}
|
|
1016
|
+
const steps = [];
|
|
1017
|
+
for (const s of segs) {
|
|
1018
|
+
if (s === "") continue;
|
|
1019
|
+
steps.push(...segToSteps(s));
|
|
1020
|
+
}
|
|
1021
|
+
return { kind: "pointer", base, steps, raw };
|
|
1022
|
+
}
|
|
1023
|
+
function looksColon(raw) {
|
|
1024
|
+
if (raw.startsWith(":")) return true;
|
|
1025
|
+
if (LINK_RE.test(raw)) return false;
|
|
1026
|
+
let q = null;
|
|
1027
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1028
|
+
const c = raw[i];
|
|
1029
|
+
if (c === "\\" && q === null) {
|
|
1030
|
+
i++;
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (q !== null) {
|
|
1034
|
+
if (c === q) q = null;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (c === "'" || c === '"') {
|
|
1038
|
+
q = c;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (c === ":") return true;
|
|
1042
|
+
}
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
function parseColon(raw) {
|
|
1046
|
+
let rest = raw;
|
|
1047
|
+
let base;
|
|
1048
|
+
if (rest.startsWith(":::")) {
|
|
1049
|
+
rest = rest.slice(3);
|
|
1050
|
+
const portions2 = splitColon(rest);
|
|
1051
|
+
const auth = portions2.shift();
|
|
1052
|
+
if (auth === void 0 || auth === "") throw new SyntaxError(`pointer: ":::" needs an authority in "${raw}"`);
|
|
1053
|
+
base = { scope: "link", authority: portionName(auth), world: true };
|
|
1054
|
+
return { kind: "pointer", base, steps: portionsToSteps(portions2), raw };
|
|
1055
|
+
}
|
|
1056
|
+
if (rest.startsWith("::")) {
|
|
1057
|
+
rest = rest.slice(2);
|
|
1058
|
+
const portions2 = splitColon(rest);
|
|
1059
|
+
const auth = portions2.shift();
|
|
1060
|
+
if (auth === void 0 || auth === "") throw new SyntaxError(`pointer: "::" needs a first portion in "${raw}"`);
|
|
1061
|
+
base = { scope: "link", authority: portionName(auth) };
|
|
1062
|
+
return { kind: "pointer", base, steps: portionsToSteps(portions2), raw };
|
|
1063
|
+
}
|
|
1064
|
+
if (rest.startsWith(":")) {
|
|
1065
|
+
return { kind: "pointer", base: { scope: "document" }, steps: portionsToSteps(splitColon(rest.slice(1))), raw };
|
|
1066
|
+
}
|
|
1067
|
+
const portions = splitColon(rest);
|
|
1068
|
+
base = { scope: "current" };
|
|
1069
|
+
if (portions.length > 0 && portions[0] === "..") {
|
|
1070
|
+
base = { scope: "parent" };
|
|
1071
|
+
portions.shift();
|
|
1072
|
+
}
|
|
1073
|
+
return { kind: "pointer", base, steps: portionsToSteps(portions), raw };
|
|
1074
|
+
}
|
|
1075
|
+
function splitColon(s) {
|
|
1076
|
+
const out = [];
|
|
1077
|
+
let cur = "";
|
|
1078
|
+
let q = null;
|
|
1079
|
+
for (let i = 0; i < s.length; i++) {
|
|
1080
|
+
const c = s[i];
|
|
1081
|
+
if (q !== null) {
|
|
1082
|
+
cur += c;
|
|
1083
|
+
if (c === q) q = null;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
1087
|
+
cur += c + s[i + 1];
|
|
1088
|
+
i++;
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
if (c === "'" || c === '"') {
|
|
1092
|
+
q = c;
|
|
1093
|
+
cur += c;
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (c === ":") {
|
|
1097
|
+
out.push(cur.trim());
|
|
1098
|
+
cur = "";
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
cur += c;
|
|
1102
|
+
}
|
|
1103
|
+
out.push(cur.trim());
|
|
1104
|
+
return out.filter((p) => p !== "");
|
|
1105
|
+
}
|
|
1106
|
+
function portionsToSteps(portions) {
|
|
1107
|
+
const out = [];
|
|
1108
|
+
for (const p of portions) out.push(...portionToSteps(p));
|
|
1109
|
+
return out;
|
|
1110
|
+
}
|
|
1111
|
+
function portionToSteps(p) {
|
|
1112
|
+
if (p === "..") return [{ sel: "parent" }];
|
|
1113
|
+
let name = "";
|
|
1114
|
+
let i = 0;
|
|
1115
|
+
if (p[0] === "'" || p[0] === '"') {
|
|
1116
|
+
const q = p[0];
|
|
1117
|
+
i = 1;
|
|
1118
|
+
for (; i < p.length; i++) {
|
|
1119
|
+
if (p[i] === q) {
|
|
1120
|
+
if (q === "'" && p[i + 1] === "'") {
|
|
1121
|
+
name += q;
|
|
1122
|
+
i++;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
i++;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
if (q === '"' && p[i] === "\\" && i + 1 < p.length) {
|
|
1129
|
+
name += p[i + 1];
|
|
1130
|
+
i++;
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
name += p[i];
|
|
1134
|
+
}
|
|
1135
|
+
} else {
|
|
1136
|
+
for (; i < p.length; i++) {
|
|
1137
|
+
const c = p[i];
|
|
1138
|
+
if (c === "\\" && i + 1 < p.length) {
|
|
1139
|
+
name += p[i + 1];
|
|
1140
|
+
i++;
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
if (c === "[") break;
|
|
1144
|
+
if (c === " " || c === " ") throw new SyntaxError(`pointer: a key containing a space must be quoted ("${p}")`);
|
|
1145
|
+
name += c;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const steps = [];
|
|
1149
|
+
if (name !== "") steps.push({ sel: "key", name });
|
|
1150
|
+
while (i < p.length) {
|
|
1151
|
+
if (p[i] !== "[") throw new SyntaxError(`pointer: malformed portion "${p}"`);
|
|
1152
|
+
let j = i + 1;
|
|
1153
|
+
let digits = "";
|
|
1154
|
+
while (j < p.length && p[j] >= "0" && p[j] <= "9") {
|
|
1155
|
+
digits += p[j];
|
|
1156
|
+
j++;
|
|
1157
|
+
}
|
|
1158
|
+
if (digits === "" || p[j] !== "]") throw new SyntaxError(`pointer: malformed index in "${p}"`);
|
|
1159
|
+
steps.push({ sel: "index", n: Number.parseInt(digits, 10) });
|
|
1160
|
+
i = j + 1;
|
|
1161
|
+
}
|
|
1162
|
+
return steps;
|
|
1163
|
+
}
|
|
1164
|
+
function portionName(p) {
|
|
1165
|
+
const steps = portionToSteps(p);
|
|
1166
|
+
if (steps.length !== 1 || steps[0].sel !== "key") throw new SyntaxError(`pointer: bad authority portion "${p}"`);
|
|
1167
|
+
return steps[0].name;
|
|
1168
|
+
}
|
|
1169
|
+
function renderPointer(p, opts = {}) {
|
|
1170
|
+
const spaced = opts.spaced !== false;
|
|
1171
|
+
const sep2 = spaced ? ": " : ":";
|
|
1172
|
+
const toks = [];
|
|
1173
|
+
for (const st of p.steps) {
|
|
1174
|
+
if (st.sel === "parent") toks.push("..");
|
|
1175
|
+
else if (st.sel === "key") toks.push(colonSegment(st.name));
|
|
1176
|
+
else if (toks.length > 0 && !toks[toks.length - 1].endsWith("..")) toks[toks.length - 1] += `[${st.n}]`;
|
|
1177
|
+
else toks.push(`[${st.n}]`);
|
|
1178
|
+
}
|
|
1179
|
+
const body = toks.join(sep2);
|
|
1180
|
+
switch (p.base.scope) {
|
|
1181
|
+
case "current":
|
|
1182
|
+
return body;
|
|
1183
|
+
case "parent":
|
|
1184
|
+
return body === "" ? ".." : ".." + sep2 + body;
|
|
1185
|
+
case "document":
|
|
1186
|
+
return ":" + (body === "" ? "" : (spaced ? " " : "") + body);
|
|
1187
|
+
case "link": {
|
|
1188
|
+
const ladder = p.base.world === true ? ":::" : "::";
|
|
1189
|
+
const auth = colonSegment(p.base.authority);
|
|
1190
|
+
return ladder + (spaced ? " " : "") + auth + (body === "" ? "" : sep2 + body);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function colonSegment(name) {
|
|
1195
|
+
if (name === "..") return "\\.\\.";
|
|
1196
|
+
if (/[ \t]/.test(name) || /^['"]/.test(name)) return `'${name.replace(/'/g, "''")}'`;
|
|
1197
|
+
return name.replace(/[\\:[\]*&#~?!()<>=|]/g, (c) => "\\" + c);
|
|
1198
|
+
}
|
|
1199
|
+
function parsePath(path8) {
|
|
1200
|
+
const segs = splitSlash(path8);
|
|
1201
|
+
const out = [];
|
|
1202
|
+
for (let i = 0; i < segs.length; i++) {
|
|
1203
|
+
if (segs[i] === "") continue;
|
|
1204
|
+
out.push(...segToSteps(segs[i]));
|
|
1205
|
+
}
|
|
1206
|
+
return out;
|
|
1207
|
+
}
|
|
1208
|
+
function splitSlash(s) {
|
|
1209
|
+
const out = [];
|
|
1210
|
+
let cur = "";
|
|
1211
|
+
for (let i = 0; i < s.length; i++) {
|
|
1212
|
+
const c = s[i];
|
|
1213
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
1214
|
+
cur += c + s[i + 1];
|
|
1215
|
+
i++;
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (c === "/") {
|
|
1219
|
+
out.push(cur);
|
|
1220
|
+
cur = "";
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
cur += c;
|
|
1224
|
+
}
|
|
1225
|
+
out.push(cur);
|
|
1226
|
+
return out;
|
|
1227
|
+
}
|
|
1228
|
+
function readUntilSlash(s) {
|
|
1229
|
+
let i = 0;
|
|
1230
|
+
let seg = "";
|
|
1231
|
+
for (; i < s.length; i++) {
|
|
1232
|
+
const c = s[i];
|
|
1233
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
1234
|
+
seg += c + s[i + 1];
|
|
1235
|
+
i++;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (c === "/") break;
|
|
1239
|
+
seg += c;
|
|
1240
|
+
}
|
|
1241
|
+
return { seg, rest: s.slice(i) };
|
|
1242
|
+
}
|
|
1243
|
+
function segToSteps(seg) {
|
|
1244
|
+
if (seg === "..") return [{ sel: "parent" }];
|
|
1245
|
+
let i = 0;
|
|
1246
|
+
let name = "";
|
|
1247
|
+
for (; i < seg.length; i++) {
|
|
1248
|
+
const c = seg[i];
|
|
1249
|
+
if (c === "\\" && i + 1 < seg.length) {
|
|
1250
|
+
name += c + seg[i + 1];
|
|
1251
|
+
i++;
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
if (c === "[") break;
|
|
1255
|
+
name += c;
|
|
1256
|
+
}
|
|
1257
|
+
const steps = [];
|
|
1258
|
+
if (name !== "") steps.push({ sel: "key", name: unescape(name) });
|
|
1259
|
+
while (i < seg.length) {
|
|
1260
|
+
if (seg[i] !== "[") throw new SyntaxError(`pointer: malformed segment "${seg}"`);
|
|
1261
|
+
let j = i + 1;
|
|
1262
|
+
let digits = "";
|
|
1263
|
+
while (j < seg.length && seg[j] >= "0" && seg[j] <= "9") {
|
|
1264
|
+
digits += seg[j];
|
|
1265
|
+
j++;
|
|
1266
|
+
}
|
|
1267
|
+
if (digits === "" || seg[j] !== "]") throw new SyntaxError(`pointer: malformed index in "${seg}"`);
|
|
1268
|
+
steps.push({ sel: "index", n: Number.parseInt(digits, 10) });
|
|
1269
|
+
i = j + 1;
|
|
1270
|
+
}
|
|
1271
|
+
return steps;
|
|
1272
|
+
}
|
|
1273
|
+
function unescape(s) {
|
|
1274
|
+
let out = "";
|
|
1275
|
+
for (let i = 0; i < s.length; i++) {
|
|
1276
|
+
if (s[i] === "\\" && i + 1 < s.length) {
|
|
1277
|
+
out += s[i + 1];
|
|
1278
|
+
i++;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
out += s[i];
|
|
1282
|
+
}
|
|
1283
|
+
return out;
|
|
1284
|
+
}
|
|
1285
|
+
function makeAnchor(body, fail) {
|
|
1286
|
+
let ordinal = false;
|
|
1287
|
+
if (body.endsWith("[]") && !body.endsWith("\\[]")) {
|
|
1288
|
+
ordinal = true;
|
|
1289
|
+
body = body.slice(0, -2);
|
|
1290
|
+
}
|
|
1291
|
+
const path8 = parsePointer(body);
|
|
1292
|
+
if (!ordinal) {
|
|
1293
|
+
const last = path8.steps[path8.steps.length - 1];
|
|
1294
|
+
if (last === void 0) fail('an anchor path needs a key segment (or a trailing "[]" for ordinal membership)');
|
|
1295
|
+
if (last.sel !== "key") fail('an anchor may not claim a position \u2014 use a trailing "[]" for ordinal membership');
|
|
1296
|
+
}
|
|
1297
|
+
return { path: path8, ...ordinal ? { ordinal: true } : {} };
|
|
1298
|
+
}
|
|
1299
|
+
function escapeSegment(name) {
|
|
1300
|
+
if (name === "..") return "\\.\\.";
|
|
1301
|
+
return name.replace(/[\\/:[\]*&#~?!()<>=|]/g, (c) => "\\" + c);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// ../parser/ts/src/yamlover.ts
|
|
1305
|
+
function parseYamlover(src, uri = "<yamlover>") {
|
|
1306
|
+
const raw = [];
|
|
1307
|
+
const lineStarts = [];
|
|
1308
|
+
const sep2 = /\r\n|\r|\n/g;
|
|
1309
|
+
let at = 0;
|
|
1310
|
+
for (let m = sep2.exec(src); m !== null; m = sep2.exec(src)) {
|
|
1311
|
+
raw.push(src.slice(at, m.index));
|
|
1312
|
+
lineStarts.push(at);
|
|
1313
|
+
at = m.index + m[0].length;
|
|
1314
|
+
}
|
|
1315
|
+
raw.push(src.slice(at));
|
|
1316
|
+
lineStarts.push(at);
|
|
1317
|
+
const p = new Block(lex(raw), raw, uri, lineStarts);
|
|
1318
|
+
for (let k = 0; k < p.lines.length; k++) {
|
|
1319
|
+
const l = p.lines[k];
|
|
1320
|
+
if (l.indent === 0 && (l.text === "---" || l.text.startsWith("--- ") || l.text === "...")) {
|
|
1321
|
+
p.i = k;
|
|
1322
|
+
p.fail('multi-document streams ("---" / "...") are not yet supported (Phase 2c)');
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
let rootSchema;
|
|
1326
|
+
const first = p.peek();
|
|
1327
|
+
if (first && /^!!<[^>]*>$/.test(first.text.trim())) {
|
|
1328
|
+
rootSchema = parseSchemaRef(first.text.trim().slice(3, -1), { block: p, lineN: first.n, col: first.indent + 3 });
|
|
1329
|
+
p.i++;
|
|
1330
|
+
}
|
|
1331
|
+
let root = p.node(0) ?? nul();
|
|
1332
|
+
if (isPointer(root)) throw new SyntaxError(`yamlover: a top-level pointer is not allowed in ${uri}`);
|
|
1333
|
+
if (p.i < p.lines.length && p.peek().indent === 0) {
|
|
1334
|
+
root = p.mergeCont(root, p.container(0));
|
|
1335
|
+
}
|
|
1336
|
+
if (p.i < p.lines.length) p.fail("unexpected content");
|
|
1337
|
+
if (rootSchema !== void 0) root.meta = { ...root.meta, schema: rootSchema };
|
|
1338
|
+
return { root, source: { concrete: "yamlover", uri } };
|
|
1339
|
+
}
|
|
1340
|
+
function lex(raw) {
|
|
1341
|
+
const out = [];
|
|
1342
|
+
for (let n = 0; n < raw.length; n++) {
|
|
1343
|
+
const line = raw[n];
|
|
1344
|
+
let indent = 0;
|
|
1345
|
+
while (indent < line.length && line[indent] === " ") indent++;
|
|
1346
|
+
const content = stripComment(line.slice(indent)).replace(/\s+$/, "");
|
|
1347
|
+
if (content === "") continue;
|
|
1348
|
+
out.push({ indent, text: content, n });
|
|
1349
|
+
}
|
|
1350
|
+
return out;
|
|
1351
|
+
}
|
|
1352
|
+
function stripComment(s) {
|
|
1353
|
+
let inS = false;
|
|
1354
|
+
let inD = false;
|
|
1355
|
+
for (let i = 0; i < s.length; i++) {
|
|
1356
|
+
const c = s[i];
|
|
1357
|
+
if (inD && c === "\\") {
|
|
1358
|
+
i++;
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
if (c === "'" && !inD) inS = !inS;
|
|
1362
|
+
else if (c === '"' && !inS) inD = !inD;
|
|
1363
|
+
else if (c === "#" && !inS && !inD && (i === 0 || s[i - 1] === " " || s[i - 1] === " ")) {
|
|
1364
|
+
return s.slice(0, i);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return s;
|
|
1368
|
+
}
|
|
1369
|
+
var Block = class {
|
|
1370
|
+
lines;
|
|
1371
|
+
raw;
|
|
1372
|
+
// all source lines (for block scalars, which keep blanks and `#`)
|
|
1373
|
+
uri;
|
|
1374
|
+
// source path/id, surfaced in parse-error messages
|
|
1375
|
+
lineStarts;
|
|
1376
|
+
// absolute offset of each raw line's first character
|
|
1377
|
+
i = 0;
|
|
1378
|
+
constructor(lines, raw, uri = "<yamlover>", lineStarts = []) {
|
|
1379
|
+
this.lines = lines;
|
|
1380
|
+
this.raw = raw;
|
|
1381
|
+
this.uri = uri;
|
|
1382
|
+
this.lineStarts = lineStarts;
|
|
1383
|
+
}
|
|
1384
|
+
/** Absolute span of `len` chars starting at column `col` of raw line `lineN`. Valid
|
|
1385
|
+
* because every Line.text is a prefix-aligned slice of its raw line (lex strips only
|
|
1386
|
+
* the comment/trailing-ws SUFFIX), so tracked columns are raw-line columns. */
|
|
1387
|
+
spanAt(lineN, col, len) {
|
|
1388
|
+
const start = (this.lineStarts[lineN] ?? 0) + col;
|
|
1389
|
+
return { uri: this.uri, start, end: start + len };
|
|
1390
|
+
}
|
|
1391
|
+
peek() {
|
|
1392
|
+
return this.lines[this.i];
|
|
1393
|
+
}
|
|
1394
|
+
/** Read ONE `&` anchor token at the head of `text` (URIs.md §`&`): `&path`, `&path[]`,
|
|
1395
|
+
* or the quoted form `&'path with spaces'`. Plain tokens run to unescaped whitespace.
|
|
1396
|
+
* Returns the Anchor plus the remaining text/column. */
|
|
1397
|
+
anchorToken(text, lineN, col) {
|
|
1398
|
+
let body;
|
|
1399
|
+
let tokenLen;
|
|
1400
|
+
if (text[1] === "'" || text[1] === '"') {
|
|
1401
|
+
const q = text[1];
|
|
1402
|
+
let j = 2;
|
|
1403
|
+
while (j < text.length) {
|
|
1404
|
+
if (q === '"' && text[j] === "\\") {
|
|
1405
|
+
j += 2;
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if (text[j] === q) {
|
|
1409
|
+
if (q === "'" && text[j + 1] === "'") {
|
|
1410
|
+
j += 2;
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
j++;
|
|
1416
|
+
}
|
|
1417
|
+
if (j >= text.length) this.fail("unterminated quoted anchor path");
|
|
1418
|
+
body = quotedScalar(text.slice(1, j + 1)).value;
|
|
1419
|
+
tokenLen = j + 1;
|
|
1420
|
+
} else if (hasSeparatorColon(text.slice(1))) {
|
|
1421
|
+
body = text.slice(1).trim();
|
|
1422
|
+
tokenLen = text.length;
|
|
1423
|
+
} else {
|
|
1424
|
+
let j = 1;
|
|
1425
|
+
while (j < text.length && text[j] !== " " && text[j] !== " ") {
|
|
1426
|
+
if (text[j] === "\\" && j + 1 < text.length) j++;
|
|
1427
|
+
j++;
|
|
1428
|
+
}
|
|
1429
|
+
body = text.slice(1, j);
|
|
1430
|
+
tokenLen = j;
|
|
1431
|
+
}
|
|
1432
|
+
const anchor = makeAnchor(body, (m) => this.fail(m));
|
|
1433
|
+
anchor.path.span = this.spanAt(lineN, col, tokenLen);
|
|
1434
|
+
const a = adv(text, tokenLen, col);
|
|
1435
|
+
return { anchor, rest: a.rest, col: a.col };
|
|
1436
|
+
}
|
|
1437
|
+
/** Attach anchors to a (non-pointer) node's meta, appending to any it already has. */
|
|
1438
|
+
attachAnchors(value, anchors) {
|
|
1439
|
+
if (anchors.length === 0) return;
|
|
1440
|
+
if (isPointer(value)) this.fail("cannot anchor a pointer");
|
|
1441
|
+
value.meta = { ...value.meta, anchors: [...value.meta?.anchors ?? [], ...anchors] };
|
|
1442
|
+
}
|
|
1443
|
+
/** Merge a container() CONTINUATION into a node that already holds its value: entries
|
|
1444
|
+
* and anchors append. A continuation carrying its own value line is a second value —
|
|
1445
|
+
* an error — except the bare anchors-only case (a synthesized null with no raw). */
|
|
1446
|
+
mergeCont(v, cont) {
|
|
1447
|
+
const bare = cont.kind === "scalar" && cont.value === null && cont.raw === "" && (cont.entries?.length ?? 0) === 0;
|
|
1448
|
+
if (cont.kind === "scalar" && !bare) this.fail("a node may carry at most one scalar value line");
|
|
1449
|
+
const entries = [...v.entries ?? [], ...cont.entries ?? []];
|
|
1450
|
+
const out = {
|
|
1451
|
+
...v,
|
|
1452
|
+
...entries.length > 0 ? { entries, array: cont.kind === "mapping" ? cont.array : v.array } : {}
|
|
1453
|
+
};
|
|
1454
|
+
this.attachAnchors(out, cont.meta?.anchors ?? []);
|
|
1455
|
+
return out;
|
|
1456
|
+
}
|
|
1457
|
+
fail(msg) {
|
|
1458
|
+
const l = this.lines[this.i] ?? this.lines[this.i - 1];
|
|
1459
|
+
const where = l ? `${this.uri}:${l.n + 1}` : this.uri;
|
|
1460
|
+
const ctx = l ? `
|
|
1461
|
+
${l.n + 1} | ${this.raw[l.n]}` : "";
|
|
1462
|
+
throw new SyntaxError(`yamlover: ${msg} at ${where}${ctx}`);
|
|
1463
|
+
}
|
|
1464
|
+
/** Parse a block node whose content lives at a column >= minIndent (null if none). */
|
|
1465
|
+
node(minIndent) {
|
|
1466
|
+
const l = this.peek();
|
|
1467
|
+
if (!l || l.indent < minIndent) return null;
|
|
1468
|
+
if (/^!!(mix|omni|set)(?=\s|$)/.test(l.text)) {
|
|
1469
|
+
this.i++;
|
|
1470
|
+
return this.valueAfter(l.text, l.indent - 1, l.n, l.indent);
|
|
1471
|
+
}
|
|
1472
|
+
if (isSeqLine(l.text) || isBackSeqLine(l.text) || l.text.startsWith("&") || splitKV(l.text)) {
|
|
1473
|
+
return this.container(l.indent);
|
|
1474
|
+
}
|
|
1475
|
+
this.i++;
|
|
1476
|
+
let v = this.valueInline(
|
|
1477
|
+
l.text,
|
|
1478
|
+
l.indent,
|
|
1479
|
+
/*allowBlock*/
|
|
1480
|
+
true,
|
|
1481
|
+
l.n,
|
|
1482
|
+
l.indent
|
|
1483
|
+
);
|
|
1484
|
+
const nxt = this.peek();
|
|
1485
|
+
if (nxt && nxt.indent === l.indent && nxt.indent >= minIndent && !isPointer(v) && v.kind === "scalar") {
|
|
1486
|
+
v = this.mergeCont(v, this.container(l.indent));
|
|
1487
|
+
}
|
|
1488
|
+
return v;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* One ordered container at `indent` — yamlover's single container model. Entries keep
|
|
1492
|
+
* their order (→ integer keys [0],[1],…); each MAY also carry a string key. So keyless
|
|
1493
|
+
* (`- value`) and keyed (`key: value`) entries can be MIXED in one node ("partially
|
|
1494
|
+
* ordered, partially keyed"). `array` is just a projection hint: true iff all-keyless.
|
|
1495
|
+
*
|
|
1496
|
+
* `keylessOnly` is for the same-indent sequence-under-a-key case, where a keyed line at
|
|
1497
|
+
* this indent is a SIBLING of the outer key, not a member — so we stop at it.
|
|
1498
|
+
*/
|
|
1499
|
+
container(indent, keylessOnly = false) {
|
|
1500
|
+
const entries = [];
|
|
1501
|
+
const anchors = [];
|
|
1502
|
+
let self;
|
|
1503
|
+
for (; ; ) {
|
|
1504
|
+
const l = this.peek();
|
|
1505
|
+
if (!l || l.indent !== indent) break;
|
|
1506
|
+
let value;
|
|
1507
|
+
let entry;
|
|
1508
|
+
if (l.text.startsWith("&")) {
|
|
1509
|
+
if (keylessOnly) break;
|
|
1510
|
+
this.i++;
|
|
1511
|
+
let { rest, col } = { rest: l.text, col: l.indent };
|
|
1512
|
+
while (rest.startsWith("&")) {
|
|
1513
|
+
const r = this.anchorToken(rest, l.n, col);
|
|
1514
|
+
anchors.push(r.anchor);
|
|
1515
|
+
({ rest, col } = r);
|
|
1516
|
+
}
|
|
1517
|
+
if (rest !== "") {
|
|
1518
|
+
const v = this.valueInline(
|
|
1519
|
+
rest,
|
|
1520
|
+
indent,
|
|
1521
|
+
/*allowBlock*/
|
|
1522
|
+
false,
|
|
1523
|
+
l.n,
|
|
1524
|
+
col
|
|
1525
|
+
);
|
|
1526
|
+
if (isPointer(v) || v.kind !== "scalar") this.fail("only a scalar value may share a line with an own-line anchor");
|
|
1527
|
+
if (self !== void 0) this.fail("a node may carry at most one scalar value line");
|
|
1528
|
+
self = v;
|
|
1529
|
+
}
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
if (isSeqLine(l.text)) {
|
|
1533
|
+
this.i++;
|
|
1534
|
+
const afterDash = l.text.slice(1);
|
|
1535
|
+
const lead = afterDash.length - afterDash.trimStart().length;
|
|
1536
|
+
const contentCol = l.indent + 1 + lead;
|
|
1537
|
+
const rest = afterDash.trim();
|
|
1538
|
+
if (rest === "") {
|
|
1539
|
+
value = this.node(indent + 1) ?? nul();
|
|
1540
|
+
} else if (!/^(!!<|\*|&)/.test(rest) && splitKV(rest)) {
|
|
1541
|
+
this.i--;
|
|
1542
|
+
this.lines[this.i] = { indent: contentCol, text: rest, n: l.n };
|
|
1543
|
+
value = this.container(contentCol);
|
|
1544
|
+
} else {
|
|
1545
|
+
value = this.valueAfter(rest, indent, l.n, contentCol);
|
|
1546
|
+
}
|
|
1547
|
+
entry = { key: null, edge: isPointer(value) ? "ref" : "contain", value };
|
|
1548
|
+
} else if (isBackSeqLine(l.text)) {
|
|
1549
|
+
if (keylessOnly) break;
|
|
1550
|
+
this.i++;
|
|
1551
|
+
const a = adv(l.text, 2, l.indent);
|
|
1552
|
+
const rest = a.rest;
|
|
1553
|
+
if (!rest.startsWith("*")) this.fail('a "~-" entry needs a pointer value (the container that holds this node)');
|
|
1554
|
+
const ptr = parsePointer(unquoteIfQuoted(rest.slice(1)));
|
|
1555
|
+
ptr.span = this.spanAt(l.n, a.col, rest.length);
|
|
1556
|
+
value = ptr;
|
|
1557
|
+
entry = { key: null, edge: "back", value };
|
|
1558
|
+
} else {
|
|
1559
|
+
if (keylessOnly) break;
|
|
1560
|
+
if (/^~[ \t]/.test(l.text)) this.fail('the "~" sigil must sit tight against the key or "-" marker (write "~key:" or "~-")');
|
|
1561
|
+
const kv = splitKV(l.text);
|
|
1562
|
+
if (!kv) {
|
|
1563
|
+
this.i++;
|
|
1564
|
+
const v = this.valueInline(
|
|
1565
|
+
l.text,
|
|
1566
|
+
indent,
|
|
1567
|
+
/*allowBlock*/
|
|
1568
|
+
false,
|
|
1569
|
+
l.n,
|
|
1570
|
+
l.indent
|
|
1571
|
+
);
|
|
1572
|
+
if (isPointer(v) || v.kind !== "scalar") this.fail("unexpected content (expected an entry, an anchor, or a scalar value line)");
|
|
1573
|
+
if (self !== void 0) this.fail("a node may carry at most one scalar value line");
|
|
1574
|
+
self = v;
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
this.i++;
|
|
1578
|
+
let key = kv.key;
|
|
1579
|
+
let back = false;
|
|
1580
|
+
if (key.startsWith("~")) {
|
|
1581
|
+
back = true;
|
|
1582
|
+
key = key.slice(1);
|
|
1583
|
+
}
|
|
1584
|
+
value = this.valueAfter(kv.rest, indent, l.n, l.indent + kv.restCol);
|
|
1585
|
+
entry = { key: unquoteKey(key), edge: back ? "back" : isPointer(value) ? "ref" : "contain", value };
|
|
1586
|
+
}
|
|
1587
|
+
entries.push(entry);
|
|
1588
|
+
}
|
|
1589
|
+
const owned = entries.filter((e2) => e2.edge !== "back");
|
|
1590
|
+
const array = owned.length > 0 && owned.every((e2) => e2.key === null);
|
|
1591
|
+
const node = self !== void 0 ? { ...self, entries, array } : entries.length === 0 ? nul() : { kind: "mapping", entries, array };
|
|
1592
|
+
this.attachAnchors(node, anchors);
|
|
1593
|
+
return node;
|
|
1594
|
+
}
|
|
1595
|
+
/** The value after `key:` or `- ` (inline `rest`, with a possible deeper block).
|
|
1596
|
+
* `col` = the column of `rest` in raw line `srcLineN` (span tracking). */
|
|
1597
|
+
valueAfter(rest, parentIndent, srcLineN, col) {
|
|
1598
|
+
({ rest, col } = adv(rest, 0, col));
|
|
1599
|
+
let schema;
|
|
1600
|
+
if (rest.startsWith("!!<")) {
|
|
1601
|
+
const close = rest.indexOf(">");
|
|
1602
|
+
if (close < 0) this.fail('unterminated "!!<\u2026>" schema tag');
|
|
1603
|
+
schema = parseSchemaRef(rest.slice(3, close), { block: this, lineN: srcLineN, col: col + 3 });
|
|
1604
|
+
({ rest, col } = adv(rest, close + 1, col));
|
|
1605
|
+
}
|
|
1606
|
+
let typeTag;
|
|
1607
|
+
const tag = /^!!(mix|omni|set)(?=\s|$)/.exec(rest);
|
|
1608
|
+
if (tag) {
|
|
1609
|
+
typeTag = tag[1];
|
|
1610
|
+
({ rest, col } = adv(rest, tag[0].length, col));
|
|
1611
|
+
}
|
|
1612
|
+
const anchors = [];
|
|
1613
|
+
while (rest.startsWith("&")) {
|
|
1614
|
+
const r = this.anchorToken(rest, srcLineN, col);
|
|
1615
|
+
anchors.push(r.anchor);
|
|
1616
|
+
({ rest, col } = r);
|
|
1617
|
+
}
|
|
1618
|
+
let value;
|
|
1619
|
+
if (/^[|>][+-]?\d*$/.test(rest)) {
|
|
1620
|
+
value = this.blockScalar(rest, parentIndent, srcLineN);
|
|
1621
|
+
value = this.attachFields(value, parentIndent);
|
|
1622
|
+
} else if (rest === "") {
|
|
1623
|
+
const nxt = this.peek();
|
|
1624
|
+
if (nxt && nxt.indent === parentIndent && isSeqLine(nxt.text)) {
|
|
1625
|
+
value = this.container(
|
|
1626
|
+
parentIndent,
|
|
1627
|
+
/*keylessOnly*/
|
|
1628
|
+
true
|
|
1629
|
+
);
|
|
1630
|
+
} else {
|
|
1631
|
+
value = this.node(parentIndent + 1) ?? nul();
|
|
1632
|
+
}
|
|
1633
|
+
} else {
|
|
1634
|
+
value = this.valueInline(
|
|
1635
|
+
rest,
|
|
1636
|
+
parentIndent,
|
|
1637
|
+
/*allowBlock*/
|
|
1638
|
+
false,
|
|
1639
|
+
srcLineN,
|
|
1640
|
+
col
|
|
1641
|
+
);
|
|
1642
|
+
value = this.attachFields(value, parentIndent);
|
|
1643
|
+
}
|
|
1644
|
+
this.attachAnchors(value, anchors);
|
|
1645
|
+
if (typeTag === "set" && !isPointer(value)) value.meta = { ...value.meta, set: true };
|
|
1646
|
+
if (schema !== void 0 && !isPointer(value)) value.meta = { ...value.meta, schema };
|
|
1647
|
+
return value;
|
|
1648
|
+
}
|
|
1649
|
+
/** A node = value + fields: a scalar value (inline OR a block scalar) followed by a DEEPER
|
|
1650
|
+
* block attaches that block's (positional and/or keyed) entries onto the scalar — one node
|
|
1651
|
+
* carrying its value and its fields (the `!!omni` shape). Non-scalars/pointers pass through. */
|
|
1652
|
+
attachFields(value, parentIndent) {
|
|
1653
|
+
const nxt = this.peek();
|
|
1654
|
+
if (nxt && nxt.indent > parentIndent && !isPointer(value) && value.kind !== "blob") {
|
|
1655
|
+
return this.mergeCont(value, this.container(nxt.indent));
|
|
1656
|
+
}
|
|
1657
|
+
return value;
|
|
1658
|
+
}
|
|
1659
|
+
/** A block scalar (`|` literal / `>` folded, with `-`/`+` chomping) introduced by the
|
|
1660
|
+
* indicator on line `srcLineN`. Reads RAW lines (blanks + `#` are significant), de-indents
|
|
1661
|
+
* by the block's own indent, and advances past the consumed lines. */
|
|
1662
|
+
blockScalar(indicator, parentIndent, srcLineN) {
|
|
1663
|
+
const folded = indicator[0] === ">";
|
|
1664
|
+
let chomp = "clip";
|
|
1665
|
+
for (const ch of indicator.slice(1)) {
|
|
1666
|
+
if (ch === "-") chomp = "strip";
|
|
1667
|
+
else if (ch === "+") chomp = "keep";
|
|
1668
|
+
}
|
|
1669
|
+
const lines = [];
|
|
1670
|
+
let blockIndent = -1;
|
|
1671
|
+
let lastN = srcLineN;
|
|
1672
|
+
for (let n = srcLineN + 1; n < this.raw.length; n++) {
|
|
1673
|
+
const r = this.raw[n];
|
|
1674
|
+
let ind = 0;
|
|
1675
|
+
while (ind < r.length && r[ind] === " ") ind++;
|
|
1676
|
+
if (ind >= r.length) {
|
|
1677
|
+
lines.push("");
|
|
1678
|
+
lastN = n;
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
if (ind <= parentIndent) break;
|
|
1682
|
+
if (blockIndent >= 0 && ind < blockIndent) break;
|
|
1683
|
+
if (blockIndent < 0) blockIndent = ind;
|
|
1684
|
+
lines.push(r.slice(blockIndent));
|
|
1685
|
+
lastN = n;
|
|
1686
|
+
}
|
|
1687
|
+
while (this.peek() && this.peek().n <= lastN) this.i++;
|
|
1688
|
+
let last = -1;
|
|
1689
|
+
for (let i = 0; i < lines.length; i++) if (lines[i] !== "") last = i;
|
|
1690
|
+
const core = lines.slice(0, last + 1);
|
|
1691
|
+
let body = folded ? foldLines(core) : core.join("\n");
|
|
1692
|
+
if (chomp === "keep") body += "\n".repeat(lines.length - (last + 1) + (last >= 0 ? 1 : 0));
|
|
1693
|
+
else if (chomp === "clip" && last >= 0) body += "\n";
|
|
1694
|
+
return { kind: "scalar", value: body, raw: body };
|
|
1695
|
+
}
|
|
1696
|
+
/** Parse a single-line inline value: flow, pointer, anchor, quoted or plain scalar.
|
|
1697
|
+
* `col` = the column of `text` in raw line `lineN` (span tracking). */
|
|
1698
|
+
valueInline(text, parentIndent, allowBlock, lineN, col) {
|
|
1699
|
+
({ rest: text, col } = adv(text, 0, col));
|
|
1700
|
+
const c = text[0];
|
|
1701
|
+
if (c === "{" || c === "[") {
|
|
1702
|
+
return new Flow(text, this.uri, (this.lineStarts[lineN] ?? 0) + col).parse();
|
|
1703
|
+
}
|
|
1704
|
+
if (c === "*") {
|
|
1705
|
+
const p = parsePointer(unquoteIfQuoted(text.slice(1)));
|
|
1706
|
+
p.span = this.spanAt(lineN, col, text.length);
|
|
1707
|
+
return p;
|
|
1708
|
+
}
|
|
1709
|
+
if (c === "&") {
|
|
1710
|
+
const anchors = [];
|
|
1711
|
+
let rest = text;
|
|
1712
|
+
let acol = col;
|
|
1713
|
+
while (rest.startsWith("&")) {
|
|
1714
|
+
const r = this.anchorToken(rest, lineN, acol);
|
|
1715
|
+
anchors.push(r.anchor);
|
|
1716
|
+
({ rest, col: acol } = r);
|
|
1717
|
+
}
|
|
1718
|
+
const v = rest === "" ? allowBlock ? this.node(parentIndent + 1) ?? nul() : nul() : this.valueInline(rest, parentIndent, allowBlock, lineN, acol);
|
|
1719
|
+
this.attachAnchors(v, anchors);
|
|
1720
|
+
return v;
|
|
1721
|
+
}
|
|
1722
|
+
if (c === "'" || c === '"') return quotedScalar(text);
|
|
1723
|
+
return plainScalar(text);
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
var Flow = class {
|
|
1727
|
+
s;
|
|
1728
|
+
i = 0;
|
|
1729
|
+
uri;
|
|
1730
|
+
base;
|
|
1731
|
+
// absolute offset of s[0] in the source (span tracking)
|
|
1732
|
+
constructor(s, uri = "<flow>", base = 0) {
|
|
1733
|
+
this.s = s;
|
|
1734
|
+
this.uri = uri;
|
|
1735
|
+
this.base = base;
|
|
1736
|
+
}
|
|
1737
|
+
fail(msg) {
|
|
1738
|
+
throw new SyntaxError(`yamlover (flow): ${msg} at offset ${this.i}`);
|
|
1739
|
+
}
|
|
1740
|
+
parse() {
|
|
1741
|
+
this.ws();
|
|
1742
|
+
const v = this.value();
|
|
1743
|
+
this.ws();
|
|
1744
|
+
if (this.i < this.s.length) this.fail("trailing characters in flow value");
|
|
1745
|
+
return v;
|
|
1746
|
+
}
|
|
1747
|
+
ws() {
|
|
1748
|
+
while (this.i < this.s.length && " ".includes(this.s[this.i])) this.i++;
|
|
1749
|
+
}
|
|
1750
|
+
value() {
|
|
1751
|
+
const c = this.s[this.i];
|
|
1752
|
+
if (c === "{") return this.map();
|
|
1753
|
+
if (c === "[") return this.seq();
|
|
1754
|
+
if (c === "*") {
|
|
1755
|
+
const start = this.i;
|
|
1756
|
+
this.i++;
|
|
1757
|
+
let txt;
|
|
1758
|
+
if (this.s[this.i] === "'" || this.s[this.i] === '"') {
|
|
1759
|
+
txt = this.quoted().value;
|
|
1760
|
+
} else {
|
|
1761
|
+
const st = this.i;
|
|
1762
|
+
let depth = 0;
|
|
1763
|
+
while (this.i < this.s.length) {
|
|
1764
|
+
const ch = this.s[this.i];
|
|
1765
|
+
if (ch === "\\" && this.i + 1 < this.s.length) {
|
|
1766
|
+
this.i += 2;
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
if (ch === "[") depth++;
|
|
1770
|
+
else if (ch === "]") {
|
|
1771
|
+
if (depth === 0) break;
|
|
1772
|
+
depth--;
|
|
1773
|
+
} else if (ch === "," || ch === "}") break;
|
|
1774
|
+
this.i++;
|
|
1775
|
+
}
|
|
1776
|
+
txt = this.s.slice(st, this.i).trim();
|
|
1777
|
+
}
|
|
1778
|
+
const p = parsePointer(txt);
|
|
1779
|
+
p.span = { uri: this.uri, start: this.base + start, end: this.base + this.i };
|
|
1780
|
+
return p;
|
|
1781
|
+
}
|
|
1782
|
+
if (c === "&") {
|
|
1783
|
+
const start = this.i;
|
|
1784
|
+
this.i++;
|
|
1785
|
+
let body;
|
|
1786
|
+
if (this.s[this.i] === "'" || this.s[this.i] === '"') {
|
|
1787
|
+
body = this.quoted().value;
|
|
1788
|
+
} else {
|
|
1789
|
+
const st = this.i;
|
|
1790
|
+
while (this.i < this.s.length && !" ,]}".includes(this.s[this.i])) {
|
|
1791
|
+
if (this.s[this.i] === "\\" && this.i + 1 < this.s.length) this.i++;
|
|
1792
|
+
this.i++;
|
|
1793
|
+
}
|
|
1794
|
+
body = this.s.slice(st, this.i);
|
|
1795
|
+
}
|
|
1796
|
+
const anchor = makeAnchor(body, (m) => this.fail(m));
|
|
1797
|
+
anchor.path.span = { uri: this.uri, start: this.base + start, end: this.base + this.i };
|
|
1798
|
+
this.ws();
|
|
1799
|
+
const v = this.value();
|
|
1800
|
+
if (isPointer(v)) this.fail("cannot anchor a pointer");
|
|
1801
|
+
v.meta = { ...v.meta, anchors: [...v.meta?.anchors ?? [], anchor] };
|
|
1802
|
+
return v;
|
|
1803
|
+
}
|
|
1804
|
+
if (c === "'" || c === '"') return this.quoted();
|
|
1805
|
+
const tok = this.readPlain();
|
|
1806
|
+
if (tok === "") this.fail("expected a value");
|
|
1807
|
+
return plainScalar(tok);
|
|
1808
|
+
}
|
|
1809
|
+
map() {
|
|
1810
|
+
this.i++;
|
|
1811
|
+
const entries = [];
|
|
1812
|
+
for (; ; ) {
|
|
1813
|
+
this.ws();
|
|
1814
|
+
if (this.s[this.i] === "}") {
|
|
1815
|
+
this.i++;
|
|
1816
|
+
break;
|
|
1817
|
+
}
|
|
1818
|
+
if (this.i >= this.s.length) this.fail("unterminated flow map");
|
|
1819
|
+
let back = false;
|
|
1820
|
+
if (this.s[this.i] === "~") {
|
|
1821
|
+
back = true;
|
|
1822
|
+
this.i++;
|
|
1823
|
+
}
|
|
1824
|
+
const key = this.s[this.i] === "'" || this.s[this.i] === '"' ? this.quoted().value : unquoteKey(this.readPlain(":,}"));
|
|
1825
|
+
this.ws();
|
|
1826
|
+
if (this.s[this.i] !== ":") this.fail('expected ":" in flow map');
|
|
1827
|
+
this.i++;
|
|
1828
|
+
this.ws();
|
|
1829
|
+
const v = this.value();
|
|
1830
|
+
entries.push({ key, edge: back ? "back" : isPointer(v) ? "ref" : "contain", value: v });
|
|
1831
|
+
this.ws();
|
|
1832
|
+
if (this.s[this.i] === ",") {
|
|
1833
|
+
this.i++;
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
if (this.s[this.i] === "}") {
|
|
1837
|
+
this.i++;
|
|
1838
|
+
break;
|
|
1839
|
+
}
|
|
1840
|
+
this.fail('expected "," or "}"');
|
|
1841
|
+
}
|
|
1842
|
+
return { kind: "mapping", entries, array: false };
|
|
1843
|
+
}
|
|
1844
|
+
seq() {
|
|
1845
|
+
this.i++;
|
|
1846
|
+
const entries = [];
|
|
1847
|
+
for (; ; ) {
|
|
1848
|
+
this.ws();
|
|
1849
|
+
if (this.s[this.i] === "]") {
|
|
1850
|
+
this.i++;
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
if (this.i >= this.s.length) this.fail("unterminated flow seq");
|
|
1854
|
+
const v = this.value();
|
|
1855
|
+
entries.push({ key: null, edge: isPointer(v) ? "ref" : "contain", value: v });
|
|
1856
|
+
this.ws();
|
|
1857
|
+
if (this.s[this.i] === ",") {
|
|
1858
|
+
this.i++;
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (this.s[this.i] === "]") {
|
|
1862
|
+
this.i++;
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
this.fail('expected "," or "]"');
|
|
1866
|
+
}
|
|
1867
|
+
return { kind: "mapping", entries, array: true };
|
|
1868
|
+
}
|
|
1869
|
+
quoted() {
|
|
1870
|
+
const start = this.i;
|
|
1871
|
+
const q = this.s[this.i];
|
|
1872
|
+
this.i++;
|
|
1873
|
+
while (this.i < this.s.length) {
|
|
1874
|
+
const c = this.s[this.i];
|
|
1875
|
+
if (q === '"' && c === "\\") {
|
|
1876
|
+
this.i += 2;
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
if (q === "'" && c === "'" && this.s[this.i + 1] === "'") {
|
|
1880
|
+
this.i += 2;
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
this.i++;
|
|
1884
|
+
if (c === q) break;
|
|
1885
|
+
}
|
|
1886
|
+
return quotedScalar(this.s.slice(start, this.i));
|
|
1887
|
+
}
|
|
1888
|
+
/** Read a plain scalar up to a stop char (default flow stops). */
|
|
1889
|
+
readPlain(stop = ",:[]{} ") {
|
|
1890
|
+
const start = this.i;
|
|
1891
|
+
while (this.i < this.s.length && !stop.includes(this.s[this.i])) this.i++;
|
|
1892
|
+
return this.s.slice(start, this.i).trim();
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
function isSeqLine(text) {
|
|
1896
|
+
return text === "-" || text.startsWith("- ");
|
|
1897
|
+
}
|
|
1898
|
+
function isBackSeqLine(text) {
|
|
1899
|
+
return text === "~-" || text.startsWith("~- ");
|
|
1900
|
+
}
|
|
1901
|
+
function splitKV(text) {
|
|
1902
|
+
let inS = false;
|
|
1903
|
+
let inD = false;
|
|
1904
|
+
for (let i = 0; i < text.length; i++) {
|
|
1905
|
+
const c = text[i];
|
|
1906
|
+
if (c === "'" && !inD) inS = !inS;
|
|
1907
|
+
else if (c === '"' && !inS) inD = !inD;
|
|
1908
|
+
else if (c === ":" && !inS && !inD) {
|
|
1909
|
+
const next = text[i + 1];
|
|
1910
|
+
if (next === void 0 || next === " " || next === " ") {
|
|
1911
|
+
const after = text.slice(i + 1);
|
|
1912
|
+
const lead = after.length - after.trimStart().length;
|
|
1913
|
+
return { key: text.slice(0, i).trim(), rest: after.trim(), restCol: i + 1 + lead };
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
function adv(s, k, col) {
|
|
1920
|
+
const sliced = s.slice(k);
|
|
1921
|
+
const lead = sliced.length - sliced.trimStart().length;
|
|
1922
|
+
return { rest: sliced.trim(), col: col + k + lead };
|
|
1923
|
+
}
|
|
1924
|
+
function parseSchemaRef(src, at) {
|
|
1925
|
+
const lead = src.length - src.trimStart().length;
|
|
1926
|
+
src = src.trim();
|
|
1927
|
+
if (src.startsWith("*")) {
|
|
1928
|
+
const p = parsePointer(src.slice(1));
|
|
1929
|
+
if (at !== void 0) p.span = at.block.spanAt(at.lineN, at.col + lead, src.length);
|
|
1930
|
+
return p;
|
|
1931
|
+
}
|
|
1932
|
+
return parseYamlover(src).root;
|
|
1933
|
+
}
|
|
1934
|
+
function foldLines(lines) {
|
|
1935
|
+
let out = "";
|
|
1936
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1937
|
+
if (i > 0) out += lines[i] === "" || lines[i - 1] === "" ? "\n" : " ";
|
|
1938
|
+
out += lines[i];
|
|
1939
|
+
}
|
|
1940
|
+
return out;
|
|
1941
|
+
}
|
|
1942
|
+
function unquoteIfQuoted(s) {
|
|
1943
|
+
s = s.trim();
|
|
1944
|
+
if ((s[0] === "'" || s[0] === '"') && s[s.length - 1] === s[0]) return quotedScalar(s).value;
|
|
1945
|
+
return s;
|
|
1946
|
+
}
|
|
1947
|
+
function hasSeparatorColon(s) {
|
|
1948
|
+
let q = null;
|
|
1949
|
+
for (let i = 0; i < s.length; i++) {
|
|
1950
|
+
const c = s[i];
|
|
1951
|
+
if (q !== null) {
|
|
1952
|
+
if (c === q) q = null;
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
if (c === "\\") {
|
|
1956
|
+
i++;
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
if (c === "'" || c === '"') {
|
|
1960
|
+
q = c;
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
if (c === ":") return true;
|
|
1964
|
+
}
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
function unquoteKey(key) {
|
|
1968
|
+
key = key.trim();
|
|
1969
|
+
if ((key[0] === "'" || key[0] === '"') && key[key.length - 1] === key[0]) return quotedScalar(key).value;
|
|
1970
|
+
return backslashUnescape(key);
|
|
1971
|
+
}
|
|
1972
|
+
function quotedScalar(text) {
|
|
1973
|
+
const q = text[0];
|
|
1974
|
+
const body = text.slice(1, text.length - 1);
|
|
1975
|
+
if (q === "'") return { kind: "scalar", value: body.replace(/''/g, "'"), raw: text };
|
|
1976
|
+
let out = "";
|
|
1977
|
+
for (let i = 0; i < body.length; i++) {
|
|
1978
|
+
if (body[i] === "\\") {
|
|
1979
|
+
const c = body[++i];
|
|
1980
|
+
switch (c) {
|
|
1981
|
+
case "n":
|
|
1982
|
+
out += "\n";
|
|
1983
|
+
break;
|
|
1984
|
+
case "t":
|
|
1985
|
+
out += " ";
|
|
1986
|
+
break;
|
|
1987
|
+
case "r":
|
|
1988
|
+
out += "\r";
|
|
1989
|
+
break;
|
|
1990
|
+
case '"':
|
|
1991
|
+
out += '"';
|
|
1992
|
+
break;
|
|
1993
|
+
case "\\":
|
|
1994
|
+
out += "\\";
|
|
1995
|
+
break;
|
|
1996
|
+
case "/":
|
|
1997
|
+
out += "/";
|
|
1998
|
+
break;
|
|
1999
|
+
case "b":
|
|
2000
|
+
out += "\b";
|
|
2001
|
+
break;
|
|
2002
|
+
case "f":
|
|
2003
|
+
out += "\f";
|
|
2004
|
+
break;
|
|
2005
|
+
case "0":
|
|
2006
|
+
out += "\0";
|
|
2007
|
+
break;
|
|
2008
|
+
case "u":
|
|
2009
|
+
out += String.fromCharCode(parseInt(body.slice(i + 1, i + 5), 16));
|
|
2010
|
+
i += 4;
|
|
2011
|
+
break;
|
|
2012
|
+
case "x":
|
|
2013
|
+
out += String.fromCharCode(parseInt(body.slice(i + 1, i + 3), 16));
|
|
2014
|
+
i += 2;
|
|
2015
|
+
break;
|
|
2016
|
+
default:
|
|
2017
|
+
out += c ?? "";
|
|
2018
|
+
}
|
|
2019
|
+
} else out += body[i];
|
|
2020
|
+
}
|
|
2021
|
+
return { kind: "scalar", value: out, raw: text };
|
|
2022
|
+
}
|
|
2023
|
+
function plainScalar(text) {
|
|
2024
|
+
const t2 = text.trim();
|
|
2025
|
+
if (t2 === "" || t2 === "~" || t2 === "null" || t2 === "Null" || t2 === "NULL") return { kind: "scalar", value: null, raw: text };
|
|
2026
|
+
if (t2 === "true" || t2 === "True" || t2 === "TRUE") return { kind: "scalar", value: true, raw: text };
|
|
2027
|
+
if (t2 === "false" || t2 === "False" || t2 === "FALSE") return { kind: "scalar", value: false, raw: text };
|
|
2028
|
+
if (/^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(t2)) return { kind: "scalar", value: Number(t2), raw: text };
|
|
2029
|
+
if (/^[-+]?0x[0-9a-fA-F]+$/.test(t2)) return { kind: "scalar", value: Number(t2), raw: text };
|
|
2030
|
+
return { kind: "scalar", value: t2, raw: text };
|
|
2031
|
+
}
|
|
2032
|
+
function backslashUnescape(s) {
|
|
2033
|
+
let out = "";
|
|
2034
|
+
for (let i = 0; i < s.length; i++) {
|
|
2035
|
+
if (s[i] === "\\" && i + 1 < s.length) {
|
|
2036
|
+
out += s[i + 1];
|
|
2037
|
+
i++;
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
out += s[i];
|
|
2041
|
+
}
|
|
2042
|
+
return out;
|
|
2043
|
+
}
|
|
2044
|
+
function nul() {
|
|
2045
|
+
return { kind: "scalar", value: null, raw: "" };
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// ../parser/ts/src/json5p.ts
|
|
2049
|
+
var WS = /* @__PURE__ */ new Set([" ", " ", "\n", "\r", "\v", "\f", "\xA0", "\uFEFF"]);
|
|
2050
|
+
function parseJson5p(src, uri = "<json5p>") {
|
|
2051
|
+
const p = new Parser(src, uri);
|
|
2052
|
+
p.ws();
|
|
2053
|
+
const root = p.value();
|
|
2054
|
+
p.ws();
|
|
2055
|
+
if (p.i < src.length) p.fail("trailing characters");
|
|
2056
|
+
if (isPointer(root)) return p.fail("a top-level pointer is not allowed");
|
|
2057
|
+
return { root, source: { concrete: "json5p", uri } };
|
|
2058
|
+
}
|
|
2059
|
+
var Parser = class {
|
|
2060
|
+
src;
|
|
2061
|
+
uri;
|
|
2062
|
+
i = 0;
|
|
2063
|
+
constructor(src, uri) {
|
|
2064
|
+
this.src = src;
|
|
2065
|
+
this.uri = uri;
|
|
2066
|
+
}
|
|
2067
|
+
fail(msg) {
|
|
2068
|
+
throw new SyntaxError(`json5p: ${msg} at offset ${this.i}`);
|
|
2069
|
+
}
|
|
2070
|
+
peek() {
|
|
2071
|
+
return this.src[this.i];
|
|
2072
|
+
}
|
|
2073
|
+
eof() {
|
|
2074
|
+
return this.i >= this.src.length;
|
|
2075
|
+
}
|
|
2076
|
+
ws() {
|
|
2077
|
+
for (; ; ) {
|
|
2078
|
+
const c = this.src[this.i];
|
|
2079
|
+
if (c === void 0) return;
|
|
2080
|
+
if (WS.has(c)) {
|
|
2081
|
+
this.i++;
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (c === "/" && this.src[this.i + 1] === "/") {
|
|
2085
|
+
this.i += 2;
|
|
2086
|
+
while (this.i < this.src.length && this.src[this.i] !== "\n" && this.src[this.i] !== "\r") this.i++;
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
if (c === "/" && this.src[this.i + 1] === "*") {
|
|
2090
|
+
this.i += 2;
|
|
2091
|
+
while (this.i < this.src.length && !(this.src[this.i] === "*" && this.src[this.i + 1] === "/")) this.i++;
|
|
2092
|
+
if (this.i >= this.src.length) this.fail("unterminated block comment");
|
|
2093
|
+
this.i += 2;
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
value() {
|
|
2100
|
+
const c = this.peek();
|
|
2101
|
+
if (c === void 0) this.fail("unexpected end of input");
|
|
2102
|
+
if (c === "{") return this.object();
|
|
2103
|
+
if (c === "[") return this.array();
|
|
2104
|
+
if (c === "*") return this.pointer();
|
|
2105
|
+
if (c === "&") return this.anchored();
|
|
2106
|
+
if (c === '"' || c === "'") return this.string();
|
|
2107
|
+
return this.numberOrKeyword();
|
|
2108
|
+
}
|
|
2109
|
+
object() {
|
|
2110
|
+
this.i++;
|
|
2111
|
+
const entries = [];
|
|
2112
|
+
for (; ; ) {
|
|
2113
|
+
this.ws();
|
|
2114
|
+
const c = this.peek();
|
|
2115
|
+
if (c === "}") {
|
|
2116
|
+
this.i++;
|
|
2117
|
+
break;
|
|
2118
|
+
}
|
|
2119
|
+
if (c === void 0) this.fail("unterminated object");
|
|
2120
|
+
let back = false;
|
|
2121
|
+
if (this.peek() === "~") {
|
|
2122
|
+
back = true;
|
|
2123
|
+
this.i++;
|
|
2124
|
+
}
|
|
2125
|
+
if (back && this.peek() === "*") {
|
|
2126
|
+
entries.push({ key: null, edge: "back", value: this.pointer() });
|
|
2127
|
+
} else {
|
|
2128
|
+
const key = this.key();
|
|
2129
|
+
this.ws();
|
|
2130
|
+
if (this.peek() !== ":") this.fail('expected ":" after key');
|
|
2131
|
+
this.i++;
|
|
2132
|
+
this.ws();
|
|
2133
|
+
const v = this.value();
|
|
2134
|
+
entries.push(makeEntry(key, back, v));
|
|
2135
|
+
}
|
|
2136
|
+
this.ws();
|
|
2137
|
+
const n = this.peek();
|
|
2138
|
+
if (n === ",") {
|
|
2139
|
+
this.i++;
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
if (n === "}") {
|
|
2143
|
+
this.i++;
|
|
2144
|
+
break;
|
|
2145
|
+
}
|
|
2146
|
+
this.fail('expected "," or "}"');
|
|
2147
|
+
}
|
|
2148
|
+
return { kind: "mapping", entries, array: false };
|
|
2149
|
+
}
|
|
2150
|
+
array() {
|
|
2151
|
+
this.i++;
|
|
2152
|
+
const entries = [];
|
|
2153
|
+
for (; ; ) {
|
|
2154
|
+
this.ws();
|
|
2155
|
+
const c = this.peek();
|
|
2156
|
+
if (c === "]") {
|
|
2157
|
+
this.i++;
|
|
2158
|
+
break;
|
|
2159
|
+
}
|
|
2160
|
+
if (c === void 0) this.fail("unterminated array");
|
|
2161
|
+
if (c === "~") {
|
|
2162
|
+
this.i++;
|
|
2163
|
+
if (this.peek() !== "*") this.fail('expected a pointer after "~" (keyless back member)');
|
|
2164
|
+
entries.push({ key: null, edge: "back", value: this.pointer() });
|
|
2165
|
+
} else {
|
|
2166
|
+
entries.push(makeEntry(null, false, this.value()));
|
|
2167
|
+
}
|
|
2168
|
+
this.ws();
|
|
2169
|
+
const n = this.peek();
|
|
2170
|
+
if (n === ",") {
|
|
2171
|
+
this.i++;
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2174
|
+
if (n === "]") {
|
|
2175
|
+
this.i++;
|
|
2176
|
+
break;
|
|
2177
|
+
}
|
|
2178
|
+
this.fail('expected "," or "]"');
|
|
2179
|
+
}
|
|
2180
|
+
return { kind: "mapping", entries, array: true };
|
|
2181
|
+
}
|
|
2182
|
+
key() {
|
|
2183
|
+
const c = this.peek();
|
|
2184
|
+
if (c === '"' || c === "'") return this.string().value;
|
|
2185
|
+
return this.identifier();
|
|
2186
|
+
}
|
|
2187
|
+
identifier() {
|
|
2188
|
+
const start = this.i;
|
|
2189
|
+
const c = this.peek();
|
|
2190
|
+
if (c === void 0 || !isIdStart(c)) this.fail("expected an identifier key");
|
|
2191
|
+
this.i++;
|
|
2192
|
+
while (!this.eof() && isIdPart(this.src[this.i])) this.i++;
|
|
2193
|
+
return this.src.slice(start, this.i);
|
|
2194
|
+
}
|
|
2195
|
+
pointer() {
|
|
2196
|
+
const start = this.i;
|
|
2197
|
+
this.i++;
|
|
2198
|
+
const c = this.peek();
|
|
2199
|
+
if (c !== '"' && c !== "'") this.fail('a json5p pointer must be a quoted string after "*"');
|
|
2200
|
+
const s = this.string();
|
|
2201
|
+
const p = parsePointer(s.value);
|
|
2202
|
+
p.span = { uri: this.uri, start, end: this.i };
|
|
2203
|
+
return p;
|
|
2204
|
+
}
|
|
2205
|
+
anchored() {
|
|
2206
|
+
const start = this.i;
|
|
2207
|
+
this.i++;
|
|
2208
|
+
const c = this.peek();
|
|
2209
|
+
const body = c === '"' || c === "'" ? this.string().value : this.identifier();
|
|
2210
|
+
const anchor = makeAnchor(body, (m) => this.fail(m));
|
|
2211
|
+
anchor.path.span = { uri: this.uri, start, end: this.i };
|
|
2212
|
+
this.ws();
|
|
2213
|
+
const v = this.value();
|
|
2214
|
+
if (isPointer(v)) this.fail("cannot anchor a pointer");
|
|
2215
|
+
v.meta = { ...v.meta, anchors: [anchor, ...v.meta?.anchors ?? []] };
|
|
2216
|
+
return v;
|
|
2217
|
+
}
|
|
2218
|
+
string() {
|
|
2219
|
+
const quote = this.src[this.i];
|
|
2220
|
+
const start = this.i;
|
|
2221
|
+
this.i++;
|
|
2222
|
+
let out = "";
|
|
2223
|
+
for (; ; ) {
|
|
2224
|
+
if (this.eof()) this.fail("unterminated string");
|
|
2225
|
+
const c = this.src[this.i];
|
|
2226
|
+
if (c === quote) {
|
|
2227
|
+
this.i++;
|
|
2228
|
+
break;
|
|
2229
|
+
}
|
|
2230
|
+
if (c === "\\") {
|
|
2231
|
+
out += this.escape();
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
out += c;
|
|
2235
|
+
this.i++;
|
|
2236
|
+
}
|
|
2237
|
+
return { kind: "scalar", value: out, raw: this.src.slice(start, this.i) };
|
|
2238
|
+
}
|
|
2239
|
+
escape() {
|
|
2240
|
+
this.i++;
|
|
2241
|
+
const c = this.src[this.i];
|
|
2242
|
+
if (c === void 0) this.fail("bad escape sequence");
|
|
2243
|
+
this.i++;
|
|
2244
|
+
switch (c) {
|
|
2245
|
+
case '"':
|
|
2246
|
+
return '"';
|
|
2247
|
+
case "'":
|
|
2248
|
+
return "'";
|
|
2249
|
+
case "\\":
|
|
2250
|
+
return "\\";
|
|
2251
|
+
case "/":
|
|
2252
|
+
return "/";
|
|
2253
|
+
case "b":
|
|
2254
|
+
return "\b";
|
|
2255
|
+
case "f":
|
|
2256
|
+
return "\f";
|
|
2257
|
+
case "n":
|
|
2258
|
+
return "\n";
|
|
2259
|
+
case "r":
|
|
2260
|
+
return "\r";
|
|
2261
|
+
case "t":
|
|
2262
|
+
return " ";
|
|
2263
|
+
case "v":
|
|
2264
|
+
return "\v";
|
|
2265
|
+
case "0":
|
|
2266
|
+
return "\0";
|
|
2267
|
+
case "x": {
|
|
2268
|
+
const h = this.src.slice(this.i, this.i + 2);
|
|
2269
|
+
this.i += 2;
|
|
2270
|
+
return String.fromCharCode(parseInt(h, 16));
|
|
2271
|
+
}
|
|
2272
|
+
case "u": {
|
|
2273
|
+
const h = this.src.slice(this.i, this.i + 4);
|
|
2274
|
+
this.i += 4;
|
|
2275
|
+
return String.fromCharCode(parseInt(h, 16));
|
|
2276
|
+
}
|
|
2277
|
+
case "\n":
|
|
2278
|
+
return "";
|
|
2279
|
+
case "\r":
|
|
2280
|
+
if (this.src[this.i] === "\n") this.i++;
|
|
2281
|
+
return "";
|
|
2282
|
+
default:
|
|
2283
|
+
return c;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
numberOrKeyword() {
|
|
2287
|
+
const start = this.i;
|
|
2288
|
+
while (!this.eof() && !isDelim(this.src[this.i])) this.i++;
|
|
2289
|
+
const tok = this.src.slice(start, this.i);
|
|
2290
|
+
if (tok === "") this.fail(`unexpected character ${JSON.stringify(this.src[this.i])}`);
|
|
2291
|
+
if (tok === "true") return scalar(true, tok);
|
|
2292
|
+
if (tok === "false") return scalar(false, tok);
|
|
2293
|
+
if (tok === "null") return scalar(null, tok);
|
|
2294
|
+
const n = json5number(tok);
|
|
2295
|
+
if (n === void 0) this.fail(`invalid token "${tok}"`);
|
|
2296
|
+
return scalar(n, tok);
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
function makeEntry(key, back, v) {
|
|
2300
|
+
const edge = back ? "back" : isPointer(v) ? "ref" : "contain";
|
|
2301
|
+
return { key, edge, value: v };
|
|
2302
|
+
}
|
|
2303
|
+
function scalar(value, raw) {
|
|
2304
|
+
return { kind: "scalar", value, raw };
|
|
2305
|
+
}
|
|
2306
|
+
function isIdStart(c) {
|
|
2307
|
+
return /[A-Za-z_$]/.test(c) || c.charCodeAt(0) > 127;
|
|
2308
|
+
}
|
|
2309
|
+
function isIdPart(c) {
|
|
2310
|
+
return /[A-Za-z0-9_$]/.test(c) || c.charCodeAt(0) > 127;
|
|
2311
|
+
}
|
|
2312
|
+
function isDelim(c) {
|
|
2313
|
+
return WS.has(c) || c === "," || c === ":" || c === "{" || c === "}" || c === "[" || c === "]" || c === '"' || c === "'" || c === "/";
|
|
2314
|
+
}
|
|
2315
|
+
function json5number(tok) {
|
|
2316
|
+
if (/^[+-]?0[xX][0-9a-fA-F]+$/.test(tok)) return Number(tok);
|
|
2317
|
+
if (/^[+-]?Infinity$/.test(tok)) return tok[0] === "-" ? -Infinity : Infinity;
|
|
2318
|
+
if (/^[+-]?NaN$/.test(tok)) return NaN;
|
|
2319
|
+
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(tok)) return Number(tok);
|
|
2320
|
+
return void 0;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// ../engine/ts/src/walk.ts
|
|
2324
|
+
var { h64Raw, create64 } = await e();
|
|
2325
|
+
var hashBytes = (bytes) => "xxh64:" + h64Raw(bytes).toString(16).padStart(16, "0");
|
|
2326
|
+
var YAMLOVER_DIR = ".yamlover";
|
|
2327
|
+
var MAX_TEXT_BYTES = 1 << 20;
|
|
2328
|
+
var MAX_DOC_BYTES = 64 << 20;
|
|
2329
|
+
var HASH_INLINE_MAX = 1 << 20;
|
|
2330
|
+
var HASH_CHUNK = 8 << 20;
|
|
2331
|
+
function walkTree(absDir, opts = {}) {
|
|
2332
|
+
const g = walkTreeGen(absDir, opts);
|
|
2333
|
+
let r = g.next();
|
|
2334
|
+
while (!r.done) r = g.next();
|
|
2335
|
+
return r.value;
|
|
2336
|
+
}
|
|
2337
|
+
async function walkTreeAsync(absDir, opts = {}) {
|
|
2338
|
+
const g = walkTreeGen(absDir, opts);
|
|
2339
|
+
const every = Math.max(1, opts.yieldEvery ?? 50);
|
|
2340
|
+
let r = g.next();
|
|
2341
|
+
while (!r.done) {
|
|
2342
|
+
opts.onProgress?.({ done: r.value.done, message: r.value.path });
|
|
2343
|
+
if (r.value.done % every === 0) await yieldLoop();
|
|
2344
|
+
r = g.next();
|
|
2345
|
+
}
|
|
2346
|
+
return r.value;
|
|
2347
|
+
}
|
|
2348
|
+
function* walkTreeGen(absDir, opts = {}) {
|
|
2349
|
+
const ctx = { root: path.resolve(absDir), opts, files: /* @__PURE__ */ new Map(), count: 0 };
|
|
2350
|
+
const root = yield* dirNode(ctx.root, ctx);
|
|
2351
|
+
root.meta = { ...root.meta, documentRoot: true };
|
|
2352
|
+
const defsRoot = findDefsRoot(absDir);
|
|
2353
|
+
const defsDir = path.join(defsRoot, "$defs");
|
|
2354
|
+
const arrayRoot = root.array || (root.entries?.length ? root.entries.every((e2) => e2.key === null) : false);
|
|
2355
|
+
if (!arrayRoot && fs.existsSync(defsDir) && root.entries && !root.entries.some((e2) => e2.key === "yamlover")) {
|
|
2356
|
+
const shared = [{ key: "$defs", edge: "contain", value: yield* dirNode(defsDir, ctx) }];
|
|
2357
|
+
const tagsDir = path.join(defsRoot, "tags");
|
|
2358
|
+
if (fs.existsSync(tagsDir)) shared.push({ key: "tags", edge: "contain", value: yield* dirNode(tagsDir, ctx) });
|
|
2359
|
+
root.entries.push({ key: "yamlover", edge: "contain", value: { kind: "mapping", entries: shared, array: false } });
|
|
2360
|
+
}
|
|
2361
|
+
applySchemas(root, defsRoot);
|
|
2362
|
+
return {
|
|
2363
|
+
doc: { root, source: { concrete: "directory", uri: absDir } },
|
|
2364
|
+
files: [...ctx.files.values()]
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
function reindex(store, absDir, opts = {}) {
|
|
2368
|
+
const prev = store.stale ? /* @__PURE__ */ new Map() : store.manifest();
|
|
2369
|
+
const { doc, files } = walkTree(absDir, { ...opts, cache: opts.cache ?? manifestCache(prev) });
|
|
2370
|
+
store.indexDocument(doc, files);
|
|
2371
|
+
return diffManifest(prev, files);
|
|
2372
|
+
}
|
|
2373
|
+
async function reindexAsync(store, absDir, opts = {}) {
|
|
2374
|
+
const prev = store.stale ? /* @__PURE__ */ new Map() : store.manifest();
|
|
2375
|
+
const onProgress = opts.onProgress;
|
|
2376
|
+
const total = onProgress ? await countChildren(path.resolve(absDir), opts) : void 0;
|
|
2377
|
+
const { doc, files } = await walkTreeAsync(absDir, {
|
|
2378
|
+
...opts,
|
|
2379
|
+
cache: opts.cache ?? manifestCache(prev),
|
|
2380
|
+
onProgress: onProgress && ((p) => onProgress({ ...p, total }))
|
|
2381
|
+
});
|
|
2382
|
+
onProgress?.({ done: total ?? files.length, total, message: "writing index\u2026" });
|
|
2383
|
+
await yieldLoop();
|
|
2384
|
+
store.indexDocument(doc, files);
|
|
2385
|
+
return diffManifest(prev, files);
|
|
2386
|
+
}
|
|
2387
|
+
function manifestCache(prev) {
|
|
2388
|
+
return (rel, size, mtimeMs) => {
|
|
2389
|
+
const r = prev.get(rel);
|
|
2390
|
+
return r && r.size === size && r.mtimeMs === mtimeMs ? r.hash : null;
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
async function countChildren(absRoot, opts) {
|
|
2394
|
+
let n = 0;
|
|
2395
|
+
const visit = async (dir) => {
|
|
2396
|
+
let entries;
|
|
2397
|
+
try {
|
|
2398
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
2399
|
+
} catch {
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
for (const e2 of entries) {
|
|
2403
|
+
if (e2.name === YAMLOVER_DIR || e2.name.startsWith(".")) continue;
|
|
2404
|
+
const abs = path.join(dir, e2.name);
|
|
2405
|
+
if (opts.ignore?.(abs)) continue;
|
|
2406
|
+
n++;
|
|
2407
|
+
const isDir = e2.isDirectory() || e2.isSymbolicLink() && (await fs.promises.stat(abs).catch(() => null))?.isDirectory();
|
|
2408
|
+
if (isDir) await visit(abs);
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
await visit(absRoot);
|
|
2412
|
+
return n;
|
|
2413
|
+
}
|
|
2414
|
+
function diffManifest(prev, files) {
|
|
2415
|
+
let added = [], removed = [];
|
|
2416
|
+
const changed = [];
|
|
2417
|
+
const current = new Map(files.map((f) => [f.path, f]));
|
|
2418
|
+
for (const f of files) {
|
|
2419
|
+
const old = prev.get(f.path);
|
|
2420
|
+
if (!old) added.push(f.path);
|
|
2421
|
+
else if (old.size !== f.size || old.mtimeMs !== f.mtimeMs) changed.push(f.path);
|
|
2422
|
+
}
|
|
2423
|
+
for (const p of prev.keys()) if (!current.has(p)) removed.push(p);
|
|
2424
|
+
const moved = [];
|
|
2425
|
+
if (removed.length > 0 && added.length > 0) {
|
|
2426
|
+
const matched = /* @__PURE__ */ new Set();
|
|
2427
|
+
const tiers = [(f) => f.hash, (f) => `${f.size}:${f.mtimeMs}`];
|
|
2428
|
+
for (const key of tiers) {
|
|
2429
|
+
const outs = groupBy(removed.filter((p) => !matched.has(p)).map((p) => prev.get(p)), key);
|
|
2430
|
+
const ins = groupBy(added.filter((p) => !matched.has(p)).map((p) => current.get(p)), key);
|
|
2431
|
+
for (const [k, o] of outs) {
|
|
2432
|
+
const i = ins.get(k);
|
|
2433
|
+
if (o.length !== 1 || i?.length !== 1) continue;
|
|
2434
|
+
const [from, to] = [o[0], i[0]];
|
|
2435
|
+
if (from.hash && to.hash && from.hash !== to.hash) continue;
|
|
2436
|
+
moved.push({ from: from.path, to: to.path });
|
|
2437
|
+
matched.add(from.path);
|
|
2438
|
+
matched.add(to.path);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (matched.size > 0) {
|
|
2442
|
+
added = added.filter((p) => !matched.has(p));
|
|
2443
|
+
removed = removed.filter((p) => !matched.has(p));
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
return { added, changed, removed, moved };
|
|
2447
|
+
}
|
|
2448
|
+
function groupBy(list, key) {
|
|
2449
|
+
const m = /* @__PURE__ */ new Map();
|
|
2450
|
+
for (const f of list) {
|
|
2451
|
+
const k = key(f);
|
|
2452
|
+
if (k != null) m.set(k, [...m.get(k) ?? [], f]);
|
|
2453
|
+
}
|
|
2454
|
+
return m;
|
|
2455
|
+
}
|
|
2456
|
+
async function hashFileAsync(abs, onChunk) {
|
|
2457
|
+
const fh = await fs.promises.open(abs, "r");
|
|
2458
|
+
try {
|
|
2459
|
+
const hasher = create64();
|
|
2460
|
+
const buf = Buffer.alloc(HASH_CHUNK);
|
|
2461
|
+
let done = 0;
|
|
2462
|
+
for (; ; ) {
|
|
2463
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length);
|
|
2464
|
+
if (bytesRead === 0) break;
|
|
2465
|
+
hasher.update(bytesRead === buf.length ? buf : buf.subarray(0, bytesRead));
|
|
2466
|
+
done += bytesRead;
|
|
2467
|
+
onChunk?.(done);
|
|
2468
|
+
}
|
|
2469
|
+
return "xxh64:" + hasher.digest().toString(16).padStart(16, "0");
|
|
2470
|
+
} finally {
|
|
2471
|
+
await fh.close();
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
function record(ctx, abs, hash, size, mtimeMs) {
|
|
2475
|
+
const rel = path.relative(ctx.root, abs).split(path.sep).join("/");
|
|
2476
|
+
if (rel.startsWith("..")) return;
|
|
2477
|
+
ctx.files.set(rel, { path: rel, hash, size, mtimeMs });
|
|
2478
|
+
}
|
|
2479
|
+
function readTracked(ctx, abs) {
|
|
2480
|
+
const stat = fs.statSync(abs);
|
|
2481
|
+
const bytes = fs.readFileSync(abs);
|
|
2482
|
+
record(ctx, abs, hashBytes(bytes), stat.size, stat.mtimeMs);
|
|
2483
|
+
return bytes;
|
|
2484
|
+
}
|
|
2485
|
+
function loadMeta(dir, ctx) {
|
|
2486
|
+
const file = path.join(dir, YAMLOVER_DIR, "meta.yamlover");
|
|
2487
|
+
if (!fs.existsSync(file)) return {};
|
|
2488
|
+
try {
|
|
2489
|
+
const plain = toPlain(parseYamlover(readTracked(ctx, file).toString("utf8"), file).root);
|
|
2490
|
+
const props = plain?.properties ?? {};
|
|
2491
|
+
return props && typeof props === "object" ? props : {};
|
|
2492
|
+
} catch {
|
|
2493
|
+
return {};
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
function* dirNode(dir, ctx) {
|
|
2497
|
+
const meta = loadMeta(dir, ctx);
|
|
2498
|
+
const names = fs.readdirSync(dir).filter((n) => n !== YAMLOVER_DIR && !n.startsWith(".")).filter((n) => !ctx.opts.ignore?.(path.join(dir, n))).sort();
|
|
2499
|
+
const entries = [];
|
|
2500
|
+
for (const name of names) {
|
|
2501
|
+
const abs = path.join(dir, name);
|
|
2502
|
+
const child = yield* childNode(abs, meta[name], ctx);
|
|
2503
|
+
entries.push({ key: name, edge: "contain", value: child });
|
|
2504
|
+
yield { done: ++ctx.count, path: path.relative(ctx.root, abs).split(path.sep).join("/") };
|
|
2505
|
+
}
|
|
2506
|
+
const node = { kind: "mapping", entries, array: false };
|
|
2507
|
+
return applyMeta(applyBody(dir, node, ctx), meta);
|
|
2508
|
+
}
|
|
2509
|
+
function* childNode(abs, m, ctx) {
|
|
2510
|
+
const stat = fs.statSync(abs);
|
|
2511
|
+
if (stat.isDirectory()) return yield* dirNode(abs, ctx);
|
|
2512
|
+
const ext = path.extname(abs).toLowerCase();
|
|
2513
|
+
const fmt = m?.format ?? EXT_FORMAT[ext] ?? null;
|
|
2514
|
+
if (m?.type === "binary") return blob(abs, fmt ?? "application/octet-stream", ctx);
|
|
2515
|
+
if (fmt && (DOC_FORMATS[fmt] || TEXT_FORMATS.has(fmt))) {
|
|
2516
|
+
if (stat.size > MAX_DOC_BYTES) return blob(abs, fmt, ctx);
|
|
2517
|
+
if (DOC_FORMATS[fmt]) return parsedDoc(abs, DOC_FORMATS[fmt], ctx);
|
|
2518
|
+
return textScalar(abs, fmt, ctx);
|
|
2519
|
+
}
|
|
2520
|
+
if (fmt) return blob(abs, fmt, ctx);
|
|
2521
|
+
if (looksBinary(abs)) return blob(abs, "application/octet-stream", ctx);
|
|
2522
|
+
return parsedScalar(abs, ext, ctx);
|
|
2523
|
+
}
|
|
2524
|
+
function applyMeta(node, meta) {
|
|
2525
|
+
for (const e2 of node.entries ?? []) {
|
|
2526
|
+
if (e2.key == null || isPointer(e2.value)) continue;
|
|
2527
|
+
const m = meta[e2.key];
|
|
2528
|
+
if (!m) continue;
|
|
2529
|
+
if (m.uniqueItems) e2.value = { ...e2.value, meta: { ...e2.value.meta, set: true } };
|
|
2530
|
+
if (e2.value.kind === "blob") continue;
|
|
2531
|
+
if (m.format && !e2.value.meta?.schema) e2.value = { ...e2.value, meta: { ...e2.value.meta, schema: inlineFormat(m.format) } };
|
|
2532
|
+
}
|
|
2533
|
+
return node;
|
|
2534
|
+
}
|
|
2535
|
+
function blob(abs, format, ctx) {
|
|
2536
|
+
const stat = fs.statSync(abs);
|
|
2537
|
+
const rel = path.relative(ctx.root, abs).split(path.sep).join("/");
|
|
2538
|
+
const cached = ctx.opts.cache?.(rel, stat.size, stat.mtimeMs) ?? null;
|
|
2539
|
+
if (cached) {
|
|
2540
|
+
record(ctx, abs, cached, stat.size, stat.mtimeMs);
|
|
2541
|
+
return { kind: "blob", format, contentHash: cached, size: stat.size };
|
|
2542
|
+
}
|
|
2543
|
+
const inlineMax = ctx.opts.hashInlineMax ?? HASH_INLINE_MAX;
|
|
2544
|
+
const contentHash = stat.size <= inlineMax ? hashBytes(fs.readFileSync(abs)) : null;
|
|
2545
|
+
record(ctx, abs, contentHash, stat.size, stat.mtimeMs);
|
|
2546
|
+
return { kind: "blob", format, contentHash, size: stat.size };
|
|
2547
|
+
}
|
|
2548
|
+
function textScalar(abs, format, ctx) {
|
|
2549
|
+
const text = readTracked(ctx, abs).toString("utf8");
|
|
2550
|
+
return { kind: "scalar", value: text, raw: text, meta: { schema: inlineFormat(format) } };
|
|
2551
|
+
}
|
|
2552
|
+
function parsedScalar(abs, ext, ctx) {
|
|
2553
|
+
return parsedDoc(abs, ext === ".json" || ext === ".json5" || ext === ".json5p" ? "json5p" : "yamlover", ctx);
|
|
2554
|
+
}
|
|
2555
|
+
function parsedDoc(abs, lang, ctx) {
|
|
2556
|
+
const text = readTracked(ctx, abs).toString("utf8");
|
|
2557
|
+
try {
|
|
2558
|
+
const doc = lang === "json5p" ? parseJson5p(text, abs) : parseYamlover(text, abs);
|
|
2559
|
+
const root = doc.root;
|
|
2560
|
+
root.meta = { ...root.meta, documentRoot: true };
|
|
2561
|
+
return root;
|
|
2562
|
+
} catch {
|
|
2563
|
+
return { kind: "scalar", value: text, raw: text };
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function findDefsRoot(dir) {
|
|
2567
|
+
let d = path.resolve(dir);
|
|
2568
|
+
for (; ; ) {
|
|
2569
|
+
if (fs.existsSync(path.join(d, "$defs"))) return d;
|
|
2570
|
+
const up = path.dirname(d);
|
|
2571
|
+
if (up === d) return path.resolve(dir);
|
|
2572
|
+
d = up;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
function applySchemas(root, defsRoot) {
|
|
2576
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2577
|
+
const loadDef = (name) => {
|
|
2578
|
+
if (!cache.has(name)) {
|
|
2579
|
+
const defFile = path.join(defsRoot, "$defs", name);
|
|
2580
|
+
try {
|
|
2581
|
+
cache.set(name, parseYamlover(fs.readFileSync(defFile, "utf8"), defFile).root);
|
|
2582
|
+
} catch {
|
|
2583
|
+
cache.set(name, null);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
return cache.get(name);
|
|
2587
|
+
};
|
|
2588
|
+
const field = (n, key) => n.entries?.find((e2) => e2.key === key)?.value ?? null;
|
|
2589
|
+
const str = (n, key) => {
|
|
2590
|
+
const v = field(n, key);
|
|
2591
|
+
return v && !isPointer(v) && v.kind === "scalar" && v.value != null ? String(v.value) : null;
|
|
2592
|
+
};
|
|
2593
|
+
const hasFormat = (inst) => {
|
|
2594
|
+
if (inst.kind === "blob") return true;
|
|
2595
|
+
const s = inst.meta?.schema;
|
|
2596
|
+
return !!s && !isPointer(s) && s.kind === "mapping" && !!field(s, "format");
|
|
2597
|
+
};
|
|
2598
|
+
const apply = (inst, schema, depth) => {
|
|
2599
|
+
if (depth > 64 || isPointer(inst)) return;
|
|
2600
|
+
let name = null;
|
|
2601
|
+
let s;
|
|
2602
|
+
if (isPointer(schema)) {
|
|
2603
|
+
const last = schema.steps[schema.steps.length - 1];
|
|
2604
|
+
name = last?.sel === "key" ? last.name : null;
|
|
2605
|
+
s = name ? loadDef(name) : null;
|
|
2606
|
+
} else s = schema;
|
|
2607
|
+
if (!s || isPointer(s)) return;
|
|
2608
|
+
const fmt = str(s, "format") ?? (name && str(s, "type") === "object" ? `x-yamlover-${name}` : null);
|
|
2609
|
+
if (fmt && !hasFormat(inst)) inst.meta = { ...inst.meta, schema: inlineFormat(fmt) };
|
|
2610
|
+
const stype = str(s, "type");
|
|
2611
|
+
if (stype === "object" || stype === "variant" || stype === "mixed") {
|
|
2612
|
+
const props = field(s, "properties");
|
|
2613
|
+
const addl = field(s, "additionalProperties");
|
|
2614
|
+
for (const e2 of inst.entries ?? []) {
|
|
2615
|
+
if (e2.key == null || isPointer(e2.value)) continue;
|
|
2616
|
+
const declared = props && !isPointer(props) ? field(props, e2.key) : null;
|
|
2617
|
+
const sub = declared ?? addl;
|
|
2618
|
+
if (sub) apply(e2.value, sub, depth + 1);
|
|
2619
|
+
}
|
|
2620
|
+
} else if (stype === "array") {
|
|
2621
|
+
const items = field(s, "items");
|
|
2622
|
+
if (items) {
|
|
2623
|
+
for (const e2 of inst.entries ?? []) if (!isPointer(e2.value)) apply(e2.value, items, depth + 1);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2627
|
+
const walk = (node) => {
|
|
2628
|
+
if (node.meta?.schema) apply(node, node.meta.schema, 0);
|
|
2629
|
+
for (const e2 of node.entries ?? []) if (!isPointer(e2.value)) walk(e2.value);
|
|
2630
|
+
};
|
|
2631
|
+
walk(root);
|
|
2632
|
+
}
|
|
2633
|
+
function inlineFormat(format) {
|
|
2634
|
+
return { kind: "mapping", entries: [{ key: "format", edge: "contain", value: { kind: "scalar", value: format, raw: format } }], array: false };
|
|
2635
|
+
}
|
|
2636
|
+
function applyBody(dir, node, ctx) {
|
|
2637
|
+
const file = path.join(dir, YAMLOVER_DIR, "body.yamlover");
|
|
2638
|
+
if (!fs.existsSync(file)) return node;
|
|
2639
|
+
const bodyDoc = parseYamlover(readTracked(ctx, file).toString("utf8"), file);
|
|
2640
|
+
const body = bodyDoc.root;
|
|
2641
|
+
if (body.kind !== "mapping" && body.kind !== "scalar" || !body.entries) return node;
|
|
2642
|
+
const meta = { ...node.meta, ...body.meta, documentRoot: true };
|
|
2643
|
+
if (body.kind === "mapping" && (body.array || body.entries.length > 0 && body.entries.every((e2) => e2.key === null))) {
|
|
2644
|
+
const byKey = new Map(node.entries.map((e2) => [e2.key, e2]));
|
|
2645
|
+
const ordered = [];
|
|
2646
|
+
for (const e2 of body.entries) {
|
|
2647
|
+
const targetKey = isPointer(e2.value) ? pointerLeafKey(e2.value) : null;
|
|
2648
|
+
const hit = targetKey != null ? byKey.get(targetKey) : null;
|
|
2649
|
+
if (hit) {
|
|
2650
|
+
ordered.push(hit);
|
|
2651
|
+
byKey.delete(targetKey);
|
|
2652
|
+
} else ordered.push(e2);
|
|
2653
|
+
}
|
|
2654
|
+
for (const e2 of node.entries) if (e2.key != null && byKey.has(e2.key)) ordered.push(e2);
|
|
2655
|
+
return { kind: "mapping", entries: ordered, array: true, meta };
|
|
2656
|
+
}
|
|
2657
|
+
const merged = new Map(node.entries.map((e2) => [e2.key, e2]));
|
|
2658
|
+
const order = node.entries.map((e2) => e2.key);
|
|
2659
|
+
for (const e2 of body.entries) {
|
|
2660
|
+
const existing = merged.get(e2.key);
|
|
2661
|
+
if (!existing) {
|
|
2662
|
+
order.push(e2.key);
|
|
2663
|
+
merged.set(e2.key, e2);
|
|
2664
|
+
} else merged.set(e2.key, augmentEntry(existing, e2));
|
|
2665
|
+
}
|
|
2666
|
+
const entries = order.map((k) => merged.get(k));
|
|
2667
|
+
if (body.kind === "scalar") return { kind: "scalar", value: body.value, raw: body.raw, entries, array: false, meta };
|
|
2668
|
+
return { kind: "mapping", entries, array: false, meta };
|
|
2669
|
+
}
|
|
2670
|
+
function augmentEntry(base, overlay) {
|
|
2671
|
+
if (isPointer(base.value) || isPointer(overlay.value)) return overlay;
|
|
2672
|
+
const b = base.value, o = overlay.value;
|
|
2673
|
+
const value = {
|
|
2674
|
+
...b,
|
|
2675
|
+
entries: o.entries ?? b.entries,
|
|
2676
|
+
array: o.entries ? o.array : b.array,
|
|
2677
|
+
meta: b.meta || o.meta ? { ...b.meta, ...o.meta } : void 0
|
|
2678
|
+
};
|
|
2679
|
+
return { key: base.key, edge: "contain", value };
|
|
2680
|
+
}
|
|
2681
|
+
function pointerLeafKey(v) {
|
|
2682
|
+
if (!isPointer(v)) return null;
|
|
2683
|
+
const last = v.steps[v.steps.length - 1];
|
|
2684
|
+
if (last?.sel === "key") return last.name;
|
|
2685
|
+
if (v.steps.length === 0 && v.base.scope === "current") return v.raw;
|
|
2686
|
+
return null;
|
|
2687
|
+
}
|
|
2688
|
+
function looksBinary(abs) {
|
|
2689
|
+
try {
|
|
2690
|
+
if (fs.statSync(abs).size > MAX_TEXT_BYTES) return true;
|
|
2691
|
+
const fd = fs.openSync(abs, "r");
|
|
2692
|
+
try {
|
|
2693
|
+
const buf = Buffer.alloc(4096);
|
|
2694
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
2695
|
+
return buf.subarray(0, n).includes(0);
|
|
2696
|
+
} finally {
|
|
2697
|
+
fs.closeSync(fd);
|
|
2698
|
+
}
|
|
2699
|
+
} catch {
|
|
2700
|
+
return true;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
var EXT_FORMAT = {
|
|
2704
|
+
".png": "image/png",
|
|
2705
|
+
".jpg": "image/jpeg",
|
|
2706
|
+
".jpeg": "image/jpeg",
|
|
2707
|
+
".gif": "image/gif",
|
|
2708
|
+
".webp": "image/webp",
|
|
2709
|
+
".avif": "image/avif",
|
|
2710
|
+
".bmp": "image/bmp",
|
|
2711
|
+
".ico": "image/x-icon",
|
|
2712
|
+
".svg": "image/svg+xml",
|
|
2713
|
+
".pdf": "application/pdf",
|
|
2714
|
+
".djvu": "image/vnd.djvu",
|
|
2715
|
+
".djv": "image/vnd.djvu",
|
|
2716
|
+
".psd": "image/vnd.adobe.photoshop",
|
|
2717
|
+
".psb": "image/vnd.adobe.photoshop",
|
|
2718
|
+
".tif": "image/tiff",
|
|
2719
|
+
".tiff": "image/tiff",
|
|
2720
|
+
".heic": "image/heic",
|
|
2721
|
+
".heif": "image/heic",
|
|
2722
|
+
".fb2": "application/x-fictionbook+xml",
|
|
2723
|
+
".epub": "application/epub+zip",
|
|
2724
|
+
".html": "text/html",
|
|
2725
|
+
".htm": "text/html",
|
|
2726
|
+
".md": "text/markdown",
|
|
2727
|
+
".markdown": "text/markdown",
|
|
2728
|
+
".adoc": "text/asciidoc",
|
|
2729
|
+
".asciidoc": "text/asciidoc",
|
|
2730
|
+
".asc": "text/asciidoc",
|
|
2731
|
+
".csv": "text/csv",
|
|
2732
|
+
".tsv": "text/tab-separated-values",
|
|
2733
|
+
".rtf": "application/rtf",
|
|
2734
|
+
".doc": "application/msword",
|
|
2735
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2736
|
+
".xls": "application/vnd.ms-excel",
|
|
2737
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2738
|
+
".kml": "application/vnd.google-earth.kml+xml",
|
|
2739
|
+
".kmz": "application/vnd.google-earth.kmz",
|
|
2740
|
+
".puml": "text/x-plantuml",
|
|
2741
|
+
".plantuml": "text/x-plantuml",
|
|
2742
|
+
".iuml": "text/x-plantuml",
|
|
2743
|
+
".pu": "text/x-plantuml",
|
|
2744
|
+
// Plain text kept as RAW BYTES (text/plain is deliberately NOT in TEXT_FORMATS), so the
|
|
2745
|
+
// client decodes it under a chosen encoding (CP866/Win-1251/KOI8-R/UTF-8) — legacy
|
|
2746
|
+
// Cyrillic .txt files are common — rather than the server fixing UTF-8.
|
|
2747
|
+
".txt": "text/plain",
|
|
2748
|
+
".text": "text/plain",
|
|
2749
|
+
".log": "text/plain",
|
|
2750
|
+
".ini": "text/plain"
|
|
2751
|
+
};
|
|
2752
|
+
var TEXT_FORMATS = /* @__PURE__ */ new Set(["text/markdown", "text/asciidoc", "text/x-plantuml", "text/csv", "text/tab-separated-values"]);
|
|
2753
|
+
var DOC_FORMATS = {
|
|
2754
|
+
"yamlover": "yamlover",
|
|
2755
|
+
"yaml": "yamlover",
|
|
2756
|
+
"yamlover/meta": "yamlover",
|
|
2757
|
+
"yaml/meta": "yamlover",
|
|
2758
|
+
"json": "json5p",
|
|
2759
|
+
"json5": "json5p",
|
|
2760
|
+
"json5p": "json5p",
|
|
2761
|
+
"json/meta": "json5p",
|
|
2762
|
+
"json5p/meta": "json5p",
|
|
2763
|
+
"json/schema": "json5p"
|
|
2764
|
+
};
|
|
2765
|
+
|
|
2766
|
+
// ../engine/ts/src/rewrite.ts
|
|
2767
|
+
import * as path2 from "node:path";
|
|
2768
|
+
|
|
2769
|
+
// ../parser/ts/src/serialize-yamlover.ts
|
|
2770
|
+
function pointerToken(raw) {
|
|
2771
|
+
if (raw !== raw.trim()) return `*'${raw.replace(/'/g, "''")}'`;
|
|
2772
|
+
return "*" + raw;
|
|
2773
|
+
}
|
|
2774
|
+
function anchorToken(body) {
|
|
2775
|
+
if (/^['"]/.test(body) || hasUnquotedSpace(body)) return `&'${body.replace(/'/g, "''")}'`;
|
|
2776
|
+
return "&" + body;
|
|
2777
|
+
}
|
|
2778
|
+
function hasUnquotedSpace(s) {
|
|
2779
|
+
let q = null;
|
|
2780
|
+
for (let i = 0; i < s.length; i++) {
|
|
2781
|
+
const c = s[i];
|
|
2782
|
+
if (q !== null) {
|
|
2783
|
+
if (c === q) q = null;
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
if (c === "\\") {
|
|
2787
|
+
i++;
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
if (c === "'" || c === '"') {
|
|
2791
|
+
q = c;
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
if (c === " " || c === " ") return true;
|
|
2795
|
+
}
|
|
2796
|
+
return false;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// ../engine/ts/src/rewrite.ts
|
|
2800
|
+
function under(p, x) {
|
|
2801
|
+
if (x === ":") return true;
|
|
2802
|
+
return p === x || p.startsWith(x + ":") || p.startsWith(x + "[");
|
|
2803
|
+
}
|
|
2804
|
+
function planRewrites(doc, edges, oldStore, newStore, opts = {}) {
|
|
2805
|
+
const mapPath = (p) => under(p, oldStore) ? newStore + p.slice(oldStore.length) : p;
|
|
2806
|
+
const plan = { edits: /* @__PURE__ */ new Map(), rewritten: [], unrewritten: [] };
|
|
2807
|
+
for (const e2 of edges) {
|
|
2808
|
+
if (e2.target.kind !== "node" || !under(e2.target.path, oldStore)) continue;
|
|
2809
|
+
const miss = (reason) => {
|
|
2810
|
+
plan.unrewritten.push({ file: e2.ptr.span?.uri ?? "<unknown>", from: e2.from, raw: e2.raw, reason });
|
|
2811
|
+
};
|
|
2812
|
+
if (e2.anchor) {
|
|
2813
|
+
const span2 = e2.ptr.span;
|
|
2814
|
+
if (!span2) {
|
|
2815
|
+
miss("anchor has no source span");
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
if (opts.root !== void 0 && path2.relative(opts.root, span2.uri).startsWith("..")) {
|
|
2819
|
+
miss("source file is outside the served root (grafted)");
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
const holder2 = mapPath(e2.holder);
|
|
2823
|
+
const docRoot2 = mapPath(e2.docRoot);
|
|
2824
|
+
const container = mapPath(e2.target.path);
|
|
2825
|
+
let cRaw = null;
|
|
2826
|
+
switch (e2.ptr.base.scope) {
|
|
2827
|
+
case "link":
|
|
2828
|
+
cRaw = "//" + renderSuffix("/", container).replace(/^\//, "");
|
|
2829
|
+
break;
|
|
2830
|
+
case "document":
|
|
2831
|
+
cRaw = under(container, docRoot2) ? docForm(docRoot2, container) : null;
|
|
2832
|
+
break;
|
|
2833
|
+
case "current": {
|
|
2834
|
+
const p = parentOf(holder2);
|
|
2835
|
+
if (p !== null && under(container, p)) cRaw = renderSuffix(p, container).replace(/^\//, "");
|
|
2836
|
+
else if (under(container, docRoot2)) cRaw = docForm(docRoot2, container);
|
|
2837
|
+
break;
|
|
2838
|
+
}
|
|
2839
|
+
case "parent": {
|
|
2840
|
+
const p1 = parentOf(holder2);
|
|
2841
|
+
const p2 = p1 === null ? null : parentOf(p1);
|
|
2842
|
+
if (p2 !== null && under(container, p2)) cRaw = ".." + suffixAfter(p2, container);
|
|
2843
|
+
else if (under(container, docRoot2)) cRaw = docForm(docRoot2, container);
|
|
2844
|
+
break;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
if (cRaw === null) {
|
|
2848
|
+
miss("anchor container left the holder's document");
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
let newBody = e2.label != null ? (cRaw === "" ? "" : cRaw === "/" ? "/" : cRaw + "/") + escapeSegment(e2.label) : cRaw + "[]";
|
|
2852
|
+
const tail = newBody.endsWith("[]") ? "[]" : "";
|
|
2853
|
+
newBody = matchStyle(newBody.slice(0, newBody.length - tail.length), e2.raw) + tail;
|
|
2854
|
+
if (newBody === e2.raw.slice(1)) continue;
|
|
2855
|
+
const tok = isJson5pUri(span2.uri) ? json5pAnchorToken(newBody) : anchorToken(newBody);
|
|
2856
|
+
const list2 = plan.edits.get(span2.uri) ?? [];
|
|
2857
|
+
list2.push({ start: span2.start, end: span2.end, text: tok });
|
|
2858
|
+
plan.edits.set(span2.uri, list2);
|
|
2859
|
+
plan.rewritten.push({ file: span2.uri, from: e2.from, oldRaw: e2.raw, newRaw: "&" + newBody });
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
if (under(e2.holder, oldStore) && (e2.ptr.base.scope === "current" || e2.ptr.base.scope === "document" && under(e2.docRoot, oldStore))) {
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
const span = e2.ptr.span;
|
|
2866
|
+
if (!span) {
|
|
2867
|
+
miss("pointer has no source span");
|
|
2868
|
+
continue;
|
|
2869
|
+
}
|
|
2870
|
+
if (opts.root !== void 0 && path2.relative(opts.root, span.uri).startsWith("..")) {
|
|
2871
|
+
miss("source file is outside the served root (grafted)");
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
const holder = mapPath(e2.holder);
|
|
2875
|
+
const docRoot = mapPath(e2.docRoot);
|
|
2876
|
+
const target = mapPath(e2.target.path);
|
|
2877
|
+
let newRaw = null;
|
|
2878
|
+
switch (e2.ptr.base.scope) {
|
|
2879
|
+
case "link":
|
|
2880
|
+
newRaw = "//" + renderSuffix("/", target).replace(/^\//, "");
|
|
2881
|
+
break;
|
|
2882
|
+
case "document":
|
|
2883
|
+
newRaw = under(target, docRoot) ? docForm(docRoot, target) : null;
|
|
2884
|
+
break;
|
|
2885
|
+
case "current":
|
|
2886
|
+
if (under(target, holder) && target !== holder) newRaw = relForm(holder, target);
|
|
2887
|
+
else if (under(target, docRoot)) newRaw = docForm(docRoot, target);
|
|
2888
|
+
break;
|
|
2889
|
+
case "parent": {
|
|
2890
|
+
const p = parentOf(holder);
|
|
2891
|
+
if (p !== null && under(target, p)) newRaw = ".." + suffixAfter(p, target);
|
|
2892
|
+
else if (under(target, docRoot)) newRaw = docForm(docRoot, target);
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
if (newRaw === null) {
|
|
2897
|
+
miss("target left the holder's document");
|
|
2898
|
+
continue;
|
|
2899
|
+
}
|
|
2900
|
+
newRaw = matchStyle(newRaw, e2.raw);
|
|
2901
|
+
if (newRaw === e2.raw) continue;
|
|
2902
|
+
const token = isJson5pUri(span.uri) ? json5pToken(newRaw) : pointerToken(newRaw);
|
|
2903
|
+
const list = plan.edits.get(span.uri) ?? [];
|
|
2904
|
+
list.push({ start: span.start, end: span.end, text: token });
|
|
2905
|
+
plan.edits.set(span.uri, list);
|
|
2906
|
+
plan.rewritten.push({ file: span.uri, from: e2.from, oldRaw: e2.raw, newRaw });
|
|
2907
|
+
}
|
|
2908
|
+
return plan;
|
|
2909
|
+
}
|
|
2910
|
+
function applyEdits(text, edits) {
|
|
2911
|
+
const sorted = [...edits].sort((a, b) => b.start - a.start);
|
|
2912
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
2913
|
+
if (sorted[i].end > sorted[i - 1].start) {
|
|
2914
|
+
throw new Error(`overlapping edits at ${sorted[i].start}..${sorted[i].end} and ${sorted[i - 1].start}..`);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
let out = text;
|
|
2918
|
+
for (const e2 of sorted) out = out.slice(0, e2.start) + e2.text + out.slice(e2.end);
|
|
2919
|
+
return out;
|
|
2920
|
+
}
|
|
2921
|
+
function nominalPath(doc, e2) {
|
|
2922
|
+
if (e2.anchor) return null;
|
|
2923
|
+
let base;
|
|
2924
|
+
switch (e2.ptr.base.scope) {
|
|
2925
|
+
case "link":
|
|
2926
|
+
base = ":" + e2.ptr.base.authority;
|
|
2927
|
+
break;
|
|
2928
|
+
case "document":
|
|
2929
|
+
base = e2.docRoot;
|
|
2930
|
+
break;
|
|
2931
|
+
case "current":
|
|
2932
|
+
base = e2.holder;
|
|
2933
|
+
break;
|
|
2934
|
+
case "parent":
|
|
2935
|
+
base = parentOf(e2.holder) ?? "/";
|
|
2936
|
+
break;
|
|
2937
|
+
}
|
|
2938
|
+
let p = base === ":" ? "" : base;
|
|
2939
|
+
for (const st of e2.ptr.steps) {
|
|
2940
|
+
if (st.sel === "parent") {
|
|
2941
|
+
const up = parentOf(p === "" ? ":" : p);
|
|
2942
|
+
if (up === null) return null;
|
|
2943
|
+
p = up === ":" ? "" : up;
|
|
2944
|
+
} else if (st.sel === "key") p += ":" + st.name;
|
|
2945
|
+
else p += "[" + st.n + "]";
|
|
2946
|
+
}
|
|
2947
|
+
return p === "" ? ":" : p;
|
|
2948
|
+
}
|
|
2949
|
+
function tokensOf(p) {
|
|
2950
|
+
const out = [];
|
|
2951
|
+
const re = /\[(\d+)\]|:([^:[]*)/g;
|
|
2952
|
+
for (let m = re.exec(p); m !== null; m = re.exec(p)) {
|
|
2953
|
+
if (m[1] !== void 0) out.push({ n: Number(m[1]) });
|
|
2954
|
+
else if (m[2] !== "") out.push({ key: m[2] });
|
|
2955
|
+
}
|
|
2956
|
+
return out;
|
|
2957
|
+
}
|
|
2958
|
+
function parentOf(p) {
|
|
2959
|
+
if (p === ":" || p === "") return null;
|
|
2960
|
+
const toks = tokensOf(p).slice(0, -1);
|
|
2961
|
+
if (toks.length === 0) return ":";
|
|
2962
|
+
return renderPath(toks);
|
|
2963
|
+
}
|
|
2964
|
+
function renderPath(toks) {
|
|
2965
|
+
let s = "";
|
|
2966
|
+
for (const t2 of toks) s += t2.key !== void 0 ? ":" + t2.key : "[" + t2.n + "]";
|
|
2967
|
+
return s === "" ? ":" : s;
|
|
2968
|
+
}
|
|
2969
|
+
function renderSuffix(base, p) {
|
|
2970
|
+
const toks = tokensOf(p).slice(base === ":" ? 0 : tokensOf(base).length);
|
|
2971
|
+
let s = "";
|
|
2972
|
+
for (const t2 of toks) s += t2.key !== void 0 ? "/" + escapeSegment(t2.key) : "[" + t2.n + "]";
|
|
2973
|
+
return s;
|
|
2974
|
+
}
|
|
2975
|
+
function docForm(docRoot, target) {
|
|
2976
|
+
const s = renderSuffix(docRoot, target);
|
|
2977
|
+
return s === "" ? "/" : s.startsWith("/") ? s : "/" + s;
|
|
2978
|
+
}
|
|
2979
|
+
function relForm(holder, target) {
|
|
2980
|
+
return renderSuffix(holder, target).replace(/^\//, "");
|
|
2981
|
+
}
|
|
2982
|
+
function suffixAfter(base, target) {
|
|
2983
|
+
return renderSuffix(base, target);
|
|
2984
|
+
}
|
|
2985
|
+
function isJson5pUri(uri) {
|
|
2986
|
+
return /\.(json|json5|json5p)$/i.test(uri);
|
|
2987
|
+
}
|
|
2988
|
+
function json5pToken(raw) {
|
|
2989
|
+
return `*'${raw.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
2990
|
+
}
|
|
2991
|
+
function matchStyle(slashRaw, originalRaw) {
|
|
2992
|
+
if (!originalRaw.includes(":")) return slashRaw;
|
|
2993
|
+
try {
|
|
2994
|
+
return renderPointer(parsePointer(slashRaw));
|
|
2995
|
+
} catch {
|
|
2996
|
+
return slashRaw;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
function json5pAnchorToken(body) {
|
|
3000
|
+
return `&'${body.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
// ../engine/ts/src/mv.ts
|
|
3004
|
+
import * as fs2 from "node:fs";
|
|
3005
|
+
import * as path3 from "node:path";
|
|
3006
|
+
function mv(absRoot, fromRel, toRel, opts = {}) {
|
|
3007
|
+
const root = path3.resolve(absRoot);
|
|
3008
|
+
const absFrom = path3.resolve(root, fromRel);
|
|
3009
|
+
const absTo = path3.resolve(root, toRel);
|
|
3010
|
+
const relFrom = relInside(root, absFrom, "source");
|
|
3011
|
+
const relTo = relInside(root, absTo, "target");
|
|
3012
|
+
for (const rel of [relFrom, relTo]) {
|
|
3013
|
+
for (const seg of rel.split("/")) {
|
|
3014
|
+
if (seg.startsWith(".")) throw new Error(`mv: hidden/overlay segments are not movable ("${seg}" in "${rel}")`);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
if (!fs2.existsSync(absFrom)) throw new Error(`mv: source does not exist: ${relFrom}`);
|
|
3018
|
+
if (fs2.existsSync(absTo)) throw new Error(`mv: target already exists: ${relTo}`);
|
|
3019
|
+
if (absTo === absFrom || absTo.startsWith(absFrom + path3.sep)) {
|
|
3020
|
+
throw new Error(`mv: cannot move "${relFrom}" into itself`);
|
|
3021
|
+
}
|
|
3022
|
+
const { doc } = walkTree(root, opts);
|
|
3023
|
+
const edges = resolveDocument(doc);
|
|
3024
|
+
const plan = planRewrites(doc, edges, storeOf(relFrom), storeOf(relTo), { root });
|
|
3025
|
+
const editedFiles = [];
|
|
3026
|
+
for (const [uri, edits] of plan.edits) {
|
|
3027
|
+
fs2.writeFileSync(uri, applyEdits(fs2.readFileSync(uri, "utf8"), edits));
|
|
3028
|
+
editedFiles.push(path3.relative(root, uri).split(path3.sep).join("/"));
|
|
3029
|
+
}
|
|
3030
|
+
fs2.mkdirSync(path3.dirname(absTo), { recursive: true });
|
|
3031
|
+
fs2.renameSync(absFrom, absTo);
|
|
3032
|
+
return { from: relFrom, to: relTo, rewritten: plan.rewritten, unrewritten: plan.unrewritten, editedFiles };
|
|
3033
|
+
}
|
|
3034
|
+
function relinkMoved(absRoot, moved, opts = {}) {
|
|
3035
|
+
const root = path3.resolve(absRoot);
|
|
3036
|
+
const { doc } = walkTree(root, opts);
|
|
3037
|
+
const edges = resolveDocument(doc);
|
|
3038
|
+
const rewritten = [];
|
|
3039
|
+
const unrewritten = [];
|
|
3040
|
+
const merged = /* @__PURE__ */ new Map();
|
|
3041
|
+
for (const m of moved) {
|
|
3042
|
+
const oldStore = storeOf(m.from);
|
|
3043
|
+
const stale = edges.filter((e2) => {
|
|
3044
|
+
if (e2.target.kind === "node" && !under(e2.target.path, oldStore)) return false;
|
|
3045
|
+
const nom = nominalPath(doc, e2);
|
|
3046
|
+
return nom !== null && under(nom, oldStore);
|
|
3047
|
+
});
|
|
3048
|
+
const shimmed = stale.map((e2) => ({ ...e2, target: { kind: "node", node: doc.root, path: nominalPath(doc, e2) } }));
|
|
3049
|
+
const plan = planRewrites(doc, shimmed, oldStore, storeOf(m.to), { root });
|
|
3050
|
+
for (const [uri, edits] of plan.edits) merged.set(uri, [...merged.get(uri) ?? [], ...edits]);
|
|
3051
|
+
rewritten.push(...plan.rewritten);
|
|
3052
|
+
unrewritten.push(...plan.unrewritten);
|
|
3053
|
+
}
|
|
3054
|
+
const editedFiles = [];
|
|
3055
|
+
for (const [uri, edits] of merged) {
|
|
3056
|
+
fs2.writeFileSync(uri, applyEdits(fs2.readFileSync(uri, "utf8"), edits));
|
|
3057
|
+
editedFiles.push(path3.relative(root, uri).split(path3.sep).join("/"));
|
|
3058
|
+
}
|
|
3059
|
+
return { rewritten, unrewritten, editedFiles };
|
|
3060
|
+
}
|
|
3061
|
+
function relInside(root, abs, what) {
|
|
3062
|
+
const rel = path3.relative(root, abs);
|
|
3063
|
+
if (rel === "" || rel.startsWith("..") || path3.isAbsolute(rel)) {
|
|
3064
|
+
throw new Error(`mv: ${what} escapes the served root`);
|
|
3065
|
+
}
|
|
3066
|
+
return rel.split(path3.sep).join("/");
|
|
3067
|
+
}
|
|
3068
|
+
function storeOf(rel) {
|
|
3069
|
+
return ":" + rel.split("/").join(":");
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
// ../engine/ts/src/watch.ts
|
|
3073
|
+
import fs3 from "node:fs";
|
|
3074
|
+
import path4 from "node:path";
|
|
3075
|
+
var OVERLAY_FILES = /* @__PURE__ */ new Set(["body.yamlover", "meta.yamlover", "settings.yamlover"]);
|
|
3076
|
+
function watchTree(absRoot, onBatch, opts = {}) {
|
|
3077
|
+
const root = path4.resolve(absRoot);
|
|
3078
|
+
const debounceMs = opts.debounceMs ?? 300;
|
|
3079
|
+
const pending = /* @__PURE__ */ new Set();
|
|
3080
|
+
let timer = null;
|
|
3081
|
+
let running = false;
|
|
3082
|
+
let closed = false;
|
|
3083
|
+
const relevant = (rel) => {
|
|
3084
|
+
const segs = rel.split(path4.sep);
|
|
3085
|
+
for (let i = 0; i < segs.length; i++) {
|
|
3086
|
+
if (!segs[i].startsWith(".")) continue;
|
|
3087
|
+
const isOverlay = segs[i] === ".yamlover" && i === segs.length - 2 && OVERLAY_FILES.has(segs[i + 1]);
|
|
3088
|
+
if (!isOverlay) return false;
|
|
3089
|
+
}
|
|
3090
|
+
return !opts.ignore?.(path4.join(root, rel));
|
|
3091
|
+
};
|
|
3092
|
+
const fire = () => {
|
|
3093
|
+
timer = null;
|
|
3094
|
+
if (running || closed || pending.size === 0) return;
|
|
3095
|
+
const batch = [...pending].map((p) => p.split(path4.sep).join("/"));
|
|
3096
|
+
pending.clear();
|
|
3097
|
+
running = true;
|
|
3098
|
+
try {
|
|
3099
|
+
onBatch(batch);
|
|
3100
|
+
} finally {
|
|
3101
|
+
running = false;
|
|
3102
|
+
if (pending.size > 0) timer = setTimeout(fire, debounceMs);
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
const watcher = fs3.watch(root, { recursive: true }, (_event, filename) => {
|
|
3106
|
+
if (closed || filename == null) return;
|
|
3107
|
+
const rel = String(filename);
|
|
3108
|
+
if (!relevant(rel)) return;
|
|
3109
|
+
pending.add(rel);
|
|
3110
|
+
if (timer) clearTimeout(timer);
|
|
3111
|
+
timer = setTimeout(fire, debounceMs);
|
|
3112
|
+
});
|
|
3113
|
+
return () => {
|
|
3114
|
+
closed = true;
|
|
3115
|
+
if (timer) clearTimeout(timer);
|
|
3116
|
+
watcher.close();
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
// ../engine/ts/src/settings.ts
|
|
3121
|
+
import fs4 from "node:fs";
|
|
3122
|
+
import path5 from "node:path";
|
|
3123
|
+
var DEFAULT_SETTINGS = {
|
|
3124
|
+
annotations: { location: ":annotations" },
|
|
3125
|
+
tags: { location: ":tags" }
|
|
3126
|
+
};
|
|
3127
|
+
function loadSettings(absRoot) {
|
|
3128
|
+
const file = path5.join(absRoot, ".yamlover", "settings.yamlover");
|
|
3129
|
+
if (!fs4.existsSync(file)) return DEFAULT_SETTINGS;
|
|
3130
|
+
let root;
|
|
3131
|
+
try {
|
|
3132
|
+
root = parseYamlover(fs4.readFileSync(file, "utf8"), file).root;
|
|
3133
|
+
} catch {
|
|
3134
|
+
return DEFAULT_SETTINGS;
|
|
3135
|
+
}
|
|
3136
|
+
return {
|
|
3137
|
+
annotations: { location: locationSetting(valueAt(root, "annotations", "location"), DEFAULT_SETTINGS.annotations.location) },
|
|
3138
|
+
tags: { location: locationSetting(valueAt(root, "tags", "location"), DEFAULT_SETTINGS.tags.location) }
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
function valueAt(node, ...keys) {
|
|
3142
|
+
let v = node;
|
|
3143
|
+
for (const key of keys) {
|
|
3144
|
+
if (v === void 0 || isPointer(v)) return void 0;
|
|
3145
|
+
v = v.entries?.find((e2) => e2.key === key)?.value;
|
|
3146
|
+
}
|
|
3147
|
+
return v;
|
|
3148
|
+
}
|
|
3149
|
+
function locationSetting(v, dflt) {
|
|
3150
|
+
if (v === void 0) return dflt;
|
|
3151
|
+
if (isPointer(v)) return pointerPath(v) ?? dflt;
|
|
3152
|
+
if (v.kind === "scalar" && typeof v.value === "string") return stringPath(v.value, dflt);
|
|
3153
|
+
return dflt;
|
|
3154
|
+
}
|
|
3155
|
+
function pointerPath(p) {
|
|
3156
|
+
if (p.base.scope !== "current" && p.base.scope !== "document") return null;
|
|
3157
|
+
if (p.steps.length === 0 || !p.steps.every((s) => s.sel === "key")) return null;
|
|
3158
|
+
const segs = p.steps.map((s) => s.name);
|
|
3159
|
+
if (segs.some((seg) => seg === ".." || seg === "")) return null;
|
|
3160
|
+
return ":" + segs.join(":");
|
|
3161
|
+
}
|
|
3162
|
+
function stringPath(v, dflt) {
|
|
3163
|
+
if (v.trim() === "") return dflt;
|
|
3164
|
+
const segs = v.trim().split(/[/:]/).filter((seg) => seg !== "");
|
|
3165
|
+
if (segs.length === 0 || segs.some((seg) => seg === "..")) return dflt;
|
|
3166
|
+
return ":" + segs.join(":");
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
// ../engine/ts/src/query.ts
|
|
3170
|
+
function parseQuery(text) {
|
|
3171
|
+
let rest = text.trim();
|
|
3172
|
+
let base = { scope: "current" };
|
|
3173
|
+
if (rest.startsWith(":::")) {
|
|
3174
|
+
rest = rest.slice(3);
|
|
3175
|
+
base = { scope: "link", authority: "" };
|
|
3176
|
+
} else if (rest.startsWith("::")) {
|
|
3177
|
+
rest = rest.slice(2);
|
|
3178
|
+
base = { scope: "link", authority: "" };
|
|
3179
|
+
} else if (rest.startsWith(":")) {
|
|
3180
|
+
rest = rest.slice(1);
|
|
3181
|
+
base = { scope: "document" };
|
|
3182
|
+
}
|
|
3183
|
+
const raws = splitPortions(rest);
|
|
3184
|
+
if (base.scope === "link") {
|
|
3185
|
+
const auth = raws.shift();
|
|
3186
|
+
if (auth === void 0 || auth === "") throw new SyntaxError(`query: "::" needs an authority in "${text}"`);
|
|
3187
|
+
base = { scope: "link", authority: keyName(auth) };
|
|
3188
|
+
} else if (base.scope === "current" && raws[0] === "..") {
|
|
3189
|
+
base = { scope: "parent" };
|
|
3190
|
+
raws.shift();
|
|
3191
|
+
}
|
|
3192
|
+
const portions = [];
|
|
3193
|
+
for (const r of raws) portions.push(...parsePortion(r));
|
|
3194
|
+
return { base, portions };
|
|
3195
|
+
}
|
|
3196
|
+
function splitPortions(s) {
|
|
3197
|
+
const out = [];
|
|
3198
|
+
let cur = "";
|
|
3199
|
+
let q = null;
|
|
3200
|
+
for (let i = 0; i < s.length; i++) {
|
|
3201
|
+
const c = s[i];
|
|
3202
|
+
if (q !== null) {
|
|
3203
|
+
cur += c;
|
|
3204
|
+
if (c === q) q = null;
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
3208
|
+
cur += c + s[i + 1];
|
|
3209
|
+
i++;
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
3212
|
+
if (c === "'" || c === '"') {
|
|
3213
|
+
q = c;
|
|
3214
|
+
cur += c;
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
if (c === "!" && s.startsWith("!!<", i)) {
|
|
3218
|
+
const close = s.indexOf(">", i);
|
|
3219
|
+
if (close < 0) throw new SyntaxError(`query: unterminated "!!<\u2026>" in "${s}"`);
|
|
3220
|
+
cur += s.slice(i, close + 1);
|
|
3221
|
+
i = close;
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
if (c === ":") {
|
|
3225
|
+
out.push(cur.trim());
|
|
3226
|
+
cur = "";
|
|
3227
|
+
continue;
|
|
3228
|
+
}
|
|
3229
|
+
cur += c;
|
|
3230
|
+
}
|
|
3231
|
+
out.push(cur.trim());
|
|
3232
|
+
return out.filter((p) => p !== "");
|
|
3233
|
+
}
|
|
3234
|
+
var CMP = /^(>=|<=|!=|=|>|<)/;
|
|
3235
|
+
function parsePortion(r) {
|
|
3236
|
+
if (r === "...") return [{ kind: "descend" }];
|
|
3237
|
+
if (r === "..") return [{ kind: "spine" }];
|
|
3238
|
+
if (r === "?..") return [{ kind: "up", sel: "any" }];
|
|
3239
|
+
if (r === "[]..") return [{ kind: "up", sel: "keyless" }];
|
|
3240
|
+
if (r.startsWith("!!<") && r.endsWith(">")) return [parseMeta(r.slice(3, -1))];
|
|
3241
|
+
{
|
|
3242
|
+
const sp = unquotedSpace(r);
|
|
3243
|
+
if (sp > 0) {
|
|
3244
|
+
const head = parseValTest(r.slice(0, sp));
|
|
3245
|
+
if (head !== null) return [{ kind: "combo", test: head, then: parsePortion(r.slice(sp + 1).trim()) }];
|
|
3246
|
+
throw new SyntaxError(`query: a key containing a space must be quoted ("${r}")`);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (r.endsWith("..") && !r.endsWith("\\..") && r.length > 2) {
|
|
3250
|
+
return [{ kind: "up", sel: keyName(r.slice(0, -2)) }];
|
|
3251
|
+
}
|
|
3252
|
+
if (r === "?") return [{ kind: "anykey" }];
|
|
3253
|
+
if (r === "[?]") return [{ kind: "anypos" }];
|
|
3254
|
+
if (r.endsWith("[?]") && !r.endsWith("\\[?]")) {
|
|
3255
|
+
return [...parsePortion(r.slice(0, -3)), { kind: "anypos" }];
|
|
3256
|
+
}
|
|
3257
|
+
const test = parseValTest(r);
|
|
3258
|
+
if (test !== null) return [test];
|
|
3259
|
+
const steps = parsePointer(r).steps;
|
|
3260
|
+
return steps.map((st) => st.sel === "key" ? { kind: "key", name: st.name } : st.sel === "index" ? { kind: "index", n: st.n } : { kind: "spine" });
|
|
3261
|
+
}
|
|
3262
|
+
function parseValTest(r) {
|
|
3263
|
+
const m = CMP.exec(r);
|
|
3264
|
+
if (m) {
|
|
3265
|
+
const lit = literal(
|
|
3266
|
+
r.slice(m[0].length).trim(),
|
|
3267
|
+
/*bareWordsOk*/
|
|
3268
|
+
true
|
|
3269
|
+
);
|
|
3270
|
+
return { kind: "valtest", op: m[0], value: lit };
|
|
3271
|
+
}
|
|
3272
|
+
const t2 = r.trim();
|
|
3273
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t2)) return { kind: "valtest", op: "=", value: Number(t2) };
|
|
3274
|
+
if (t2 === "true") return { kind: "valtest", op: "=", value: true };
|
|
3275
|
+
if (t2 === "false") return { kind: "valtest", op: "=", value: false };
|
|
3276
|
+
if (t2 === "null") return { kind: "valtest", op: "=", value: null };
|
|
3277
|
+
return null;
|
|
3278
|
+
}
|
|
3279
|
+
function literal(t2, bareWordsOk) {
|
|
3280
|
+
if ((t2[0] === "'" || t2[0] === '"') && t2[t2.length - 1] === t2[0]) {
|
|
3281
|
+
return t2.slice(1, -1).replace(t2[0] === "'" ? /''/g : /\\"/g, t2[0]);
|
|
3282
|
+
}
|
|
3283
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t2)) return Number(t2);
|
|
3284
|
+
if (t2 === "true") return true;
|
|
3285
|
+
if (t2 === "false") return false;
|
|
3286
|
+
if (t2 === "null") return null;
|
|
3287
|
+
if (bareWordsOk) return t2;
|
|
3288
|
+
throw new SyntaxError(`query: bad literal "${t2}"`);
|
|
3289
|
+
}
|
|
3290
|
+
function parseMeta(inner) {
|
|
3291
|
+
const t2 = inner.trim();
|
|
3292
|
+
if (t2.startsWith("*")) {
|
|
3293
|
+
const p = parsePointer(t2.slice(1).trim());
|
|
3294
|
+
const last = p.steps[p.steps.length - 1];
|
|
3295
|
+
if (!last || last.sel !== "key") throw new SyntaxError(`query: bad schema matcher "!!<${inner}>"`);
|
|
3296
|
+
return { kind: "meta", schema: last.name };
|
|
3297
|
+
}
|
|
3298
|
+
const root = parseYamlover(t2).root;
|
|
3299
|
+
const out = { kind: "meta" };
|
|
3300
|
+
for (const e2 of root.entries ?? []) {
|
|
3301
|
+
if (isPointer(e2.value) || e2.value.kind !== "scalar") continue;
|
|
3302
|
+
if (e2.key === "type") out.type = String(e2.value.value);
|
|
3303
|
+
if (e2.key === "format") out.format = String(e2.value.value);
|
|
3304
|
+
}
|
|
3305
|
+
if (out.type === void 0 && out.format === void 0) {
|
|
3306
|
+
throw new SyntaxError(`query: a "!!<\u2026>" matcher needs type:/format: or a schema pointer ("${inner}")`);
|
|
3307
|
+
}
|
|
3308
|
+
return out;
|
|
3309
|
+
}
|
|
3310
|
+
function unquotedSpace(s) {
|
|
3311
|
+
let q = null;
|
|
3312
|
+
for (let i = 0; i < s.length; i++) {
|
|
3313
|
+
const c = s[i];
|
|
3314
|
+
if (q !== null) {
|
|
3315
|
+
if (c === q) q = null;
|
|
3316
|
+
continue;
|
|
3317
|
+
}
|
|
3318
|
+
if (c === "\\") {
|
|
3319
|
+
i++;
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3322
|
+
if (c === "'" || c === '"') {
|
|
3323
|
+
q = c;
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
if (c === "!" && s.startsWith("!!<", i)) {
|
|
3327
|
+
const close = s.indexOf(">", i);
|
|
3328
|
+
if (close < 0) return -1;
|
|
3329
|
+
i = close;
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
if (c === " ") return i;
|
|
3333
|
+
}
|
|
3334
|
+
return -1;
|
|
3335
|
+
}
|
|
3336
|
+
function keyName(portion) {
|
|
3337
|
+
const steps = parsePointer(portion).steps;
|
|
3338
|
+
if (steps.length !== 1 || steps[0].sel !== "key") throw new SyntaxError(`query: bad name portion "${portion}"`);
|
|
3339
|
+
return steps[0].name;
|
|
3340
|
+
}
|
|
3341
|
+
function evalQuery(s, text, from = ":") {
|
|
3342
|
+
const q = parseQuery(text);
|
|
3343
|
+
let binds;
|
|
3344
|
+
switch (q.base.scope) {
|
|
3345
|
+
case "current":
|
|
3346
|
+
binds = [from];
|
|
3347
|
+
break;
|
|
3348
|
+
case "parent":
|
|
3349
|
+
binds = compact(spineParent(s, from) === null ? [] : [spineParent(s, from)]);
|
|
3350
|
+
break;
|
|
3351
|
+
case "document":
|
|
3352
|
+
binds = [docRootOf(s, from)];
|
|
3353
|
+
break;
|
|
3354
|
+
case "link": {
|
|
3355
|
+
const hit = childByKey(s, ":", q.base.authority);
|
|
3356
|
+
binds = hit === null ? [] : [hit];
|
|
3357
|
+
break;
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
for (const p of q.portions) {
|
|
3361
|
+
binds = dedup(step(s, binds, p));
|
|
3362
|
+
if (binds.length === 0) break;
|
|
3363
|
+
}
|
|
3364
|
+
return dedup(binds);
|
|
3365
|
+
}
|
|
3366
|
+
function step(s, binds, p) {
|
|
3367
|
+
const out = [];
|
|
3368
|
+
for (const b of binds) {
|
|
3369
|
+
switch (p.kind) {
|
|
3370
|
+
case "key": {
|
|
3371
|
+
const hit = childByKey(s, b, p.name);
|
|
3372
|
+
if (hit !== null) out.push(hit);
|
|
3373
|
+
break;
|
|
3374
|
+
}
|
|
3375
|
+
case "index": {
|
|
3376
|
+
const row = s.db.prepare(
|
|
3377
|
+
"SELECT to_path AS t FROM edge WHERE from_path = ? AND kind IN ('contain','ref') AND pos = ? LIMIT 1"
|
|
3378
|
+
).get(b, p.n);
|
|
3379
|
+
if (row) out.push(row.t);
|
|
3380
|
+
break;
|
|
3381
|
+
}
|
|
3382
|
+
case "anykey": {
|
|
3383
|
+
for (const r of ownEntries(s, b)) if (r.label !== null) out.push(r.to);
|
|
3384
|
+
for (const r of anchorEntries(s, b)) if (r.label !== null) out.push(r.member);
|
|
3385
|
+
break;
|
|
3386
|
+
}
|
|
3387
|
+
case "anypos": {
|
|
3388
|
+
for (const r of ownEntries(s, b)) out.push(r.to);
|
|
3389
|
+
for (const r of anchorEntries(s, b)) out.push(r.member);
|
|
3390
|
+
break;
|
|
3391
|
+
}
|
|
3392
|
+
case "descend":
|
|
3393
|
+
out.push(...descend(s, b));
|
|
3394
|
+
break;
|
|
3395
|
+
case "spine": {
|
|
3396
|
+
const up = spineParent(s, b);
|
|
3397
|
+
if (up !== null) out.push(up);
|
|
3398
|
+
break;
|
|
3399
|
+
}
|
|
3400
|
+
case "up":
|
|
3401
|
+
out.push(...uplinks(s, b, p.sel));
|
|
3402
|
+
break;
|
|
3403
|
+
case "valtest":
|
|
3404
|
+
if (valOk(s.node(b), p)) out.push(b);
|
|
3405
|
+
break;
|
|
3406
|
+
case "meta":
|
|
3407
|
+
if (metaOk(s, b, p)) out.push(b);
|
|
3408
|
+
break;
|
|
3409
|
+
case "combo": {
|
|
3410
|
+
if (!valOk(s.node(b), p.test)) break;
|
|
3411
|
+
let inner = [b];
|
|
3412
|
+
for (const t2 of p.then) inner = step(s, inner, t2);
|
|
3413
|
+
out.push(...inner);
|
|
3414
|
+
break;
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
return out;
|
|
3419
|
+
}
|
|
3420
|
+
function ownEntries(s, p) {
|
|
3421
|
+
return s.db.prepare(
|
|
3422
|
+
"SELECT to_path AS t, label FROM edge WHERE from_path = ? AND kind IN ('contain','ref') ORDER BY pos"
|
|
3423
|
+
).all(p).map((r) => ({ to: r.t, label: r.label }));
|
|
3424
|
+
}
|
|
3425
|
+
function anchorEntries(s, p) {
|
|
3426
|
+
return s.db.prepare(
|
|
3427
|
+
"SELECT from_path AS f, label FROM edge WHERE to_path = ? AND kind = 'back' ORDER BY from_path"
|
|
3428
|
+
).all(p).map((r) => ({ member: r.f, label: r.label }));
|
|
3429
|
+
}
|
|
3430
|
+
function childByKey(s, p, name) {
|
|
3431
|
+
const own = s.db.prepare(
|
|
3432
|
+
"SELECT to_path AS t FROM edge WHERE from_path = ? AND kind IN ('contain','ref') AND label = ? LIMIT 1"
|
|
3433
|
+
).get(p, name);
|
|
3434
|
+
if (own) return own.t;
|
|
3435
|
+
const anchored = s.db.prepare(
|
|
3436
|
+
"SELECT from_path AS f FROM edge WHERE to_path = ? AND kind = 'back' AND label = ? ORDER BY from_path LIMIT 1"
|
|
3437
|
+
).get(p, name);
|
|
3438
|
+
return anchored ? anchored.f : null;
|
|
3439
|
+
}
|
|
3440
|
+
function spineParent(s, p) {
|
|
3441
|
+
const row = s.db.prepare(
|
|
3442
|
+
"SELECT from_path AS f FROM edge WHERE to_path = ? AND kind = 'contain' LIMIT 1"
|
|
3443
|
+
).get(p);
|
|
3444
|
+
return row ? row.f : null;
|
|
3445
|
+
}
|
|
3446
|
+
function uplinks(s, p, sel) {
|
|
3447
|
+
const out = [];
|
|
3448
|
+
const label = sel === "any" || sel === "keyless" ? null : sel;
|
|
3449
|
+
const contains = s.db.prepare(
|
|
3450
|
+
sel === "any" ? "SELECT from_path AS x FROM edge WHERE to_path = ? AND kind = 'contain'" : "SELECT from_path AS x FROM edge WHERE to_path = ? AND kind = 'contain' AND label " + (sel === "keyless" ? "IS NULL" : "= ?")
|
|
3451
|
+
);
|
|
3452
|
+
const backs = s.db.prepare(
|
|
3453
|
+
sel === "any" ? "SELECT to_path AS x FROM edge WHERE from_path = ? AND kind = 'back'" : "SELECT to_path AS x FROM edge WHERE from_path = ? AND kind = 'back' AND label " + (sel === "keyless" ? "IS NULL" : "= ?")
|
|
3454
|
+
);
|
|
3455
|
+
const refs = s.db.prepare(
|
|
3456
|
+
sel === "any" ? "SELECT from_path AS x FROM edge WHERE to_path = ? AND kind = 'ref'" : "SELECT from_path AS x FROM edge WHERE to_path = ? AND kind = 'ref' AND label " + (sel === "keyless" ? "IS NULL" : "= ?")
|
|
3457
|
+
);
|
|
3458
|
+
const args = (q) => label === null ? q.all(p) : q.all(p, label);
|
|
3459
|
+
for (const r of args(contains)) out.push(r.x);
|
|
3460
|
+
for (const r of args(backs)) out.push(r.x);
|
|
3461
|
+
for (const r of args(refs)) out.push(r.x);
|
|
3462
|
+
return out;
|
|
3463
|
+
}
|
|
3464
|
+
function descend(s, p) {
|
|
3465
|
+
const out = [p];
|
|
3466
|
+
const kids = s.db.prepare(
|
|
3467
|
+
"SELECT to_path AS t FROM edge WHERE from_path = ? AND kind = 'contain' ORDER BY pos"
|
|
3468
|
+
).all(p);
|
|
3469
|
+
for (const k of kids) out.push(...descend(s, k.t));
|
|
3470
|
+
return out;
|
|
3471
|
+
}
|
|
3472
|
+
function docRootOf(s, p) {
|
|
3473
|
+
let cur = p;
|
|
3474
|
+
while (cur !== null && cur !== ":") {
|
|
3475
|
+
const row = s.node(cur);
|
|
3476
|
+
if (row?.meta && row.meta.documentRoot === true) return cur;
|
|
3477
|
+
cur = spineParent(s, cur);
|
|
3478
|
+
}
|
|
3479
|
+
return ":";
|
|
3480
|
+
}
|
|
3481
|
+
function valOk(row, t2) {
|
|
3482
|
+
if (!row || row.type !== "scalar") return false;
|
|
3483
|
+
const v = row.value;
|
|
3484
|
+
switch (t2.op) {
|
|
3485
|
+
case "=":
|
|
3486
|
+
return v === t2.value;
|
|
3487
|
+
case "!=":
|
|
3488
|
+
return v !== t2.value;
|
|
3489
|
+
case ">":
|
|
3490
|
+
return typeof v === "number" && typeof t2.value === "number" && v > t2.value;
|
|
3491
|
+
case ">=":
|
|
3492
|
+
return typeof v === "number" && typeof t2.value === "number" && v >= t2.value;
|
|
3493
|
+
case "<":
|
|
3494
|
+
return typeof v === "number" && typeof t2.value === "number" && v < t2.value;
|
|
3495
|
+
case "<=":
|
|
3496
|
+
return typeof v === "number" && typeof t2.value === "number" && v <= t2.value;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
function metaOk(s, p, t2) {
|
|
3500
|
+
const row = s.node(p);
|
|
3501
|
+
if (!row) return false;
|
|
3502
|
+
if (t2.schema !== void 0) return row.format === `x-yamlover-${t2.schema}`;
|
|
3503
|
+
if (t2.format !== void 0 && row.format !== t2.format) return false;
|
|
3504
|
+
if (t2.type !== void 0) {
|
|
3505
|
+
switch (t2.type) {
|
|
3506
|
+
case "binary":
|
|
3507
|
+
return row.type === "blob";
|
|
3508
|
+
case "array":
|
|
3509
|
+
return row.is_array;
|
|
3510
|
+
case "object":
|
|
3511
|
+
return row.type === "mapping" && !row.is_array;
|
|
3512
|
+
case "string":
|
|
3513
|
+
return row.type === "scalar" && typeof row.value === "string";
|
|
3514
|
+
case "integer":
|
|
3515
|
+
return row.type === "scalar" && typeof row.value === "number" && Number.isInteger(row.value);
|
|
3516
|
+
case "number":
|
|
3517
|
+
return row.type === "scalar" && typeof row.value === "number";
|
|
3518
|
+
case "boolean":
|
|
3519
|
+
return row.type === "scalar" && typeof row.value === "boolean";
|
|
3520
|
+
case "variant":
|
|
3521
|
+
return row.type === "scalar" && hasOwnChild(s, p);
|
|
3522
|
+
default:
|
|
3523
|
+
return false;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
return true;
|
|
3527
|
+
}
|
|
3528
|
+
function hasOwnChild(s, p) {
|
|
3529
|
+
return s.db.prepare("SELECT 1 FROM edge WHERE from_path = ? AND kind IN ('contain','ref') LIMIT 1").get(p) !== void 0;
|
|
3530
|
+
}
|
|
3531
|
+
function dedup(paths) {
|
|
3532
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3533
|
+
const out = [];
|
|
3534
|
+
for (const p of paths) if (!seen.has(p)) {
|
|
3535
|
+
seen.add(p);
|
|
3536
|
+
out.push(p);
|
|
3537
|
+
}
|
|
3538
|
+
return out;
|
|
3539
|
+
}
|
|
3540
|
+
function compact(xs) {
|
|
3541
|
+
return xs.filter((x) => x !== null);
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
// src/server/gitignore.ts
|
|
3545
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
3546
|
+
import fs5 from "node:fs";
|
|
3547
|
+
import path6 from "node:path";
|
|
3548
|
+
function buildGitIgnore(dataRoot) {
|
|
3549
|
+
const gitRoot = findGitRoot(dataRoot) ?? dataRoot;
|
|
3550
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3551
|
+
const matcherFor = (dir) => {
|
|
3552
|
+
if (!cache.has(dir)) {
|
|
3553
|
+
let ig = null;
|
|
3554
|
+
try {
|
|
3555
|
+
const gi = path6.join(dir, ".gitignore");
|
|
3556
|
+
if (fs5.statSync(gi).isFile()) ig = (0, import_ignore.default)().add(fs5.readFileSync(gi, "utf-8"));
|
|
3557
|
+
} catch {
|
|
3558
|
+
}
|
|
3559
|
+
cache.set(dir, ig);
|
|
3560
|
+
}
|
|
3561
|
+
return cache.get(dir);
|
|
3562
|
+
};
|
|
3563
|
+
return (absPath) => {
|
|
3564
|
+
const fromRoot = path6.relative(gitRoot, absPath);
|
|
3565
|
+
if (fromRoot === ".git" || fromRoot.startsWith(".git" + path6.sep)) return true;
|
|
3566
|
+
let isDir = false;
|
|
3567
|
+
try {
|
|
3568
|
+
isDir = fs5.statSync(absPath).isDirectory();
|
|
3569
|
+
} catch {
|
|
3570
|
+
}
|
|
3571
|
+
const rel = path6.relative(gitRoot, path6.dirname(absPath));
|
|
3572
|
+
const segs = rel === "" ? [] : rel.split(path6.sep);
|
|
3573
|
+
let dir = gitRoot;
|
|
3574
|
+
const dirs = [gitRoot];
|
|
3575
|
+
for (const s of segs) {
|
|
3576
|
+
dir = path6.join(dir, s);
|
|
3577
|
+
dirs.push(dir);
|
|
3578
|
+
}
|
|
3579
|
+
for (const d of dirs) {
|
|
3580
|
+
const ig = matcherFor(d);
|
|
3581
|
+
if (!ig) continue;
|
|
3582
|
+
const relToDir = path6.relative(d, absPath);
|
|
3583
|
+
if (!relToDir) continue;
|
|
3584
|
+
if (ig.ignores(relToDir) || isDir && ig.ignores(relToDir + "/")) return true;
|
|
3585
|
+
}
|
|
3586
|
+
return false;
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
function findGitRoot(start) {
|
|
3590
|
+
let dir = path6.resolve(start);
|
|
3591
|
+
try {
|
|
3592
|
+
if (!fs5.statSync(dir).isDirectory()) dir = path6.dirname(dir);
|
|
3593
|
+
} catch {
|
|
3594
|
+
return null;
|
|
3595
|
+
}
|
|
3596
|
+
for (; ; ) {
|
|
3597
|
+
if (fs5.existsSync(path6.join(dir, ".git"))) return dir;
|
|
3598
|
+
const parent = path6.dirname(dir);
|
|
3599
|
+
if (parent === dir) return null;
|
|
3600
|
+
dir = parent;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
// src/server/node-kind.ts
|
|
3605
|
+
function ownedEntries(s, p) {
|
|
3606
|
+
return s.entries(p).filter((e2) => e2.kind !== "back");
|
|
3607
|
+
}
|
|
3608
|
+
function displayKind(s, p, row) {
|
|
3609
|
+
const ents = ownedEntries(s, p);
|
|
3610
|
+
if (row.type === "blob") return ents.length ? "omni" : "binary";
|
|
3611
|
+
if (row.type === "scalar") return ents.length ? "omni" : "scalar";
|
|
3612
|
+
if (!ents.length) return row.is_array ? "array" : "object";
|
|
3613
|
+
if (row.is_array) return "array";
|
|
3614
|
+
return ents.some((e2) => e2.label === null) ? "mix" : "object";
|
|
3615
|
+
}
|
|
3616
|
+
function typeName(s, p, row) {
|
|
3617
|
+
const k = displayKind(s, p, row);
|
|
3618
|
+
if (k === "scalar") return scalarType(row.value);
|
|
3619
|
+
if (k === "mix") return "mixed";
|
|
3620
|
+
if (k === "omni") return "variant";
|
|
3621
|
+
return k;
|
|
3622
|
+
}
|
|
3623
|
+
function scalarType(v) {
|
|
3624
|
+
if (v === null) return "null";
|
|
3625
|
+
if (typeof v === "boolean") return "boolean";
|
|
3626
|
+
if (typeof v === "number") return Number.isInteger(v) ? "integer" : "number";
|
|
3627
|
+
return "string";
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// src/server/tasks.ts
|
|
3631
|
+
var PROGRESS_EMIT_MS = 150;
|
|
3632
|
+
var KEEP_FINISHED_MS = 5e3;
|
|
3633
|
+
var TaskRegistry = class {
|
|
3634
|
+
constructor(emit) {
|
|
3635
|
+
this.emit = emit;
|
|
3636
|
+
}
|
|
3637
|
+
seq = 0;
|
|
3638
|
+
tasks = /* @__PURE__ */ new Map();
|
|
3639
|
+
lastEmit = /* @__PURE__ */ new Map();
|
|
3640
|
+
start(label) {
|
|
3641
|
+
const id = "t" + ++this.seq;
|
|
3642
|
+
const info = { id, label, state: "running", progress: { done: 0 }, startedAt: Date.now() };
|
|
3643
|
+
this.tasks.set(id, info);
|
|
3644
|
+
this.send(info, true);
|
|
3645
|
+
const finish = (state, error) => {
|
|
3646
|
+
if (info.state !== "running") return;
|
|
3647
|
+
info.state = state;
|
|
3648
|
+
info.finishedAt = Date.now();
|
|
3649
|
+
if (error !== void 0) info.error = error;
|
|
3650
|
+
this.send(info, true);
|
|
3651
|
+
};
|
|
3652
|
+
return {
|
|
3653
|
+
id,
|
|
3654
|
+
progress: (done, total, message) => {
|
|
3655
|
+
if (info.state !== "running") return;
|
|
3656
|
+
info.progress = { done, ...total !== void 0 && { total }, ...message !== void 0 && { message } };
|
|
3657
|
+
this.send(info, false);
|
|
3658
|
+
},
|
|
3659
|
+
done: () => finish("done"),
|
|
3660
|
+
fail: (err) => finish("error", String(err?.message ?? err))
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
/** Running tasks + ones finished within the last few seconds (pruning the rest). */
|
|
3664
|
+
list() {
|
|
3665
|
+
const now = Date.now();
|
|
3666
|
+
for (const [id, t2] of this.tasks) {
|
|
3667
|
+
if (t2.state !== "running" && now - (t2.finishedAt ?? 0) > KEEP_FINISHED_MS) {
|
|
3668
|
+
this.tasks.delete(id);
|
|
3669
|
+
this.lastEmit.delete(id);
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
return [...this.tasks.values()].map((t2) => ({ ...t2, progress: { ...t2.progress } }));
|
|
3673
|
+
}
|
|
3674
|
+
send(t2, always) {
|
|
3675
|
+
const now = Date.now();
|
|
3676
|
+
if (!always && now - (this.lastEmit.get(t2.id) ?? 0) < PROGRESS_EMIT_MS) return;
|
|
3677
|
+
this.lastEmit.set(t2.id, now);
|
|
3678
|
+
this.emit({ ...t2, progress: { ...t2.progress } });
|
|
3679
|
+
}
|
|
3680
|
+
};
|
|
3681
|
+
|
|
3682
|
+
// src/server/engine-api.ts
|
|
3683
|
+
var LINK_KEY = "$yamloverLink";
|
|
3684
|
+
var BINARY_KEY = "$yamloverBinary";
|
|
3685
|
+
var MIXED_KEY = "$yamloverMixed";
|
|
3686
|
+
function createHandlers(dataRoot, opts = {}) {
|
|
3687
|
+
const rootName = path7.basename(path7.resolve(dataRoot)) || "/";
|
|
3688
|
+
const dbPath = path7.join(dataRoot, ".yamlover", "index.db");
|
|
3689
|
+
const settings = loadSettings(dataRoot);
|
|
3690
|
+
const ignore2 = opts.gitignore === false ? void 0 : buildGitIgnore(dataRoot);
|
|
3691
|
+
fs6.mkdirSync(path7.dirname(dbPath), { recursive: true });
|
|
3692
|
+
const store0 = new Store(dbPath);
|
|
3693
|
+
const store = () => store0;
|
|
3694
|
+
const log = opts.log ?? (() => {
|
|
3695
|
+
});
|
|
3696
|
+
let closed = false;
|
|
3697
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
3698
|
+
const sseWrite = (frame) => {
|
|
3699
|
+
const payload = JSON.stringify(frame);
|
|
3700
|
+
for (const res of sseClients) res.write(`data: ${payload}
|
|
3701
|
+
|
|
3702
|
+
`);
|
|
3703
|
+
};
|
|
3704
|
+
const broadcast = (diff) => {
|
|
3705
|
+
if (diff.added.length + diff.changed.length + diff.removed.length + diff.moved.length === 0) return;
|
|
3706
|
+
const toClient = (rel) => segsToStr(rel.split("/"));
|
|
3707
|
+
sseWrite({
|
|
3708
|
+
type: "diff",
|
|
3709
|
+
added: diff.added.map(toClient),
|
|
3710
|
+
changed: diff.changed.map(toClient),
|
|
3711
|
+
removed: diff.removed.map(toClient),
|
|
3712
|
+
moved: diff.moved.map((m) => ({ from: toClient(m.from), to: toClient(m.to) }))
|
|
3713
|
+
});
|
|
3714
|
+
};
|
|
3715
|
+
const announce = (d) => broadcast({ added: [], changed: [], removed: [], moved: [], ...d });
|
|
3716
|
+
const relFileOf = (clientPath) => strToSegs(clientPath).map(String).join("/");
|
|
3717
|
+
const tasks = new TaskRegistry((t2) => sseWrite({ type: "task", task: t2 }));
|
|
3718
|
+
let chain = Promise.resolve();
|
|
3719
|
+
const enqueue = (fn) => {
|
|
3720
|
+
const p = chain.then(fn);
|
|
3721
|
+
chain = p.catch(() => {
|
|
3722
|
+
});
|
|
3723
|
+
return p;
|
|
3724
|
+
};
|
|
3725
|
+
const gib = (b) => (b / 2 ** 30).toFixed(1);
|
|
3726
|
+
const BIG_FILE_BYTES = 256 * 2 ** 20;
|
|
3727
|
+
let hashing = false;
|
|
3728
|
+
const scheduleHasher = () => {
|
|
3729
|
+
if (hashing || closed) return;
|
|
3730
|
+
if (store0.unhashedFiles(1).length === 0) return;
|
|
3731
|
+
hashing = true;
|
|
3732
|
+
void (async () => {
|
|
3733
|
+
const skip = /* @__PURE__ */ new Set();
|
|
3734
|
+
let done = 0;
|
|
3735
|
+
let lastLog = 0;
|
|
3736
|
+
const t0 = Date.now();
|
|
3737
|
+
let h = null;
|
|
3738
|
+
try {
|
|
3739
|
+
for (; ; ) {
|
|
3740
|
+
if (closed) break;
|
|
3741
|
+
const pending = store0.unhashedFiles().filter((f) => !skip.has(f.path));
|
|
3742
|
+
if (pending.length === 0) break;
|
|
3743
|
+
h ??= tasks.start("hashing large files");
|
|
3744
|
+
const next = pending[0];
|
|
3745
|
+
const total = done + pending.length;
|
|
3746
|
+
h.progress(done, total, next.path);
|
|
3747
|
+
const abs = path7.join(dataRoot, ...next.path.split("/"));
|
|
3748
|
+
let hash = null;
|
|
3749
|
+
try {
|
|
3750
|
+
hash = await hashFileAsync(abs, (bytes) => {
|
|
3751
|
+
if (next.size >= BIG_FILE_BYTES) h?.progress(done, total, `${next.path} \u2014 ${gib(bytes)}/${gib(next.size)} GiB`);
|
|
3752
|
+
});
|
|
3753
|
+
} catch {
|
|
3754
|
+
}
|
|
3755
|
+
const st = hash !== null ? fs6.statSync(abs, { throwIfNoEntry: false }) : void 0;
|
|
3756
|
+
const fresh = st !== void 0 && st.size === next.size && st.mtimeMs === next.mtimeMs;
|
|
3757
|
+
const ok = hash !== null && fresh && !closed ? await enqueue(() => store0.setFileHash(next.path, hash, next.size, next.mtimeMs)) : false;
|
|
3758
|
+
if (!ok) {
|
|
3759
|
+
skip.add(next.path);
|
|
3760
|
+
continue;
|
|
3761
|
+
}
|
|
3762
|
+
done++;
|
|
3763
|
+
const now = Date.now();
|
|
3764
|
+
if (now - lastLog >= 500) {
|
|
3765
|
+
lastLog = now;
|
|
3766
|
+
log(`hashing ${done}/${total} \u2014 ${next.path}`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
h?.done();
|
|
3770
|
+
if (h) log(`hashing done \u2014 ${done} file(s) in ${((Date.now() - t0) / 1e3).toFixed(1)}s`);
|
|
3771
|
+
} catch (e2) {
|
|
3772
|
+
h?.fail(e2);
|
|
3773
|
+
log(`hashing FAILED \u2014 ${String(e2?.message ?? e2)}`);
|
|
3774
|
+
} finally {
|
|
3775
|
+
hashing = false;
|
|
3776
|
+
}
|
|
3777
|
+
})();
|
|
3778
|
+
};
|
|
3779
|
+
const doReindex = () => reindexAsync(store0, dataRoot, { ignore: ignore2 });
|
|
3780
|
+
const runIndexTask = (label) => enqueue(async () => {
|
|
3781
|
+
const h = tasks.start(label);
|
|
3782
|
+
const t0 = Date.now();
|
|
3783
|
+
let lastLog = 0;
|
|
3784
|
+
log(`${label}\u2026`);
|
|
3785
|
+
try {
|
|
3786
|
+
const diff = await reindexAsync(store0, dataRoot, {
|
|
3787
|
+
ignore: ignore2,
|
|
3788
|
+
onProgress: (p) => {
|
|
3789
|
+
h.progress(p.done, p.total, p.message);
|
|
3790
|
+
const now = Date.now();
|
|
3791
|
+
if (now - lastLog >= 500) {
|
|
3792
|
+
lastLog = now;
|
|
3793
|
+
log(`${label} ${p.done}/${p.total ?? "?"}${p.message ? ` \u2014 ${p.message}` : ""}`);
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
});
|
|
3797
|
+
h.done();
|
|
3798
|
+
log(
|
|
3799
|
+
`${label} done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (+${diff.added.length} ~${diff.changed.length} \u2212${diff.removed.length} \u2192${diff.moved.length})`
|
|
3800
|
+
);
|
|
3801
|
+
broadcast(diff);
|
|
3802
|
+
scheduleHasher();
|
|
3803
|
+
return diff;
|
|
3804
|
+
} catch (e2) {
|
|
3805
|
+
h.fail(e2);
|
|
3806
|
+
log(`${label} FAILED \u2014 ${String(e2?.message ?? e2)}`);
|
|
3807
|
+
throw e2;
|
|
3808
|
+
}
|
|
3809
|
+
});
|
|
3810
|
+
const reconcile = () => enqueue(async () => {
|
|
3811
|
+
const h = tasks.start("reconciling");
|
|
3812
|
+
try {
|
|
3813
|
+
const diff = await doReindex();
|
|
3814
|
+
if (diff.moved.length > 0) {
|
|
3815
|
+
const r = relinkMoved(dataRoot, diff.moved, { ignore: ignore2 });
|
|
3816
|
+
if (r.editedFiles.length > 0) {
|
|
3817
|
+
const follow = reindex(store0, dataRoot, { ignore: ignore2 });
|
|
3818
|
+
diff.changed = [.../* @__PURE__ */ new Set([...diff.changed, ...follow.changed])];
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
h.done();
|
|
3822
|
+
broadcast(diff);
|
|
3823
|
+
scheduleHasher();
|
|
3824
|
+
return diff;
|
|
3825
|
+
} catch (e2) {
|
|
3826
|
+
h.fail(e2);
|
|
3827
|
+
throw e2;
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
const ready = runIndexTask(`indexing ${rootName}`);
|
|
3831
|
+
const stopWatch = opts.watch ? watchTree(dataRoot, () => {
|
|
3832
|
+
reconcile().catch((e2) => log(`reconcile FAILED \u2014 ${String(e2?.message ?? e2)}`));
|
|
3833
|
+
}, { ignore: ignore2 }) : null;
|
|
3834
|
+
const handler = (req, res, url) => {
|
|
3835
|
+
try {
|
|
3836
|
+
const s = store();
|
|
3837
|
+
if (req.method === "GET" && url.pathname === "/api/events") {
|
|
3838
|
+
res.statusCode = 200;
|
|
3839
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
3840
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
3841
|
+
res.setHeader("Connection", "keep-alive");
|
|
3842
|
+
res.write(": connected\n\n");
|
|
3843
|
+
sseClients.add(res);
|
|
3844
|
+
const ping = setInterval(() => res.write(": ping\n\n"), 3e4);
|
|
3845
|
+
req.on("close", () => {
|
|
3846
|
+
clearInterval(ping);
|
|
3847
|
+
sseClients.delete(res);
|
|
3848
|
+
});
|
|
3849
|
+
return;
|
|
3850
|
+
}
|
|
3851
|
+
if (req.method === "POST" && url.pathname === "/api/reindex") {
|
|
3852
|
+
reconcile().then((diff) => sendJson(res, 200, diff)).catch((e2) => sendJson(res, 500, { error: String(e2.message || e2) }));
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
if (url.pathname === "/api/tasks") {
|
|
3856
|
+
sendJson(res, 200, tasks.list());
|
|
3857
|
+
return;
|
|
3858
|
+
}
|
|
3859
|
+
if (url.pathname === "/api/query") {
|
|
3860
|
+
const q = url.searchParams.get("q") || "";
|
|
3861
|
+
const at = storePath(strToSegs(url.searchParams.get("path") || ":"));
|
|
3862
|
+
try {
|
|
3863
|
+
const results = evalQuery(s, q, at).map((p2) => segsToStr(storePathToSegs(p2)));
|
|
3864
|
+
sendJson(res, 200, { results });
|
|
3865
|
+
} catch (e2) {
|
|
3866
|
+
sendJson(res, 400, { error: String(e2.message || e2) });
|
|
3867
|
+
}
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
if (url.pathname === "/api/dangling") {
|
|
3871
|
+
sendJson(res, 200, s.dangling().map((d) => ({ from: segsToStr(storePathToSegs(d.from)), raw: d.raw, reason: d.reason })));
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
if (req.method === "POST" && url.pathname === "/api/annotate") {
|
|
3875
|
+
readBody(req).then(
|
|
3876
|
+
(data) => (
|
|
3877
|
+
// Queued: an incremental row added while a full walk (whose disk snapshot predates
|
|
3878
|
+
// this annotation) is committing would be silently swapped away.
|
|
3879
|
+
enqueue(() => {
|
|
3880
|
+
const a = data;
|
|
3881
|
+
const tagStore = storePath(strToSegs(a.tag ?? ""));
|
|
3882
|
+
if (!a?.tag || s.node(tagStore)?.format !== TAG_FORMAT) {
|
|
3883
|
+
throw new Error("annotation needs a `tag` that is an x-yamlover-tag node");
|
|
3884
|
+
}
|
|
3885
|
+
const annPath = writeAnnotation(dataRoot, settings.annotations.location, a);
|
|
3886
|
+
const doc = parseYamlover(fs6.readFileSync(path7.join(dataRoot, ...strToSegs(annPath).map(String)), "utf8"), annPath);
|
|
3887
|
+
s.addAnnotation(storePath(strToSegs(annPath)), storePath(strToSegs(a.target)), doc, tagStore);
|
|
3888
|
+
announce({ added: [relFileOf(annPath)] });
|
|
3889
|
+
return { path: annPath };
|
|
3890
|
+
})
|
|
3891
|
+
)
|
|
3892
|
+
).then((body) => sendJson(res, 201, body)).catch((e2) => sendJson(res, 400, { error: String(e2.message || e2) }));
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
if (req.method === "DELETE" && url.pathname === "/api/annotate") {
|
|
3896
|
+
const annPath = url.searchParams.get("path") || "";
|
|
3897
|
+
enqueue(() => {
|
|
3898
|
+
deleteAnnotation(dataRoot, s, annPath);
|
|
3899
|
+
s.removeAnnotation(storePath(strToSegs(annPath)));
|
|
3900
|
+
announce({ removed: [relFileOf(annPath)] });
|
|
3901
|
+
}).then(() => sendJson(res, 200, { ok: true })).catch((e2) => sendJson(res, 400, { error: String(e2.message || e2) }));
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
if (req.method === "POST" && url.pathname === "/api/tag") {
|
|
3905
|
+
readBody(req).then(
|
|
3906
|
+
(data) => enqueue(async () => {
|
|
3907
|
+
const name = String(data?.name ?? "").trim();
|
|
3908
|
+
if (!name) throw new Error("tag needs a non-empty name");
|
|
3909
|
+
const segs2 = [...strToSegs(settings.tags.location), name];
|
|
3910
|
+
const tagPath = segsToStr(segs2);
|
|
3911
|
+
const existing = s.node(storePath(segs2));
|
|
3912
|
+
if (existing) {
|
|
3913
|
+
if (existing.format !== TAG_FORMAT) throw new Error(`a node already exists at ${tagPath} and is not a tag`);
|
|
3914
|
+
const color = s.node(storePath(segs2) + ":color")?.value;
|
|
3915
|
+
return { path: tagPath, name, color: typeof color === "string" ? color : null, created: false };
|
|
3916
|
+
}
|
|
3917
|
+
const written = writeTag(dataRoot, settings.tags.location, name);
|
|
3918
|
+
s.addTag(storePath(strToSegs(settings.tags.location)), name, written.pos, written.node);
|
|
3919
|
+
if (s.node(storePath(segs2))?.format !== TAG_FORMAT) throw new Error(`the created tag did not index as a tag: ${tagPath}`);
|
|
3920
|
+
announce(written.createdFile ? { added: [written.file] } : { changed: [written.file] });
|
|
3921
|
+
return { path: tagPath, name, color: null, created: true };
|
|
3922
|
+
})
|
|
3923
|
+
).then((body) => sendJson(res, 201, body)).catch((e2) => sendJson(res, 400, { error: String(e2.message || e2) }));
|
|
3924
|
+
return;
|
|
3925
|
+
}
|
|
3926
|
+
if (req.method === "POST" && url.pathname === "/api/paste") {
|
|
3927
|
+
readBody(req).then(
|
|
3928
|
+
(data) => enqueue(async () => {
|
|
3929
|
+
const result = handlePaste(dataRoot, s, data);
|
|
3930
|
+
broadcast(await doReindex());
|
|
3931
|
+
scheduleHasher();
|
|
3932
|
+
return result;
|
|
3933
|
+
})
|
|
3934
|
+
).then((result) => sendJson(res, 201, result)).catch((e2) => sendJson(res, 400, { error: String(e2.message || e2) }));
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
if (req.method === "POST" && url.pathname === "/api/mv") {
|
|
3938
|
+
readBody(req).then(
|
|
3939
|
+
(data) => enqueue(async () => {
|
|
3940
|
+
const { from, to } = data;
|
|
3941
|
+
const rel = (p2, what) => {
|
|
3942
|
+
const segs2 = strToSegs(p2);
|
|
3943
|
+
if (segs2.length === 0) throw new Error(`mv: ${what} must name a file or directory`);
|
|
3944
|
+
if (segs2.some((g) => typeof g === "number")) throw new Error(`mv: ${what} must be a file/directory path (no positions)`);
|
|
3945
|
+
return segs2.join("/");
|
|
3946
|
+
};
|
|
3947
|
+
const report = mv(dataRoot, rel(from ?? "", "from"), rel(to ?? "", "to"), { ignore: ignore2 });
|
|
3948
|
+
const diff = await doReindex();
|
|
3949
|
+
broadcast(diff);
|
|
3950
|
+
return { ...report, diff };
|
|
3951
|
+
})
|
|
3952
|
+
).then((body) => sendJson(res, 200, body)).catch((e2) => sendJson(res, 400, { error: String(e2.message || e2) }));
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
const segs = strToSegs(url.searchParams.get("path") || ":");
|
|
3956
|
+
const p = storePath(segs);
|
|
3957
|
+
const depth = parseDepth(url.searchParams.get("depth"));
|
|
3958
|
+
if (url.pathname === "/api/info") {
|
|
3959
|
+
sendJson(res, 200, { root: rootName });
|
|
3960
|
+
return;
|
|
3961
|
+
}
|
|
3962
|
+
if (url.pathname === "/api/annotations") {
|
|
3963
|
+
sendJson(res, 200, annotationsFor(dataRoot, s, segs));
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
if (url.pathname === "/api/tagged") {
|
|
3967
|
+
const row2 = s.node(p);
|
|
3968
|
+
if (!row2 || row2.format !== TAG_FORMAT) return notFound(res, url);
|
|
3969
|
+
sendJson(res, 200, taggedMaterials(dataRoot, s, p));
|
|
3970
|
+
return;
|
|
3971
|
+
}
|
|
3972
|
+
if (url.pathname === "/api/tree") {
|
|
3973
|
+
const row2 = s.node(p);
|
|
3974
|
+
if (!row2) return notFound(res, url);
|
|
3975
|
+
const label = segs.length === 0 ? rootName : labelFor(s, p, segs[segs.length - 1]);
|
|
3976
|
+
sendJson(res, 200, buildTree(dataRoot, s, segs, label, depth ?? 3));
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
if (url.pathname === "/api/blob") {
|
|
3980
|
+
const file = path7.join(dataRoot, ...segs.map(String));
|
|
3981
|
+
if (!fs6.existsSync(file) || fs6.statSync(file).isDirectory()) return notFound(res, url);
|
|
3982
|
+
res.statusCode = 200;
|
|
3983
|
+
res.setHeader("Content-Type", s.node(p)?.format ?? formatFromExt(file) ?? "application/octet-stream");
|
|
3984
|
+
res.setHeader("Content-Length", String(fs6.statSync(file).size));
|
|
3985
|
+
const stream = fs6.createReadStream(file);
|
|
3986
|
+
stream.on("error", () => res.destroy());
|
|
3987
|
+
stream.pipe(res);
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
3990
|
+
const row = s.node(p);
|
|
3991
|
+
if (!row) return notFound(res, url);
|
|
3992
|
+
const viewDepth = depth ?? 1;
|
|
3993
|
+
const kind = displayKind(s, p, row);
|
|
3994
|
+
if (url.pathname === "/api/json") {
|
|
3995
|
+
const wantBytes = kind === "binary" && url.searchParams.get("binary") === "1";
|
|
3996
|
+
sendJson(res, 200, {
|
|
3997
|
+
path: segsToStr(segs),
|
|
3998
|
+
type: tocType(s, p, row),
|
|
3999
|
+
format: row.format ?? null,
|
|
4000
|
+
concrete: concreteOf(dataRoot, segs, row),
|
|
4001
|
+
// dir | yamlover | null (stat-derived; engine tracks no per-node concrete yet)
|
|
4002
|
+
documentPath: documentPath(s, segs),
|
|
4003
|
+
// nearest enclosing document root (for `/…` links)
|
|
4004
|
+
title: titleOf(s, p),
|
|
4005
|
+
description: null,
|
|
4006
|
+
value: wantBytes ? binaryContent(dataRoot, segs, row) : projectValue(dataRoot, s, segs, viewDepth, true),
|
|
4007
|
+
relations: buildRelations(dataRoot, s, segs)
|
|
4008
|
+
});
|
|
4009
|
+
} else if (url.pathname === "/api/schema") {
|
|
4010
|
+
sendJson(res, 200, projectSchema(dataRoot, s, segs, viewDepth, true));
|
|
4011
|
+
} else {
|
|
4012
|
+
notFound(res, url);
|
|
4013
|
+
}
|
|
4014
|
+
} catch (exc) {
|
|
4015
|
+
sendJson(res, 400, { error: exc.message || String(exc) });
|
|
4016
|
+
}
|
|
4017
|
+
};
|
|
4018
|
+
return Object.assign(handler, {
|
|
4019
|
+
ready,
|
|
4020
|
+
close: () => {
|
|
4021
|
+
closed = true;
|
|
4022
|
+
stopWatch?.();
|
|
4023
|
+
for (const r of sseClients) r.end();
|
|
4024
|
+
sseClients.clear();
|
|
4025
|
+
store0.close();
|
|
4026
|
+
}
|
|
4027
|
+
});
|
|
4028
|
+
}
|
|
4029
|
+
function tocType(s, p, row) {
|
|
4030
|
+
return typeName(s, p, row);
|
|
4031
|
+
}
|
|
4032
|
+
function concreteOf(dataRoot, segs, row) {
|
|
4033
|
+
if (row.type !== "mapping") return null;
|
|
4034
|
+
if (segs.some((g) => typeof g === "number")) return null;
|
|
4035
|
+
const abs = path7.resolve(dataRoot, ...segs.map(String));
|
|
4036
|
+
let st;
|
|
4037
|
+
try {
|
|
4038
|
+
st = fs6.statSync(abs);
|
|
4039
|
+
} catch {
|
|
4040
|
+
return null;
|
|
4041
|
+
}
|
|
4042
|
+
if (!st.isDirectory()) return null;
|
|
4043
|
+
return fs6.existsSync(path7.join(abs, ".yamlover")) ? "yamlover" : "dir";
|
|
4044
|
+
}
|
|
4045
|
+
var relKey = (label, other) => `${label ?? ""}\0${other}`;
|
|
4046
|
+
function downstreamEntries(s, p) {
|
|
4047
|
+
const isSet = !!s.node(p)?.meta?.set;
|
|
4048
|
+
let own = s.entries(p).filter((e2) => e2.kind !== "back");
|
|
4049
|
+
const seen = new Set(own.map((e2) => relKey(e2.label, e2.to)));
|
|
4050
|
+
if (isSet) {
|
|
4051
|
+
const kept = /* @__PURE__ */ new Set();
|
|
4052
|
+
own = own.filter((e2) => {
|
|
4053
|
+
const k = relKey(e2.label, e2.to);
|
|
4054
|
+
if (kept.has(k)) return false;
|
|
4055
|
+
kept.add(k);
|
|
4056
|
+
return true;
|
|
4057
|
+
});
|
|
4058
|
+
}
|
|
4059
|
+
const out = [...own];
|
|
4060
|
+
const backs = s.relationships(p).in.filter((e2) => e2.kind === "back" && e2.from).sort((a, b) => a.from < b.from ? -1 : a.from > b.from ? 1 : 0);
|
|
4061
|
+
for (const e2 of backs) {
|
|
4062
|
+
const k = relKey(e2.label, e2.from);
|
|
4063
|
+
if (e2.label != null || isSet) {
|
|
4064
|
+
if (seen.has(k)) continue;
|
|
4065
|
+
seen.add(k);
|
|
4066
|
+
}
|
|
4067
|
+
out.push({ to: e2.from, label: e2.label, pos: null, kind: "ref" });
|
|
4068
|
+
}
|
|
4069
|
+
return out;
|
|
4070
|
+
}
|
|
4071
|
+
function projectValue(dataRoot, s, segs, depth, top) {
|
|
4072
|
+
const p = storePath(segs);
|
|
4073
|
+
const row = s.node(p);
|
|
4074
|
+
const k = displayKind(s, p, row);
|
|
4075
|
+
if (!top && depth <= 0) return linkMarker(dataRoot, s, segs);
|
|
4076
|
+
if (k === "binary" && !top) return linkMarker(dataRoot, s, segs);
|
|
4077
|
+
if (k === "binary") return { size: row.size, format: row.format };
|
|
4078
|
+
const kids = downstreamEntries(s, p);
|
|
4079
|
+
const project = (c) => c.kind === "contain" ? projectValue(dataRoot, s, [...segs, c.label ?? c.pos ?? 0], depth - 1, false) : linkMarker(dataRoot, s, storePathToSegs(c.to));
|
|
4080
|
+
if (k === "array") return kids.map(project);
|
|
4081
|
+
if (k === "omni" || k === "mix") {
|
|
4082
|
+
const entries = kids.map((c) => ({ key: c.label, value: project(c) }));
|
|
4083
|
+
const marker = { kind: k, entries };
|
|
4084
|
+
if (k === "omni") marker.value = row.value;
|
|
4085
|
+
return { [MIXED_KEY]: marker };
|
|
4086
|
+
}
|
|
4087
|
+
if (k === "object") {
|
|
4088
|
+
const out = {};
|
|
4089
|
+
for (const c of kids) out[c.label ?? String(c.pos)] = project(c);
|
|
4090
|
+
return out;
|
|
4091
|
+
}
|
|
4092
|
+
return row.value;
|
|
4093
|
+
}
|
|
4094
|
+
function projectSchema(dataRoot, s, segs, depth, top) {
|
|
4095
|
+
const p = storePath(segs);
|
|
4096
|
+
const row = s.node(p);
|
|
4097
|
+
const k = displayKind(s, p, row);
|
|
4098
|
+
if ((k === "object" || k === "array" || k === "mix" || k === "omni") && depth <= 0) return linkMarker(dataRoot, s, segs);
|
|
4099
|
+
if (k === "binary" && !top) return linkMarker(dataRoot, s, segs);
|
|
4100
|
+
const schema = { type: typeName(s, p, row) };
|
|
4101
|
+
if (row.format) schema.format = row.format;
|
|
4102
|
+
const kids = downstreamEntries(s, p);
|
|
4103
|
+
const sub = (c) => c.kind === "contain" ? projectSchema(dataRoot, s, [...segs, c.label ?? c.pos ?? 0], depth - 1, false) : linkMarker(dataRoot, s, storePathToSegs(c.to));
|
|
4104
|
+
if (k === "object" || k === "mix" || k === "omni") {
|
|
4105
|
+
const props = {};
|
|
4106
|
+
for (const c of kids) props[c.label ?? `[${c.pos}]`] = sub(c);
|
|
4107
|
+
schema.properties = props;
|
|
4108
|
+
if (k === "omni") schema.value = row.value;
|
|
4109
|
+
} else if (k === "array") {
|
|
4110
|
+
schema.prefixItems = kids.map(sub);
|
|
4111
|
+
schema.items = false;
|
|
4112
|
+
} else if (k === "binary") {
|
|
4113
|
+
schema.const = { size: row.size, format: row.format };
|
|
4114
|
+
} else {
|
|
4115
|
+
schema.const = row.value;
|
|
4116
|
+
}
|
|
4117
|
+
const t2 = titleOf(s, p);
|
|
4118
|
+
if (t2) schema.title = t2;
|
|
4119
|
+
return schema;
|
|
4120
|
+
}
|
|
4121
|
+
function linkMarker(dataRoot, s, segs) {
|
|
4122
|
+
const p = storePath(segs);
|
|
4123
|
+
const row = s.node(p);
|
|
4124
|
+
const k = displayKind(s, p, row);
|
|
4125
|
+
const info = { kind: k, type: tocType(s, p, row), path: segsToStr(segs) };
|
|
4126
|
+
if (row.format) info.format = row.format;
|
|
4127
|
+
const concrete = concreteOf(dataRoot, segs, row);
|
|
4128
|
+
if (concrete) info.concrete = concrete;
|
|
4129
|
+
const title = titleOf(s, p);
|
|
4130
|
+
if (title) info.title = title;
|
|
4131
|
+
if (k === "binary") info.size = row.size;
|
|
4132
|
+
else if (k === "scalar") info.value = row.value;
|
|
4133
|
+
else if (k === "omni" || k === "mix") {
|
|
4134
|
+
info.count = ownedEntries(s, p).length;
|
|
4135
|
+
if (k === "omni") info.value = row.value;
|
|
4136
|
+
} else info.count = s.children(p).length;
|
|
4137
|
+
if (row.format === TAG_FORMAT) {
|
|
4138
|
+
const c = s.node(p + ":color")?.value;
|
|
4139
|
+
if (typeof c === "string") info.color = c;
|
|
4140
|
+
}
|
|
4141
|
+
return { [LINK_KEY]: info };
|
|
4142
|
+
}
|
|
4143
|
+
var segsEqual = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
4144
|
+
function scopedPath(s, src, currentDoc) {
|
|
4145
|
+
if (segsEqual(documentRootSegs(s, src), currentDoc)) return segsToStr(src.slice(currentDoc.length));
|
|
4146
|
+
return "::" + segsToStr(src).slice(1);
|
|
4147
|
+
}
|
|
4148
|
+
function buildRelations(dataRoot, s, segs) {
|
|
4149
|
+
const p = storePath(segs);
|
|
4150
|
+
const out = {};
|
|
4151
|
+
const put = (label, marker) => {
|
|
4152
|
+
let k = label;
|
|
4153
|
+
for (let i = 2; k in out; i++) k = `${label} (${i})`;
|
|
4154
|
+
out[k] = marker;
|
|
4155
|
+
};
|
|
4156
|
+
if (segs.length > 0) put("..", linkMarker(dataRoot, s, segs.slice(0, -1)));
|
|
4157
|
+
const currentDoc = documentRootSegs(s, segs);
|
|
4158
|
+
const { out: outEdges, in: inEdges } = s.relationships(p);
|
|
4159
|
+
const upstream = /* @__PURE__ */ new Map();
|
|
4160
|
+
const addUp = (src, label) => {
|
|
4161
|
+
if (src) upstream.set(relKey(label, src), src);
|
|
4162
|
+
};
|
|
4163
|
+
for (const e2 of inEdges) if (e2.kind === "ref") addUp(e2.from, e2.label);
|
|
4164
|
+
for (const e2 of outEdges) if (e2.kind === "back") addUp(e2.to, e2.label);
|
|
4165
|
+
for (const src of upstream.values()) {
|
|
4166
|
+
const segs2 = storePathToSegs(src);
|
|
4167
|
+
put(scopedPath(s, segs2, currentDoc), linkMarker(dataRoot, s, segs2));
|
|
4168
|
+
}
|
|
4169
|
+
return out;
|
|
4170
|
+
}
|
|
4171
|
+
function binaryContent(dataRoot, segs, row) {
|
|
4172
|
+
const file = path7.join(dataRoot, ...segs.map(String));
|
|
4173
|
+
const bytes = fs6.existsSync(file) ? fs6.readFileSync(file) : Buffer.alloc(0);
|
|
4174
|
+
return { [BINARY_KEY]: { format: row.format ?? null, size: row.size ?? bytes.length, base64: bytes.toString("base64") } };
|
|
4175
|
+
}
|
|
4176
|
+
function buildTree(dataRoot, s, segs, label, depth) {
|
|
4177
|
+
const p = storePath(segs);
|
|
4178
|
+
const row = s.node(p);
|
|
4179
|
+
const node = {
|
|
4180
|
+
path: segsToStr(segs),
|
|
4181
|
+
label,
|
|
4182
|
+
type: tocType(s, p, row),
|
|
4183
|
+
format: row.format ?? null,
|
|
4184
|
+
concrete: concreteOf(dataRoot, segs, row),
|
|
4185
|
+
hasChildren: s.hasChildren(p),
|
|
4186
|
+
children: []
|
|
4187
|
+
};
|
|
4188
|
+
if (s.hasChildren(p) && depth > 0) {
|
|
4189
|
+
for (const c of s.children(p)) {
|
|
4190
|
+
const seg = c.label ?? c.pos ?? 0;
|
|
4191
|
+
node.children.push(buildTree(dataRoot, s, [...segs, seg], labelFor(s, c.to, seg), depth - 1));
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
return node;
|
|
4195
|
+
}
|
|
4196
|
+
function labelFor(s, p, keyOrIdx) {
|
|
4197
|
+
const t2 = titleOf(s, p);
|
|
4198
|
+
if (t2) return t2;
|
|
4199
|
+
return typeof keyOrIdx === "number" ? `[${keyOrIdx}]` : keyOrIdx;
|
|
4200
|
+
}
|
|
4201
|
+
var TAG_FORMAT = "x-yamlover-tag";
|
|
4202
|
+
function appliedTag(s, annStorePath) {
|
|
4203
|
+
const e2 = s.relationships(annStorePath).out.find(
|
|
4204
|
+
(t2) => t2.kind === "back" && t2.label === null && s.node(t2.to)?.format === TAG_FORMAT
|
|
4205
|
+
);
|
|
4206
|
+
if (!e2) return null;
|
|
4207
|
+
const segs = storePathToSegs(e2.to);
|
|
4208
|
+
const color = s.node(e2.to + ":color")?.value;
|
|
4209
|
+
return { path: segsToStr(segs), name: String(segs[segs.length - 1] ?? ""), color: typeof color === "string" ? color : null };
|
|
4210
|
+
}
|
|
4211
|
+
function annotationsFor(dataRoot, s, segs) {
|
|
4212
|
+
const p = storePath(segs);
|
|
4213
|
+
const out = [];
|
|
4214
|
+
for (const e2 of s.relationships(p).in) {
|
|
4215
|
+
if (e2.kind !== "ref") continue;
|
|
4216
|
+
const src = s.node(e2.from);
|
|
4217
|
+
if (src?.format !== "x-yamlover-annotation") continue;
|
|
4218
|
+
const aSegs = storePathToSegs(e2.from);
|
|
4219
|
+
out.push({
|
|
4220
|
+
path: segsToStr(aSegs),
|
|
4221
|
+
tag: appliedTag(s, e2.from),
|
|
4222
|
+
...projectValue(dataRoot, s, aSegs, 6, true)
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
return out;
|
|
4226
|
+
}
|
|
4227
|
+
function taggedMaterials(dataRoot, s, tagStorePath) {
|
|
4228
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4229
|
+
const out = [];
|
|
4230
|
+
const backs = s.relationships(tagStorePath).in.filter((e2) => e2.kind === "back" && e2.from).sort((a, b) => a.from < b.from ? -1 : a.from > b.from ? 1 : 0);
|
|
4231
|
+
for (const e2 of backs) {
|
|
4232
|
+
let material = e2.from;
|
|
4233
|
+
if (s.node(e2.from)?.format === "x-yamlover-annotation") {
|
|
4234
|
+
const t2 = s.relationships(e2.from).out.find((o) => o.kind === "ref" && o.label === "target");
|
|
4235
|
+
if (!t2) continue;
|
|
4236
|
+
material = t2.to;
|
|
4237
|
+
}
|
|
4238
|
+
if (seen.has(material) || !s.node(material)) continue;
|
|
4239
|
+
seen.add(material);
|
|
4240
|
+
out.push(linkMarker(dataRoot, s, storePathToSegs(material)));
|
|
4241
|
+
}
|
|
4242
|
+
return out;
|
|
4243
|
+
}
|
|
4244
|
+
function pointerRaw(clientPath) {
|
|
4245
|
+
let out = "";
|
|
4246
|
+
for (const seg of strToSegs(clientPath)) {
|
|
4247
|
+
out += typeof seg === "number" ? `[${seg}]` : (out === "" ? "" : ":") + colonSegment(seg);
|
|
4248
|
+
}
|
|
4249
|
+
return "::" + out;
|
|
4250
|
+
}
|
|
4251
|
+
function yScalar(v) {
|
|
4252
|
+
return typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(String(v ?? ""));
|
|
4253
|
+
}
|
|
4254
|
+
function writeAnnotation(dataRoot, location, a) {
|
|
4255
|
+
if (!a?.target || !a?.tag) throw new Error("annotation needs a target and a tag");
|
|
4256
|
+
const dir = path7.resolve(dataRoot, ...strToSegs(location).map(String));
|
|
4257
|
+
const root = path7.resolve(dataRoot);
|
|
4258
|
+
if (dir !== root && !dir.startsWith(root + path7.sep)) throw new Error("annotation location escapes the data root");
|
|
4259
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
4260
|
+
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4261
|
+
const file = `${id}.yamlover`;
|
|
4262
|
+
const lines = [
|
|
4263
|
+
"!!<*yamlover/$defs/annotation>",
|
|
4264
|
+
`target: ${pointerToken(pointerRaw(a.target))}`,
|
|
4265
|
+
anchorToken(`${pointerRaw(a.tag)}[]`)
|
|
4266
|
+
// the applied tag holds me (ordinal path anchor)
|
|
4267
|
+
];
|
|
4268
|
+
if (a.selector) lines.push("selector:", ...Object.entries(a.selector).map(([k, v]) => ` ${k}: ${yScalar(v)}`));
|
|
4269
|
+
if (a.description) lines.push(`description: ${yScalar(a.description)}`);
|
|
4270
|
+
lines.push(`created: ${(/* @__PURE__ */ new Date()).toISOString()}`, "");
|
|
4271
|
+
fs6.writeFileSync(path7.join(dir, file), lines.join("\n"));
|
|
4272
|
+
return `${location}:${file}`;
|
|
4273
|
+
}
|
|
4274
|
+
function writeTag(dataRoot, location, name) {
|
|
4275
|
+
if (/[/\\\r\n:]/.test(name)) throw new Error("a tag name cannot contain '/', '\\', ':' or line breaks");
|
|
4276
|
+
const root = path7.resolve(dataRoot);
|
|
4277
|
+
const dir = path7.resolve(dataRoot, ...strToSegs(location).map(String), ".yamlover");
|
|
4278
|
+
if (!dir.startsWith(root + path7.sep)) throw new Error("tags location escapes the data root");
|
|
4279
|
+
const file = path7.join(dir, "body.yamlover");
|
|
4280
|
+
const createdFile = !fs6.existsSync(file);
|
|
4281
|
+
const head = "# Named tags created from the annotation picker (settings.yamlover: tags.location).\n";
|
|
4282
|
+
const existing = createdFile ? head : fs6.readFileSync(file, "utf8");
|
|
4283
|
+
const body = (existing === "" || existing.endsWith("\n") ? existing : existing + "\n") + `${name}: !!<*yamlover/$defs/tag>
|
|
4284
|
+
`;
|
|
4285
|
+
const entries = parseYamlover(body, file).root.entries ?? [];
|
|
4286
|
+
const pos = entries.findIndex((e2) => e2.key === name);
|
|
4287
|
+
const entry = pos >= 0 ? entries[pos] : void 0;
|
|
4288
|
+
if (!entry || isPointer(entry.value) || entry.value.meta?.schema === void 0) {
|
|
4289
|
+
throw new Error(`cannot write a tag named ${JSON.stringify(name)}`);
|
|
4290
|
+
}
|
|
4291
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
4292
|
+
fs6.writeFileSync(file, body);
|
|
4293
|
+
return { node: entry.value, pos, file: [...strToSegs(location).map(String), ".yamlover", "body.yamlover"].join("/"), createdFile };
|
|
4294
|
+
}
|
|
4295
|
+
function deleteAnnotation(dataRoot, s, annPath) {
|
|
4296
|
+
const segs = strToSegs(annPath);
|
|
4297
|
+
if (!String(segs[segs.length - 1] ?? "").endsWith(".yamlover")) throw new Error("not an annotation file");
|
|
4298
|
+
if (s.node(storePath(segs))?.format !== "x-yamlover-annotation") throw new Error("not an annotation node");
|
|
4299
|
+
const root = path7.resolve(dataRoot);
|
|
4300
|
+
const file = path7.resolve(dataRoot, ...segs.map(String));
|
|
4301
|
+
if (!file.startsWith(root + path7.sep)) throw new Error("outside the served root");
|
|
4302
|
+
if (!fs6.existsSync(file) || fs6.statSync(file).isDirectory()) throw new Error("not an annotation file");
|
|
4303
|
+
fs6.rmSync(file, { force: true });
|
|
4304
|
+
}
|
|
4305
|
+
function handlePaste(dataRoot, s, input) {
|
|
4306
|
+
const segs = strToSegs(input.path || ":");
|
|
4307
|
+
const row = s.node(storePath(segs));
|
|
4308
|
+
if (!row) throw new Error(`no such node: ${input.path}`);
|
|
4309
|
+
if (input.rich != null) {
|
|
4310
|
+
const rich = parseRich(input.rich);
|
|
4311
|
+
if (row.format === "x-yamlover-chapter") return pasteRichIntoChapter(dataRoot, s, segs, rich);
|
|
4312
|
+
return pasteRichAsChapter(dataRoot, segs, rich);
|
|
4313
|
+
}
|
|
4314
|
+
if (typeof input.text === "string") {
|
|
4315
|
+
const text = input.text.replace(/\r\n?/g, "\n");
|
|
4316
|
+
if (text.trim().length === 0) throw new Error("empty paste (no text)");
|
|
4317
|
+
if (row.format === "x-yamlover-chapter") return pasteTextIntoChapter(dataRoot, s, segs, text);
|
|
4318
|
+
return pasteTextAsChapterFile(dataRoot, segs, text);
|
|
4319
|
+
}
|
|
4320
|
+
const bytes = Buffer.from(input.contentBase64 || "", "base64");
|
|
4321
|
+
if (bytes.length === 0) throw new Error("empty paste (no file bytes)");
|
|
4322
|
+
const name = sanitizeName(input.filename ?? "");
|
|
4323
|
+
if (row.format === "x-yamlover-chapter") return pasteIntoChapter(dataRoot, s, segs, name, bytes);
|
|
4324
|
+
const dirSegs = nearestDirSegs(dataRoot, segs);
|
|
4325
|
+
if (!dirSegs) throw new Error("no enclosing directory to paste into");
|
|
4326
|
+
const dir = path7.resolve(dataRoot, ...dirSegs.map(String));
|
|
4327
|
+
const final = uniqueName(dir, name);
|
|
4328
|
+
writeInside(dataRoot, dir, final, bytes);
|
|
4329
|
+
return { path: segsToStr([...dirSegs, final]), dir: segsToStr(dirSegs), open: dirSegs.length !== segs.length };
|
|
4330
|
+
}
|
|
4331
|
+
function nearestDirSegs(dataRoot, segs) {
|
|
4332
|
+
for (let i = segs.length; i >= 0; i--) {
|
|
4333
|
+
const sub = segs.slice(0, i);
|
|
4334
|
+
const abs = path7.resolve(dataRoot, ...sub.map(String));
|
|
4335
|
+
if (fs6.existsSync(abs) && fs6.statSync(abs).isDirectory()) return sub;
|
|
4336
|
+
}
|
|
4337
|
+
return null;
|
|
4338
|
+
}
|
|
4339
|
+
function chapterSource(dataRoot, s, segs) {
|
|
4340
|
+
const docSegs = documentRootSegs(s, segs);
|
|
4341
|
+
const docFs = path7.resolve(dataRoot, ...docSegs.map(String));
|
|
4342
|
+
const dirBacked = fs6.existsSync(docFs) && fs6.statSync(docFs).isDirectory();
|
|
4343
|
+
const bodyFile = dirBacked ? path7.join(docFs, ".yamlover", "body.yamlover") : docFs;
|
|
4344
|
+
if (!bodyFile.endsWith(".yamlover") || !fs6.existsSync(bodyFile)) {
|
|
4345
|
+
throw new Error("unsupported chapter source (need a .yamlover body)");
|
|
4346
|
+
}
|
|
4347
|
+
return { docSegs, bodyFile, dirBacked };
|
|
4348
|
+
}
|
|
4349
|
+
function pasteIntoChapter(dataRoot, s, segs, name, bytes) {
|
|
4350
|
+
const { docSegs, bodyFile, dirBacked } = chapterSource(dataRoot, s, segs);
|
|
4351
|
+
const writeDirSegs = dirBacked ? docSegs : docSegs.slice(0, -1);
|
|
4352
|
+
const writeDir = path7.resolve(dataRoot, ...writeDirSegs.map(String));
|
|
4353
|
+
const final = uniqueName(writeDir, name);
|
|
4354
|
+
writeInside(dataRoot, writeDir, final, bytes);
|
|
4355
|
+
const fileSegs = [...writeDirSegs, final];
|
|
4356
|
+
const pointer = dirBacked ? `*/${final}` : `*/${segsToStr(fileSegs)}`;
|
|
4357
|
+
const within = segs.slice(docSegs.length);
|
|
4358
|
+
const src = fs6.readFileSync(bodyFile, "utf8");
|
|
4359
|
+
fs6.writeFileSync(bodyFile, appendToList(src, within, "chunks", (indent) => [`${" ".repeat(indent)}- ${pointer}`]));
|
|
4360
|
+
return { path: segsToStr(fileSegs), chapter: segsToStr(segs), pointer };
|
|
4361
|
+
}
|
|
4362
|
+
function pasteTextIntoChapter(dataRoot, s, segs, text) {
|
|
4363
|
+
const { docSegs, bodyFile } = chapterSource(dataRoot, s, segs);
|
|
4364
|
+
const within = segs.slice(docSegs.length);
|
|
4365
|
+
const src = fs6.readFileSync(bodyFile, "utf8");
|
|
4366
|
+
fs6.writeFileSync(bodyFile, appendToList(src, within, "chunks", (indent) => textChunkLines(text, indent)));
|
|
4367
|
+
return { path: segsToStr(segs), chapter: segsToStr(segs) };
|
|
4368
|
+
}
|
|
4369
|
+
function pasteTextAsChapterFile(dataRoot, segs, text) {
|
|
4370
|
+
const dirSegs = nearestDirSegs(dataRoot, segs);
|
|
4371
|
+
if (!dirSegs) throw new Error("no enclosing directory to paste into");
|
|
4372
|
+
const dir = path7.resolve(dataRoot, ...dirSegs.map(String));
|
|
4373
|
+
const title = titleFromText(text);
|
|
4374
|
+
const final = uniqueName(dir, chapterFileName(title));
|
|
4375
|
+
const src = ["!!<*yamlover/$defs/chapter>", `title: ${JSON.stringify(title)}`, "chunks:", ...textChunkLines(text, 0), ""].join("\n");
|
|
4376
|
+
writeInside(dataRoot, dir, final, Buffer.from(src, "utf8"));
|
|
4377
|
+
return { path: segsToStr([...dirSegs, final]), dir: segsToStr(dirSegs), open: dirSegs.length !== segs.length };
|
|
4378
|
+
}
|
|
4379
|
+
function parseRich(raw, depth = 0) {
|
|
4380
|
+
if (depth > 8) throw new Error("rich paste: nesting too deep");
|
|
4381
|
+
const r = raw ?? {};
|
|
4382
|
+
const chunks = [];
|
|
4383
|
+
for (const c of Array.isArray(r.chunks) ? r.chunks : []) {
|
|
4384
|
+
if (typeof c?.text === "string") {
|
|
4385
|
+
if (c.text.trim()) chunks.push({ text: c.text.replace(/\r\n?/g, "\n") });
|
|
4386
|
+
continue;
|
|
4387
|
+
}
|
|
4388
|
+
const f = c?.file;
|
|
4389
|
+
if (f && typeof f.name === "string") {
|
|
4390
|
+
const bytes = Buffer.from(String(f.contentBase64 ?? ""), "base64");
|
|
4391
|
+
if (bytes.length === 0) throw new Error("rich paste: empty file chunk");
|
|
4392
|
+
chunks.push({ name: sanitizeName(f.name), bytes });
|
|
4393
|
+
continue;
|
|
4394
|
+
}
|
|
4395
|
+
throw new Error("rich paste: a chunk must be {text} or {file}");
|
|
4396
|
+
}
|
|
4397
|
+
const children = (Array.isArray(r.children) ? r.children : []).map((k) => {
|
|
4398
|
+
const sub = parseRich(k, depth + 1);
|
|
4399
|
+
const title = typeof k?.title === "string" ? String(k.title).trim() : "";
|
|
4400
|
+
return { ...sub, title: title || "Untitled" };
|
|
4401
|
+
});
|
|
4402
|
+
if (depth === 0 && chunks.length === 0 && children.length === 0) throw new Error("empty rich paste");
|
|
4403
|
+
return { title: typeof r.title === "string" && r.title.trim() ? r.title.trim() : null, chunks, children };
|
|
4404
|
+
}
|
|
4405
|
+
function richItemLines(item, indent, pointerFor) {
|
|
4406
|
+
if ("text" in item) return textChunkLines(item.text, indent);
|
|
4407
|
+
return [`${" ".repeat(indent)}- ${pointerFor(item.name, item.bytes)}`];
|
|
4408
|
+
}
|
|
4409
|
+
function richChildLines(node, indent, pointerFor) {
|
|
4410
|
+
const pad = " ".repeat(indent);
|
|
4411
|
+
const lines = [`${pad}- title: ${JSON.stringify(node.title)}`];
|
|
4412
|
+
if (node.chunks.length) lines.push(`${pad} chunks:`, ...node.chunks.flatMap((c) => richItemLines(c, indent + 2, pointerFor)));
|
|
4413
|
+
if (node.children.length) lines.push(`${pad} children:`, ...node.children.flatMap((k) => richChildLines(k, indent + 2, pointerFor)));
|
|
4414
|
+
return lines;
|
|
4415
|
+
}
|
|
4416
|
+
function pasteRichIntoChapter(dataRoot, s, segs, rich) {
|
|
4417
|
+
const { docSegs, bodyFile, dirBacked } = chapterSource(dataRoot, s, segs);
|
|
4418
|
+
const writeDirSegs = dirBacked ? docSegs : docSegs.slice(0, -1);
|
|
4419
|
+
const writeDir = path7.resolve(dataRoot, ...writeDirSegs.map(String));
|
|
4420
|
+
const files = [];
|
|
4421
|
+
const pointerFor = (name, bytes) => {
|
|
4422
|
+
const final = uniqueName(writeDir, name);
|
|
4423
|
+
writeInside(dataRoot, writeDir, final, bytes);
|
|
4424
|
+
files.push(segsToStr([...writeDirSegs, final]));
|
|
4425
|
+
return dirBacked ? `*/${final}` : `*/${segsToStr([...writeDirSegs, final])}`;
|
|
4426
|
+
};
|
|
4427
|
+
const within = segs.slice(docSegs.length);
|
|
4428
|
+
let src = fs6.readFileSync(bodyFile, "utf8");
|
|
4429
|
+
if (rich.chunks.length) src = appendToList(src, within, "chunks", (ind) => rich.chunks.flatMap((c) => richItemLines(c, ind, pointerFor)));
|
|
4430
|
+
if (rich.children.length) src = appendToList(src, within, "children", (ind) => rich.children.flatMap((k) => richChildLines(k, ind, pointerFor)));
|
|
4431
|
+
fs6.writeFileSync(bodyFile, src);
|
|
4432
|
+
return { path: segsToStr(segs), chapter: segsToStr(segs), files };
|
|
4433
|
+
}
|
|
4434
|
+
function pasteRichAsChapter(dataRoot, segs, rich) {
|
|
4435
|
+
const dirSegs = nearestDirSegs(dataRoot, segs);
|
|
4436
|
+
if (!dirSegs) throw new Error("no enclosing directory to paste into");
|
|
4437
|
+
const dir = path7.resolve(dataRoot, ...dirSegs.map(String));
|
|
4438
|
+
if (!rich.title && rich.chunks.length === 0 && rich.children.length === 1) rich = rich.children[0];
|
|
4439
|
+
const firstText = rich.chunks.find((c) => "text" in c);
|
|
4440
|
+
const title = rich.title ?? (firstText ? titleFromText(firstText.text) : rich.children[0]?.title ?? "Pasted content");
|
|
4441
|
+
if (!richHasFiles(rich)) {
|
|
4442
|
+
const final = uniqueName(dir, chapterFileName(title));
|
|
4443
|
+
const src2 = renderChapterSource(title, rich, () => {
|
|
4444
|
+
throw new Error("unreachable: no files");
|
|
4445
|
+
});
|
|
4446
|
+
writeInside(dataRoot, dir, final, Buffer.from(src2, "utf8"));
|
|
4447
|
+
return { path: segsToStr([...dirSegs, final]), dir: segsToStr(dirSegs), open: dirSegs.length !== segs.length };
|
|
4448
|
+
}
|
|
4449
|
+
const name = uniqueName(dir, chapterFileName(title).replace(/\.yamlover$/, ""));
|
|
4450
|
+
const chDir = path7.join(dir, name);
|
|
4451
|
+
if (!path7.resolve(chDir).startsWith(path7.resolve(dataRoot) + path7.sep)) throw new Error("target escapes the data root");
|
|
4452
|
+
fs6.mkdirSync(path7.join(chDir, ".yamlover"), { recursive: true });
|
|
4453
|
+
const pointerFor = (fname, bytes) => {
|
|
4454
|
+
const final = uniqueName(chDir, fname);
|
|
4455
|
+
writeInside(dataRoot, chDir, final, bytes);
|
|
4456
|
+
return `*/${final}`;
|
|
4457
|
+
};
|
|
4458
|
+
const src = renderChapterSource(title, rich, pointerFor);
|
|
4459
|
+
writeInside(dataRoot, path7.join(chDir, ".yamlover"), "body.yamlover", Buffer.from(src, "utf8"));
|
|
4460
|
+
return { path: segsToStr([...dirSegs, name]), dir: segsToStr(dirSegs), open: dirSegs.length !== segs.length };
|
|
4461
|
+
}
|
|
4462
|
+
function renderChapterSource(title, rich, pointerFor) {
|
|
4463
|
+
const lines = ["!!<*yamlover/$defs/chapter>", `title: ${JSON.stringify(title)}`];
|
|
4464
|
+
if (rich.chunks.length) lines.push("chunks:", ...rich.chunks.flatMap((c) => richItemLines(c, 0, pointerFor)));
|
|
4465
|
+
if (rich.children.length) lines.push("children:", ...rich.children.flatMap((k) => richChildLines(k, 0, pointerFor)));
|
|
4466
|
+
return lines.join("\n") + "\n";
|
|
4467
|
+
}
|
|
4468
|
+
function richHasFiles(rich) {
|
|
4469
|
+
return rich.chunks.some((c) => "bytes" in c) || rich.children.some(richHasFiles);
|
|
4470
|
+
}
|
|
4471
|
+
function titleFromText(text) {
|
|
4472
|
+
const first = text.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
|
|
4473
|
+
const t2 = first.replace(/^#{1,6}\s+/, "").trim();
|
|
4474
|
+
return (t2.length > 80 ? t2.slice(0, 79).trimEnd() + "\u2026" : t2) || "Pasted text";
|
|
4475
|
+
}
|
|
4476
|
+
function chapterFileName(title) {
|
|
4477
|
+
const base = title.replace(/[^\p{L}\p{N} ._-]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, 60).trim().replace(/^\.+/, "");
|
|
4478
|
+
return `${base || "pasted"}.yamlover`;
|
|
4479
|
+
}
|
|
4480
|
+
function sanitizeName(raw) {
|
|
4481
|
+
const base = path7.basename(String(raw || "")).replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+/, "");
|
|
4482
|
+
return base || "pasted";
|
|
4483
|
+
}
|
|
4484
|
+
function uniqueName(dir, name) {
|
|
4485
|
+
if (!fs6.existsSync(path7.join(dir, name))) return name;
|
|
4486
|
+
const ext = path7.extname(name);
|
|
4487
|
+
const stem = name.slice(0, name.length - ext.length);
|
|
4488
|
+
for (let i = 1; ; i++) {
|
|
4489
|
+
const cand = `${stem}-${i}${ext}`;
|
|
4490
|
+
if (!fs6.existsSync(path7.join(dir, cand))) return cand;
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
function writeInside(dataRoot, dir, name, bytes) {
|
|
4494
|
+
const root = path7.resolve(dataRoot);
|
|
4495
|
+
const target = path7.resolve(dir, name);
|
|
4496
|
+
if (target !== root && !target.startsWith(root + path7.sep)) throw new Error("target escapes the data root");
|
|
4497
|
+
fs6.writeFileSync(target, bytes);
|
|
4498
|
+
}
|
|
4499
|
+
var indentOf = (line) => {
|
|
4500
|
+
let i = 0;
|
|
4501
|
+
while (line[i] === " ") i++;
|
|
4502
|
+
return i;
|
|
4503
|
+
};
|
|
4504
|
+
var isContentLine = (line) => {
|
|
4505
|
+
const t2 = line.trim();
|
|
4506
|
+
return t2.length > 0 && !t2.startsWith("#");
|
|
4507
|
+
};
|
|
4508
|
+
function textChunkLines(text, indent) {
|
|
4509
|
+
const pad = " ".repeat(indent);
|
|
4510
|
+
const first = text.split("\n").find((l) => l.trim().length > 0);
|
|
4511
|
+
if (!first || /^\s/.test(first)) return [`${pad}- ${JSON.stringify(text)}`];
|
|
4512
|
+
const body = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
4513
|
+
const head = text.endsWith("\n") ? "|" : "|-";
|
|
4514
|
+
return [`${pad}- ${head}`, ...body.split("\n").map((l) => l.trim().length ? `${pad} ${l}` : "")];
|
|
4515
|
+
}
|
|
4516
|
+
function appendToList(text, chapterPath, key, renderItems) {
|
|
4517
|
+
const lines = text.split("\n");
|
|
4518
|
+
let lo = 0;
|
|
4519
|
+
let hi = lines.length;
|
|
4520
|
+
let indent = firstContentIndent(lines);
|
|
4521
|
+
for (let i = 0; i < chapterPath.length; i += 2) {
|
|
4522
|
+
const idx = Number(chapterPath[i + 1]);
|
|
4523
|
+
const kids = findKeyLine(lines, lo, hi, indent, "children");
|
|
4524
|
+
if (kids < 0) throw new Error(`no 'children:' at indent ${indent}`);
|
|
4525
|
+
const items = seqItems(lines, kids + 1, hi, indent);
|
|
4526
|
+
if (!(idx >= 0 && idx < items.length)) throw new Error(`children[${idx}] out of range (${items.length})`);
|
|
4527
|
+
hi = idx + 1 < items.length ? items[idx + 1] : seqEnd(lines, kids + 1, hi, indent);
|
|
4528
|
+
lo = items[idx] + 1;
|
|
4529
|
+
indent += 2;
|
|
4530
|
+
}
|
|
4531
|
+
const keyLine = findKeyLine(lines, lo, hi, indent, key);
|
|
4532
|
+
if (keyLine < 0) {
|
|
4533
|
+
const end = trimBack(lines, lo - 1, hi);
|
|
4534
|
+
lines.splice(end, 0, `${" ".repeat(indent)}${key}:`, ...renderItems(indent));
|
|
4535
|
+
} else {
|
|
4536
|
+
const end = seqEnd(lines, keyLine + 1, hi, indent);
|
|
4537
|
+
lines.splice(end, 0, ...renderItems(indent));
|
|
4538
|
+
}
|
|
4539
|
+
return lines.join("\n");
|
|
4540
|
+
}
|
|
4541
|
+
function firstContentIndent(lines) {
|
|
4542
|
+
for (const l of lines) if (isContentLine(l)) return indentOf(l);
|
|
4543
|
+
return 0;
|
|
4544
|
+
}
|
|
4545
|
+
function findKeyLine(lines, lo, hi, indent, key) {
|
|
4546
|
+
for (let i = lo; i < hi; i++) {
|
|
4547
|
+
if (!isContentLine(lines[i])) continue;
|
|
4548
|
+
const ind = indentOf(lines[i]);
|
|
4549
|
+
if (ind < indent) return -1;
|
|
4550
|
+
if (ind !== indent) continue;
|
|
4551
|
+
const t2 = lines[i].trim();
|
|
4552
|
+
if (t2 === `${key}:` || t2.startsWith(`${key}:`)) return i;
|
|
4553
|
+
}
|
|
4554
|
+
return -1;
|
|
4555
|
+
}
|
|
4556
|
+
function seqItems(lines, from, hi, indent) {
|
|
4557
|
+
const out = [];
|
|
4558
|
+
for (let i = from; i < hi; i++) {
|
|
4559
|
+
if (!isContentLine(lines[i])) continue;
|
|
4560
|
+
const ind = indentOf(lines[i]);
|
|
4561
|
+
if (ind < indent) break;
|
|
4562
|
+
if (ind !== indent) continue;
|
|
4563
|
+
const t2 = lines[i].trim();
|
|
4564
|
+
if (t2 === "-" || t2.startsWith("- ")) out.push(i);
|
|
4565
|
+
else break;
|
|
4566
|
+
}
|
|
4567
|
+
return out;
|
|
4568
|
+
}
|
|
4569
|
+
function seqEnd(lines, from, hi, indent) {
|
|
4570
|
+
let last = from;
|
|
4571
|
+
for (let i = from; i < hi; i++) {
|
|
4572
|
+
if (!isContentLine(lines[i])) continue;
|
|
4573
|
+
const ind = indentOf(lines[i]);
|
|
4574
|
+
if (ind < indent) return trimBack(lines, last, i);
|
|
4575
|
+
if (ind === indent) {
|
|
4576
|
+
const t2 = lines[i].trim();
|
|
4577
|
+
if (t2 === "-" || t2.startsWith("- ")) {
|
|
4578
|
+
last = i;
|
|
4579
|
+
continue;
|
|
4580
|
+
}
|
|
4581
|
+
return trimBack(lines, last, i);
|
|
4582
|
+
}
|
|
4583
|
+
last = i;
|
|
4584
|
+
}
|
|
4585
|
+
return trimBack(lines, last, hi);
|
|
4586
|
+
}
|
|
4587
|
+
function trimBack(lines, lastItemLine, end) {
|
|
4588
|
+
let e2 = end;
|
|
4589
|
+
while (e2 > lastItemLine + 1 && !isContentLine(lines[e2 - 1])) e2--;
|
|
4590
|
+
return e2;
|
|
4591
|
+
}
|
|
4592
|
+
function readBody(req) {
|
|
4593
|
+
return new Promise((resolve3, reject) => {
|
|
4594
|
+
const chunks = [];
|
|
4595
|
+
req.on("data", (c) => chunks.push(c));
|
|
4596
|
+
req.on("end", () => {
|
|
4597
|
+
try {
|
|
4598
|
+
resolve3(JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"));
|
|
4599
|
+
} catch (e2) {
|
|
4600
|
+
reject(e2);
|
|
4601
|
+
}
|
|
4602
|
+
});
|
|
4603
|
+
req.on("error", reject);
|
|
4604
|
+
});
|
|
4605
|
+
}
|
|
4606
|
+
function documentRootSegs(s, segs) {
|
|
4607
|
+
for (let i = segs.length; i >= 0; i--) {
|
|
4608
|
+
const anc = segs.slice(0, i);
|
|
4609
|
+
if (s.node(storePath(anc))?.meta?.documentRoot) return anc;
|
|
4610
|
+
}
|
|
4611
|
+
return [];
|
|
4612
|
+
}
|
|
4613
|
+
function documentPath(s, segs) {
|
|
4614
|
+
return segsToStr(documentRootSegs(s, segs));
|
|
4615
|
+
}
|
|
4616
|
+
function titleOf(s, p) {
|
|
4617
|
+
const titlePath = (p === ":" ? "" : p) + ":title";
|
|
4618
|
+
const t2 = s.node(titlePath);
|
|
4619
|
+
if (t2 && t2.type === "scalar" && !s.hasChildren(titlePath) && t2.value != null) return String(t2.value);
|
|
4620
|
+
return null;
|
|
4621
|
+
}
|
|
4622
|
+
var PATH_TOKEN = /\[\d+\]|[^:\[\]]+/g;
|
|
4623
|
+
function segsToStr(segs) {
|
|
4624
|
+
return segs.map((seg) => typeof seg === "number" ? `[${seg}]` : `:${encodeURIComponent(seg)}`).join("") || ":";
|
|
4625
|
+
}
|
|
4626
|
+
function strToSegs(str) {
|
|
4627
|
+
const out = [];
|
|
4628
|
+
for (const tok of str.match(PATH_TOKEN) || []) out.push(/^\[\d+\]$/.test(tok) ? Number(tok.slice(1, -1)) : safeDecode(tok));
|
|
4629
|
+
return out;
|
|
4630
|
+
}
|
|
4631
|
+
function storePath(segs) {
|
|
4632
|
+
return segs.map((seg) => typeof seg === "number" ? `[${seg}]` : `:${seg}`).join("") || ":";
|
|
4633
|
+
}
|
|
4634
|
+
function storePathToSegs(p) {
|
|
4635
|
+
const out = [];
|
|
4636
|
+
for (const tok of p.match(PATH_TOKEN) || []) out.push(/^\[\d+\]$/.test(tok) ? Number(tok.slice(1, -1)) : tok);
|
|
4637
|
+
return out;
|
|
4638
|
+
}
|
|
4639
|
+
function safeDecode(s) {
|
|
4640
|
+
try {
|
|
4641
|
+
return decodeURIComponent(s);
|
|
4642
|
+
} catch {
|
|
4643
|
+
return s;
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
var EXT_CT = {
|
|
4647
|
+
".png": "image/png",
|
|
4648
|
+
".jpg": "image/jpeg",
|
|
4649
|
+
".jpeg": "image/jpeg",
|
|
4650
|
+
".gif": "image/gif",
|
|
4651
|
+
".webp": "image/webp",
|
|
4652
|
+
".svg": "image/svg+xml",
|
|
4653
|
+
".bmp": "image/bmp",
|
|
4654
|
+
".ico": "image/x-icon",
|
|
4655
|
+
".pdf": "application/pdf",
|
|
4656
|
+
".tiff": "image/tiff",
|
|
4657
|
+
".tif": "image/tiff",
|
|
4658
|
+
".html": "text/html",
|
|
4659
|
+
".md": "text/markdown",
|
|
4660
|
+
".csv": "text/csv",
|
|
4661
|
+
".epub": "application/epub+zip"
|
|
4662
|
+
};
|
|
4663
|
+
function formatFromExt(file) {
|
|
4664
|
+
return EXT_CT[path7.extname(file).toLowerCase()] ?? null;
|
|
4665
|
+
}
|
|
4666
|
+
function parseDepth(raw) {
|
|
4667
|
+
if (raw == null || raw === "") return null;
|
|
4668
|
+
const n = Number(raw);
|
|
4669
|
+
return Number.isInteger(n) && n >= 0 ? n : null;
|
|
4670
|
+
}
|
|
4671
|
+
function sendJson(res, status, body) {
|
|
4672
|
+
res.statusCode = status;
|
|
4673
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
4674
|
+
res.end(JSON.stringify(body, null, 2));
|
|
4675
|
+
}
|
|
4676
|
+
function notFound(res, url) {
|
|
4677
|
+
sendJson(res, 404, { error: `no such node/endpoint: ${url.pathname}?${url.searchParams}` });
|
|
4678
|
+
}
|
|
4679
|
+
export {
|
|
4680
|
+
createHandlers
|
|
4681
|
+
};
|