@x18n/builtins 0.0.2 → 0.0.3
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/dist/checks.d.mts +19 -0
- package/dist/checks.mjs +329 -0
- package/dist/extractors.d.mts +51 -0
- package/dist/extractors.mjs +269 -0
- package/dist/finders-CqTGh4OJ.mjs +197 -0
- package/dist/finders.d.mts +31 -0
- package/dist/finders.mjs +3 -0
- package/dist/formats-BAogSrvw.mjs +1331 -0
- package/dist/formats.d.mts +34 -0
- package/dist/formats.mjs +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +38 -0
- package/dist/manifests.d.mts +10 -0
- package/dist/manifests.mjs +49 -0
- package/dist/placeholders.d.mts +27 -0
- package/dist/placeholders.mjs +131 -0
- package/dist/presets-DNnE_2MQ.mjs +1124 -0
- package/dist/presets.d.mts +58 -0
- package/dist/presets.mjs +1 -0
- package/dist/regex-B9TBGgXC.mjs +97 -0
- package/dist/reporters.d.mts +14 -0
- package/dist/reporters.mjs +203 -0
- package/dist/scanners-B27IEH4_.mjs +119 -0
- package/dist/scanners.d.mts +49 -0
- package/dist/scanners.mjs +4 -0
- package/dist/sfc-blocks-BwJwCUka.mjs +161 -0
- package/dist/vue-5GFR5hft.mjs +727 -0
- package/dist/yaml-plugin.d.mts +8 -0
- package/dist/yaml-plugin.mjs +68 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
import { X18nError, applyMessageOps, applyTextEdits, buildTree, diffText, isMessageLeaf, isMessageTree, pathId, walkLeaves } from "@x18n/core";
|
|
2
|
+
import { parseSync } from "oxc-parser";
|
|
3
|
+
import { applyEdits, modify, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
4
|
+
import JSON5 from "json5";
|
|
5
|
+
import { Document, Scalar, isMap, isNode, isScalar, parseDocument, stringify } from "yaml";
|
|
6
|
+
//#region src/formats/shared.ts
|
|
7
|
+
/** Indent of the first indented line, so edits keep the file's style. */
|
|
8
|
+
function detectIndent(text, fallback) {
|
|
9
|
+
if (fallback === "tab") return {
|
|
10
|
+
insertSpaces: false,
|
|
11
|
+
tabSize: 1
|
|
12
|
+
};
|
|
13
|
+
if (typeof fallback === "number") return {
|
|
14
|
+
insertSpaces: true,
|
|
15
|
+
tabSize: fallback
|
|
16
|
+
};
|
|
17
|
+
const match = /^([ \t]+)\S/m.exec(text);
|
|
18
|
+
if (match?.[1]?.startsWith(" ")) return {
|
|
19
|
+
insertSpaces: false,
|
|
20
|
+
tabSize: 1
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
insertSpaces: true,
|
|
24
|
+
tabSize: match?.[1]?.length ?? 2
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function detectEol(text) {
|
|
28
|
+
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
29
|
+
}
|
|
30
|
+
function toLeaf(value) {
|
|
31
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
32
|
+
if (value instanceof Date) return value.toISOString();
|
|
33
|
+
}
|
|
34
|
+
function leafText(value) {
|
|
35
|
+
return value === null ? "" : String(value);
|
|
36
|
+
}
|
|
37
|
+
function splice(text, range, insert) {
|
|
38
|
+
return text.slice(0, range.start) + insert + text.slice(range.end);
|
|
39
|
+
}
|
|
40
|
+
/** Appends `chunk` on a line of its own, in the file's line endings. */
|
|
41
|
+
function appendLines(text, chunk) {
|
|
42
|
+
const eol = detectEol(text);
|
|
43
|
+
return (text === "" || text.endsWith("\n") ? text : text + eol) + chunk.replace(/\n/g, eol);
|
|
44
|
+
}
|
|
45
|
+
function splitLines(text) {
|
|
46
|
+
const out = [];
|
|
47
|
+
let start = 0;
|
|
48
|
+
while (start < text.length) {
|
|
49
|
+
const lf = text.indexOf("\n", start);
|
|
50
|
+
const next = lf === -1 ? text.length : lf + 1;
|
|
51
|
+
const end = lf === -1 ? text.length : lf > start && text[lf - 1] === "\r" ? lf - 1 : lf;
|
|
52
|
+
out.push({
|
|
53
|
+
start,
|
|
54
|
+
end,
|
|
55
|
+
next
|
|
56
|
+
});
|
|
57
|
+
start = next;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** An entry's range widened to swallow the blank lines that separate it, so deleting it leaves one separator. */
|
|
62
|
+
function withBlankLines(text, range) {
|
|
63
|
+
const after = /^(?:[ \t]*\r?\n)*(?:[ \t]*$)?/.exec(text.slice(range.end))[0].length;
|
|
64
|
+
if (range.end + after < text.length) return {
|
|
65
|
+
start: range.start,
|
|
66
|
+
end: range.end + after
|
|
67
|
+
};
|
|
68
|
+
const before = /(?<=\n)(?:[ \t]*\r?\n)+$/.exec(text.slice(0, range.start))?.[0].length ?? 0;
|
|
69
|
+
return {
|
|
70
|
+
start: range.start - before,
|
|
71
|
+
end: text.length
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/formats/ecmascript.ts
|
|
76
|
+
/** Type-only wrappers and `defineX({ ... })` helpers that return their argument. */
|
|
77
|
+
function unwrap(node) {
|
|
78
|
+
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "ParenthesizedExpression") return unwrap(node.expression);
|
|
79
|
+
const argument = node.type === "CallExpression" && node.arguments.length === 1 ? node.arguments[0] : void 0;
|
|
80
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && /^define[A-Z]/.test(node.callee.name) && argument && argument.type !== "SpreadElement") return unwrap(argument);
|
|
81
|
+
return node;
|
|
82
|
+
}
|
|
83
|
+
function findRoot(program, fail) {
|
|
84
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
85
|
+
for (const statement of program.body) {
|
|
86
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
87
|
+
for (const declarator of statement.declarations) if (declarator.id.type === "Identifier" && declarator.init) bindings.set(declarator.id.name, declarator.init);
|
|
88
|
+
}
|
|
89
|
+
let exported;
|
|
90
|
+
for (const statement of program.body) if (statement.type === "ExportDefaultDeclaration" && statement.declaration.type !== "FunctionDeclaration" && statement.declaration.type !== "ClassDeclaration" && statement.declaration.type !== "TSInterfaceDeclaration") exported = statement.declaration;
|
|
91
|
+
else if (statement.type === "ExpressionStatement" && statement.expression.type === "AssignmentExpression" && isModuleExports(statement.expression.left)) exported = statement.expression.right;
|
|
92
|
+
else if (statement.type === "ExportDefaultDeclaration") fail("X18N2004", statement, "the default export is not an object literal");
|
|
93
|
+
if (!exported) return program.body.length === 0 ? void 0 : fail("X18N2004", program, "no `export default { ... }` or `module.exports = { ... }`");
|
|
94
|
+
let root = unwrap(exported);
|
|
95
|
+
if (root.type === "Identifier" && bindings.has(root.name)) root = unwrap(bindings.get(root.name));
|
|
96
|
+
if (root.type === "ObjectExpression") return root;
|
|
97
|
+
return fail(root.type === "Literal" || root.type === "ArrayExpression" ? "X18N2003" : "X18N2004", root, "the default export is not an object literal");
|
|
98
|
+
}
|
|
99
|
+
function isModuleExports(node) {
|
|
100
|
+
return node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && node.object.name === "module" && node.property.type === "Identifier" && node.property.name === "exports";
|
|
101
|
+
}
|
|
102
|
+
function readProperty(property, fail) {
|
|
103
|
+
if (property.type === "SpreadElement" || property.kind !== "init" || property.method || property.shorthand) return fail("X18N2004", property, "only `key: value` properties are static");
|
|
104
|
+
const { key, value } = property;
|
|
105
|
+
if (key.type === "Identifier" && !property.computed) return {
|
|
106
|
+
key: key.name,
|
|
107
|
+
range: {
|
|
108
|
+
start: key.start,
|
|
109
|
+
end: key.end
|
|
110
|
+
},
|
|
111
|
+
value: unwrap(value)
|
|
112
|
+
};
|
|
113
|
+
if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) return {
|
|
114
|
+
key: String(key.value),
|
|
115
|
+
range: typeof key.value === "string" ? {
|
|
116
|
+
start: key.start + 1,
|
|
117
|
+
end: key.end - 1
|
|
118
|
+
} : {
|
|
119
|
+
start: key.start,
|
|
120
|
+
end: key.end
|
|
121
|
+
},
|
|
122
|
+
value: unwrap(value)
|
|
123
|
+
};
|
|
124
|
+
return fail("X18N2004", key, "computed keys are not static");
|
|
125
|
+
}
|
|
126
|
+
function readLeaf(node, text) {
|
|
127
|
+
if (node.type === "Literal" && !("regex" in node) && typeof node.value !== "bigint") return {
|
|
128
|
+
value: node.value,
|
|
129
|
+
quote: typeof node.value === "string" ? text[node.start] : void 0
|
|
130
|
+
};
|
|
131
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0 && node.quasis[0]?.value.cooked != null) return {
|
|
132
|
+
value: node.quasis[0].value.cooked,
|
|
133
|
+
quote: "`"
|
|
134
|
+
};
|
|
135
|
+
if (node.type === "UnaryExpression" && node.operator === "-" && node.argument.type === "Literal" && typeof node.argument.value === "number") return {
|
|
136
|
+
value: -node.argument.value,
|
|
137
|
+
quote: void 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function readObject(node, path, module, text, fail) {
|
|
141
|
+
const out = {};
|
|
142
|
+
for (const property of node.properties) {
|
|
143
|
+
const { key, range, value } = readProperty(property, fail);
|
|
144
|
+
const childPath = [...path, key];
|
|
145
|
+
const id = pathId(childPath);
|
|
146
|
+
if (value.type === "ObjectExpression") {
|
|
147
|
+
module.ranges.set(id, {
|
|
148
|
+
key: range,
|
|
149
|
+
value: {
|
|
150
|
+
start: value.start,
|
|
151
|
+
end: value.end
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
out[key] = readObject(value, childPath, module, text, fail);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const leaf = readLeaf(value, text);
|
|
158
|
+
if (!leaf) return fail("X18N2004", value, `${value.type} is not a static value`);
|
|
159
|
+
const quoted = leaf.quote !== void 0;
|
|
160
|
+
module.ranges.set(id, {
|
|
161
|
+
key: range,
|
|
162
|
+
value: quoted ? {
|
|
163
|
+
start: value.start + 1,
|
|
164
|
+
end: value.end - 1
|
|
165
|
+
} : {
|
|
166
|
+
start: value.start,
|
|
167
|
+
end: value.end
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
module.scalars.set(id, {
|
|
171
|
+
range: {
|
|
172
|
+
start: value.start,
|
|
173
|
+
end: value.end
|
|
174
|
+
},
|
|
175
|
+
quote: leaf.quote
|
|
176
|
+
});
|
|
177
|
+
out[key] = leaf.value;
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
function readModule(text, path) {
|
|
182
|
+
const fail = (code, node, message) => {
|
|
183
|
+
throw new X18nError(code, `${path}:${text.slice(0, node.start).split("\n").length}: ${message}`);
|
|
184
|
+
};
|
|
185
|
+
const result = parseSync(path, text, {
|
|
186
|
+
lang: /\.[mc]?ts$/.test(path) ? "ts" : "js",
|
|
187
|
+
sourceType: "module"
|
|
188
|
+
});
|
|
189
|
+
if (result.errors.length > 0) throw new X18nError("X18N2002", `${path}: ${result.errors[0].message}`);
|
|
190
|
+
const root = findRoot(result.program, fail);
|
|
191
|
+
const found = {
|
|
192
|
+
ranges: /* @__PURE__ */ new Map(),
|
|
193
|
+
scalars: /* @__PURE__ */ new Map()
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
root,
|
|
197
|
+
tree: root ? readObject(root, [], found, text, fail) : {},
|
|
198
|
+
...found
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const singleEncodings = {
|
|
202
|
+
"\\": "\\\\",
|
|
203
|
+
"'": "\\'",
|
|
204
|
+
"\n": "\\n",
|
|
205
|
+
"\r": "\\r",
|
|
206
|
+
"\u2028": "\\u2028",
|
|
207
|
+
"\u2029": "\\u2029"
|
|
208
|
+
};
|
|
209
|
+
function literal$1(value, quote) {
|
|
210
|
+
if (typeof value !== "string") return String(value);
|
|
211
|
+
if (quote === "\"") return JSON.stringify(value);
|
|
212
|
+
if (quote === "`") return `\`${value.replace(/\\|`|\$\{/g, (match) => `\\${match}`)}\``;
|
|
213
|
+
return `'${value.replace(/[\\'\n\r\u2028\u2029]/g, (char) => singleEncodings[char])}'`;
|
|
214
|
+
}
|
|
215
|
+
function objectText(tree, unit, depth) {
|
|
216
|
+
const entries = Object.entries(tree);
|
|
217
|
+
if (entries.length === 0) return "{}";
|
|
218
|
+
const pad = unit.repeat(depth + 1);
|
|
219
|
+
return `{\n${entries.map(([key, child]) => {
|
|
220
|
+
const name = /^[a-z_$][\w$]*$/i.test(key) ? key : literal$1(key, "'");
|
|
221
|
+
return `${pad}${name}: ${isMessageLeaf(child) ? literal$1(child, "'") : objectText(child, unit, depth + 1)},\n`;
|
|
222
|
+
}).join("")}${unit.repeat(depth)}}`;
|
|
223
|
+
}
|
|
224
|
+
/** Indent unit: configured, else the file's (first indented line), else 2 spaces. */
|
|
225
|
+
function unitOf$1(text, indent) {
|
|
226
|
+
if (indent !== void 0) return indent === "tab" ? " " : " ".repeat(indent);
|
|
227
|
+
return /^([ \t]+)\S/m.exec(text)?.[1] ?? " ";
|
|
228
|
+
}
|
|
229
|
+
function moduleText(tree, path, unit) {
|
|
230
|
+
return `${path.endsWith(".cjs") ? "module.exports =" : "export default"} ${objectText(tree, unit, 0)}\n`;
|
|
231
|
+
}
|
|
232
|
+
function applyOp$5(text, op, path, unit) {
|
|
233
|
+
const module = readModule(text, path);
|
|
234
|
+
const scalar = module.scalars.get(pathId(op.path));
|
|
235
|
+
if (op.type === "set" && scalar) return splice(text, scalar.range, literal$1(op.value, scalar.quote));
|
|
236
|
+
const tree = applyMessageOps(module.tree, [op]);
|
|
237
|
+
return module.root ? splice(text, module.root, objectText(tree, unit, 0)) : moduleText(tree, path, unit);
|
|
238
|
+
}
|
|
239
|
+
function ecmascript() {
|
|
240
|
+
return {
|
|
241
|
+
name: "x18n:format-js",
|
|
242
|
+
formats: [{
|
|
243
|
+
id: "js",
|
|
244
|
+
extensions: [
|
|
245
|
+
"js",
|
|
246
|
+
"mjs",
|
|
247
|
+
"cjs",
|
|
248
|
+
"ts",
|
|
249
|
+
"mts",
|
|
250
|
+
"cts"
|
|
251
|
+
],
|
|
252
|
+
parse(text, ctx) {
|
|
253
|
+
const { tree, ranges } = readModule(text, ctx.path);
|
|
254
|
+
return {
|
|
255
|
+
tree,
|
|
256
|
+
ranges
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
edit(text, ops, ctx) {
|
|
260
|
+
const unit = unitOf$1(text, ctx.indent);
|
|
261
|
+
let next = text;
|
|
262
|
+
for (const op of ops) next = applyOp$5(next, op, ctx.path, unit);
|
|
263
|
+
return diffText(text, next);
|
|
264
|
+
},
|
|
265
|
+
serialize: (tree, ctx) => moduleText(tree, ctx.path, unitOf$1("", ctx.indent))
|
|
266
|
+
}]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/formats/ftl.ts
|
|
271
|
+
const trimmedEnd = (text, line) => line.start + text.slice(line.start, line.end).trimEnd().length;
|
|
272
|
+
const indentOf$1 = (text, line) => /^[ \t]*/.exec(text.slice(line.start, line.end))[0].length;
|
|
273
|
+
function finishPattern(text, draft) {
|
|
274
|
+
const lines = [...draft.lines];
|
|
275
|
+
while (lines.length > 0 && text.slice(lines.at(-1).start, lines.at(-1).end).trim() === "") lines.pop();
|
|
276
|
+
const indent = Math.min(...lines.filter((l) => text.slice(l.start, l.end).trim() !== "").map((l) => indentOf$1(text, l)));
|
|
277
|
+
const inline = text.slice(draft.inline.start, draft.inline.end).trimEnd();
|
|
278
|
+
const parts = [...inline ? [inline] : [], ...lines.map((l) => text.slice(Math.min(l.start + indent, l.end), trimmedEnd(text, l)))];
|
|
279
|
+
const first = lines[0];
|
|
280
|
+
if (!inline && !first) return {
|
|
281
|
+
value: void 0,
|
|
282
|
+
range: {
|
|
283
|
+
start: draft.after,
|
|
284
|
+
end: draft.after
|
|
285
|
+
},
|
|
286
|
+
source: {
|
|
287
|
+
start: draft.after,
|
|
288
|
+
end: draft.inline.end
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const start = inline ? draft.inline.start : first.start + indent;
|
|
292
|
+
const end = lines.length > 0 ? trimmedEnd(text, lines.at(-1)) : trimmedEnd(text, draft.inline);
|
|
293
|
+
return {
|
|
294
|
+
value: parts.join("\n"),
|
|
295
|
+
range: {
|
|
296
|
+
start,
|
|
297
|
+
end
|
|
298
|
+
},
|
|
299
|
+
source: {
|
|
300
|
+
start: draft.after,
|
|
301
|
+
end
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function read$3(text, path) {
|
|
306
|
+
const messages = [];
|
|
307
|
+
let current;
|
|
308
|
+
let commentStart;
|
|
309
|
+
let blanks = [];
|
|
310
|
+
let lineNumber = 0;
|
|
311
|
+
for (const line of splitLines(text)) {
|
|
312
|
+
lineNumber++;
|
|
313
|
+
const content = text.slice(line.start, line.end);
|
|
314
|
+
if (content.trim() === "") {
|
|
315
|
+
blanks.push(line);
|
|
316
|
+
commentStart = void 0;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (/^[ \t]/.test(content)) {
|
|
320
|
+
if (!current) throw new X18nError("X18N2002", `${path}:${lineNumber}: indented line outside a message`);
|
|
321
|
+
const attribute = /^[ \t]+\.([a-z][\w-]*)[ \t]*=[ \t]*/i.exec(content);
|
|
322
|
+
if (attribute) {
|
|
323
|
+
const nameStart = line.start + attribute[0].indexOf(".") + 1;
|
|
324
|
+
const after = line.start + attribute[0].trimEnd().length;
|
|
325
|
+
current.attributes.push({
|
|
326
|
+
name: attribute[1],
|
|
327
|
+
nameRange: {
|
|
328
|
+
start: nameStart,
|
|
329
|
+
end: nameStart + attribute[1].length
|
|
330
|
+
},
|
|
331
|
+
pattern: {
|
|
332
|
+
after,
|
|
333
|
+
inline: {
|
|
334
|
+
start: line.start + attribute[0].length,
|
|
335
|
+
end: line.end
|
|
336
|
+
},
|
|
337
|
+
lines: []
|
|
338
|
+
},
|
|
339
|
+
lines: {
|
|
340
|
+
start: line.start,
|
|
341
|
+
end: line.next
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
const last = current.attributes.at(-1);
|
|
346
|
+
(last ?? current).pattern.lines.push(...blanks, line);
|
|
347
|
+
if (last) last.lines.end = line.next;
|
|
348
|
+
}
|
|
349
|
+
current.block.end = line.next;
|
|
350
|
+
blanks = [];
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
blanks = [];
|
|
354
|
+
current = void 0;
|
|
355
|
+
if (content.startsWith("#")) {
|
|
356
|
+
commentStart = /^#(?: |$)/.test(content) ? commentStart ?? line.start : void 0;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const message = /^(-?[a-z][\w-]*)[ \t]*=[ \t]*/i.exec(content);
|
|
360
|
+
if (!message) throw new X18nError("X18N2002", `${path}:${lineNumber}: expected a message, a term or a comment`);
|
|
361
|
+
current = {
|
|
362
|
+
id: message[1],
|
|
363
|
+
idRange: {
|
|
364
|
+
start: line.start,
|
|
365
|
+
end: line.start + message[1].length
|
|
366
|
+
},
|
|
367
|
+
block: {
|
|
368
|
+
start: commentStart ?? line.start,
|
|
369
|
+
end: line.next
|
|
370
|
+
},
|
|
371
|
+
pattern: {
|
|
372
|
+
after: line.start + message[0].trimEnd().length,
|
|
373
|
+
inline: {
|
|
374
|
+
start: line.start + message[0].length,
|
|
375
|
+
end: line.end
|
|
376
|
+
},
|
|
377
|
+
lines: []
|
|
378
|
+
},
|
|
379
|
+
attributes: []
|
|
380
|
+
};
|
|
381
|
+
commentStart = void 0;
|
|
382
|
+
messages.push(current);
|
|
383
|
+
}
|
|
384
|
+
return messages;
|
|
385
|
+
}
|
|
386
|
+
function encode$2(value, indent, eol) {
|
|
387
|
+
if (value === "") return " { \"\" }";
|
|
388
|
+
const lines = value.split("\n");
|
|
389
|
+
if (lines.length === 1) return ` ${value}`;
|
|
390
|
+
return lines.map((line) => line === "" ? eol : `${eol}${" ".repeat(indent)}${line}`).join("");
|
|
391
|
+
}
|
|
392
|
+
function splitKey(key) {
|
|
393
|
+
const dot = key.indexOf(".");
|
|
394
|
+
return dot === -1 ? {
|
|
395
|
+
id: key,
|
|
396
|
+
attribute: void 0
|
|
397
|
+
} : {
|
|
398
|
+
id: key.slice(0, dot),
|
|
399
|
+
attribute: key.slice(dot + 1)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function setValue(text, key, value, messages) {
|
|
403
|
+
const eol = detectEol(text);
|
|
404
|
+
const { id, attribute } = splitKey(key);
|
|
405
|
+
const message = messages.findLast((m) => m.id === id);
|
|
406
|
+
if (!message) return appendLines(text, attribute ? `${id} =\n .${attribute} =${encode$2(value, 8, "\n")}\n` : `${id} =${encode$2(value, 4, "\n")}\n`);
|
|
407
|
+
if (!attribute) return splice(text, finishPattern(text, message.pattern).source, encode$2(value, 4, eol));
|
|
408
|
+
const existing = message.attributes.findLast((a) => a.name === attribute);
|
|
409
|
+
if (existing) return splice(text, finishPattern(text, existing.pattern).source, encode$2(value, indentOf$1(text, existing.lines) + 4, eol));
|
|
410
|
+
const end = message.block.end;
|
|
411
|
+
const lead = end === text.length && !text.endsWith("\n") ? eol : "";
|
|
412
|
+
return splice(text, {
|
|
413
|
+
start: end,
|
|
414
|
+
end
|
|
415
|
+
}, `${lead} .${attribute} =${encode$2(value, 8, eol)}${eol}`);
|
|
416
|
+
}
|
|
417
|
+
function remove(text, key, messages) {
|
|
418
|
+
const { id, attribute } = splitKey(key);
|
|
419
|
+
const message = messages.findLast((m) => m.id === id);
|
|
420
|
+
if (!message) return text;
|
|
421
|
+
const pattern = finishPattern(text, message.pattern);
|
|
422
|
+
if (!attribute) {
|
|
423
|
+
if (pattern.value === void 0) return text;
|
|
424
|
+
return message.attributes.length > 0 ? splice(text, pattern.source, "") : splice(text, withBlankLines(text, message.block), "");
|
|
425
|
+
}
|
|
426
|
+
const existing = message.attributes.findLast((a) => a.name === attribute);
|
|
427
|
+
if (!existing) return text;
|
|
428
|
+
if (pattern.value === void 0 && message.attributes.length === 1) return splice(text, withBlankLines(text, message.block), "");
|
|
429
|
+
return splice(text, existing.lines, "");
|
|
430
|
+
}
|
|
431
|
+
function applyOp$4(text, op, path) {
|
|
432
|
+
const key = op.path.join(".");
|
|
433
|
+
const messages = read$3(text, path);
|
|
434
|
+
return op.type === "set" ? setValue(text, key, leafText(op.value), messages) : remove(text, key, messages);
|
|
435
|
+
}
|
|
436
|
+
function serialize$1(tree) {
|
|
437
|
+
const messages = /* @__PURE__ */ new Map();
|
|
438
|
+
for (const leaf of walkLeaves(tree)) {
|
|
439
|
+
const { id, attribute } = splitKey(leaf.path.join("."));
|
|
440
|
+
const message = messages.get(id) ?? {
|
|
441
|
+
value: void 0,
|
|
442
|
+
attributes: []
|
|
443
|
+
};
|
|
444
|
+
messages.set(id, message);
|
|
445
|
+
if (attribute) message.attributes.push(` .${attribute} =${encode$2(leafText(leaf.value), 8, "\n")}\n`);
|
|
446
|
+
else message.value = leafText(leaf.value);
|
|
447
|
+
}
|
|
448
|
+
return [...messages].map(([id, m]) => `${id} =${m.value === void 0 ? "" : encode$2(m.value, 4, "\n")}\n${m.attributes.join("")}`).join("");
|
|
449
|
+
}
|
|
450
|
+
function ftl() {
|
|
451
|
+
return {
|
|
452
|
+
name: "x18n:format-ftl",
|
|
453
|
+
formats: [{
|
|
454
|
+
id: "ftl",
|
|
455
|
+
extensions: ["ftl"],
|
|
456
|
+
parse(text, ctx) {
|
|
457
|
+
const tree = {};
|
|
458
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
459
|
+
for (const message of read$3(text, ctx.path)) {
|
|
460
|
+
const pattern = finishPattern(text, message.pattern);
|
|
461
|
+
if (pattern.value !== void 0) {
|
|
462
|
+
tree[message.id] = pattern.value;
|
|
463
|
+
ranges.set(pathId([message.id]), {
|
|
464
|
+
key: message.idRange,
|
|
465
|
+
value: pattern.range
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
for (const attribute of message.attributes) {
|
|
469
|
+
const key = `${message.id}.${attribute.name}`;
|
|
470
|
+
const value = finishPattern(text, attribute.pattern);
|
|
471
|
+
tree[key] = value.value ?? "";
|
|
472
|
+
ranges.set(pathId([key]), {
|
|
473
|
+
key: attribute.nameRange,
|
|
474
|
+
value: value.range
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
tree,
|
|
480
|
+
ranges
|
|
481
|
+
};
|
|
482
|
+
},
|
|
483
|
+
edit(text, ops, ctx) {
|
|
484
|
+
let next = text;
|
|
485
|
+
for (const op of ops) next = applyOp$4(next, op, ctx.path);
|
|
486
|
+
return diffText(text, next);
|
|
487
|
+
},
|
|
488
|
+
serialize: serialize$1
|
|
489
|
+
}]
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region src/formats/ini.ts
|
|
494
|
+
/** Reads the value after `=`; `start` is its first non-blank offset. */
|
|
495
|
+
function readValue(text, start, end, fail) {
|
|
496
|
+
const quote = text[start];
|
|
497
|
+
if (quote === "\"" || quote === "'") {
|
|
498
|
+
const close = quote === "\"" ? /^"(?:[^"\\]|\\.)*"/.exec(text.slice(start, end)) : /^'[^']*'/.exec(text.slice(start, end));
|
|
499
|
+
if (!close || !/^\s*(?:[;#].*)?$/.test(text.slice(start + close[0].length, end))) fail("unterminated or malformed quoted value");
|
|
500
|
+
const raw = close[0];
|
|
501
|
+
let value = raw.slice(1, -1);
|
|
502
|
+
if (quote === "\"") try {
|
|
503
|
+
value = JSON.parse(raw);
|
|
504
|
+
} catch {
|
|
505
|
+
fail(`invalid escape in ${raw}`);
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
value,
|
|
509
|
+
source: {
|
|
510
|
+
start,
|
|
511
|
+
end: start + raw.length
|
|
512
|
+
},
|
|
513
|
+
inner: {
|
|
514
|
+
start: start + 1,
|
|
515
|
+
end: start + raw.length - 1
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const comment = /\s[;#]/.exec(text.slice(start, end));
|
|
520
|
+
const valueEnd = start + text.slice(start, comment ? start + comment.index : end).trimEnd().length;
|
|
521
|
+
const range = {
|
|
522
|
+
start,
|
|
523
|
+
end: valueEnd
|
|
524
|
+
};
|
|
525
|
+
return {
|
|
526
|
+
value: text.slice(start, valueEnd),
|
|
527
|
+
source: range,
|
|
528
|
+
inner: range
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function read$2(text, path) {
|
|
532
|
+
const entries = [];
|
|
533
|
+
const sections = [];
|
|
534
|
+
let section = "";
|
|
535
|
+
let lineNumber = 0;
|
|
536
|
+
const fail = (message) => {
|
|
537
|
+
throw new X18nError("X18N2002", `${path}:${lineNumber}: ${message}`);
|
|
538
|
+
};
|
|
539
|
+
for (const line of splitLines(text)) {
|
|
540
|
+
lineNumber++;
|
|
541
|
+
const content = text.slice(line.start, line.end);
|
|
542
|
+
const trimmed = content.trim();
|
|
543
|
+
if (trimmed === "" || trimmed.startsWith(";") || trimmed.startsWith("#")) continue;
|
|
544
|
+
const header = /^\s*\[([^\]]+)\]\s*(?:[;#].*)?$/.exec(content);
|
|
545
|
+
if (header) {
|
|
546
|
+
section = header[1].trim();
|
|
547
|
+
sections.push({
|
|
548
|
+
name: section,
|
|
549
|
+
line
|
|
550
|
+
});
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const eq = content.indexOf("=");
|
|
554
|
+
if (eq === -1) fail(`expected "key = value", got "${trimmed.slice(0, 20)}"`);
|
|
555
|
+
const key = content.slice(0, eq).trim();
|
|
556
|
+
if (key === "") fail("empty key");
|
|
557
|
+
const keyStart = line.start + content.indexOf(key);
|
|
558
|
+
const { value, source, inner } = readValue(text, line.start + eq + 1 + /^\s*/.exec(content.slice(eq + 1))[0].length, line.end, fail);
|
|
559
|
+
entries.push({
|
|
560
|
+
path: [...section ? section.split(".") : [], key],
|
|
561
|
+
section,
|
|
562
|
+
value,
|
|
563
|
+
range: {
|
|
564
|
+
key: {
|
|
565
|
+
start: keyStart,
|
|
566
|
+
end: keyStart + key.length
|
|
567
|
+
},
|
|
568
|
+
value: inner
|
|
569
|
+
},
|
|
570
|
+
source,
|
|
571
|
+
line
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
entries,
|
|
576
|
+
sections
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function encode$1(value, quote) {
|
|
580
|
+
if (quote === "'" && !/['\r\n]/.test(value)) return `'${value}'`;
|
|
581
|
+
return quote === "\"" || quote === "'" || /^\s|\s$|[\r\n"';#]|^\[/.test(value) ? JSON.stringify(value) : value;
|
|
582
|
+
}
|
|
583
|
+
function insertLine(text, at, line) {
|
|
584
|
+
const eol = detectEol(text);
|
|
585
|
+
const lead = at === text.length && text !== "" && !text.endsWith("\n") ? eol : "";
|
|
586
|
+
return splice(text, {
|
|
587
|
+
start: at,
|
|
588
|
+
end: at
|
|
589
|
+
}, `${lead}${line}${eol}`);
|
|
590
|
+
}
|
|
591
|
+
function applyOp$3(text, op, path) {
|
|
592
|
+
const { entries, sections } = read$2(text, path);
|
|
593
|
+
const id = pathId(op.path);
|
|
594
|
+
const entry = entries.findLast((e) => pathId(e.path) === id);
|
|
595
|
+
if (op.type === "delete") return entry ? splice(text, {
|
|
596
|
+
start: entry.line.start,
|
|
597
|
+
end: entry.line.next
|
|
598
|
+
}, "") : text;
|
|
599
|
+
const value = leafText(op.value);
|
|
600
|
+
if (entry) return splice(text, entry.source, encode$1(value, text[entry.source.start]));
|
|
601
|
+
const section = op.path.slice(0, -1).join(".");
|
|
602
|
+
const last = entries.findLast((e) => e.source.end > e.source.start);
|
|
603
|
+
const line = `${op.path.at(-1)}${last ? text.slice(last.range.key.end, last.source.start) : " = "}${encode$1(value, void 0)}`;
|
|
604
|
+
const anchor = entries.findLast((e) => e.section === section)?.line ?? sections.find((s) => s.name === section)?.line;
|
|
605
|
+
if (anchor) return insertLine(text, anchor.next, line);
|
|
606
|
+
if (section === "") return insertLine(text, sections[0]?.line.start ?? text.length, line);
|
|
607
|
+
return appendLines(text, `${text.trim() === "" ? "" : "\n"}[${section}]\n${line}\n`);
|
|
608
|
+
}
|
|
609
|
+
function serialize(tree) {
|
|
610
|
+
const blocks = [];
|
|
611
|
+
const visit = (node, path) => {
|
|
612
|
+
const children = Object.entries(node);
|
|
613
|
+
const leaves = children.filter((child) => isMessageLeaf(child[1]));
|
|
614
|
+
if (leaves.length > 0) blocks.push(`${path.length > 0 ? `[${path.join(".")}]\n` : ""}${leaves.map(([key, value]) => `${key} = ${encode$1(leafText(value), void 0)}\n`).join("")}`);
|
|
615
|
+
for (const [key, child] of children) if (!isMessageLeaf(child)) visit(child, [...path, key]);
|
|
616
|
+
};
|
|
617
|
+
visit(tree, []);
|
|
618
|
+
return blocks.join("\n");
|
|
619
|
+
}
|
|
620
|
+
function ini() {
|
|
621
|
+
return {
|
|
622
|
+
name: "x18n:format-ini",
|
|
623
|
+
formats: [{
|
|
624
|
+
id: "ini",
|
|
625
|
+
extensions: ["ini"],
|
|
626
|
+
parse(text, ctx) {
|
|
627
|
+
const { entries } = read$2(text, ctx.path);
|
|
628
|
+
const ranges = new Map(entries.map((e) => [pathId(e.path), e.range]));
|
|
629
|
+
return {
|
|
630
|
+
tree: buildTree(entries),
|
|
631
|
+
ranges
|
|
632
|
+
};
|
|
633
|
+
},
|
|
634
|
+
edit(text, ops, ctx) {
|
|
635
|
+
let next = text;
|
|
636
|
+
for (const op of ops) next = applyOp$3(next, op, ctx.path);
|
|
637
|
+
return diffText(text, next);
|
|
638
|
+
},
|
|
639
|
+
serialize
|
|
640
|
+
}]
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/formats/json.ts
|
|
645
|
+
function readTree$1(node, path, ranges) {
|
|
646
|
+
const out = {};
|
|
647
|
+
for (const property of node.children ?? []) {
|
|
648
|
+
const [keyNode, valueNode] = property.children ?? [];
|
|
649
|
+
if (!keyNode || !valueNode || typeof keyNode.value !== "string") continue;
|
|
650
|
+
const key = keyNode.value;
|
|
651
|
+
const childPath = [...path, key];
|
|
652
|
+
ranges.set(pathId(childPath), {
|
|
653
|
+
key: {
|
|
654
|
+
start: keyNode.offset + 1,
|
|
655
|
+
end: keyNode.offset + keyNode.length - 1
|
|
656
|
+
},
|
|
657
|
+
value: {
|
|
658
|
+
start: valueNode.offset,
|
|
659
|
+
end: valueNode.offset + valueNode.length
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
if (valueNode.type === "object") out[key] = readTree$1(valueNode, childPath, ranges);
|
|
663
|
+
else if (valueNode.type === "string" || valueNode.type === "number" || valueNode.type === "boolean" || valueNode.type === "null") out[key] = valueNode.value;
|
|
664
|
+
}
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
function createJsonFormat(id, extensions, allowComments, defaultIndent) {
|
|
668
|
+
return {
|
|
669
|
+
id,
|
|
670
|
+
extensions,
|
|
671
|
+
parse(text, ctx) {
|
|
672
|
+
const errors = [];
|
|
673
|
+
const root = parseTree(text, errors, {
|
|
674
|
+
allowTrailingComma: true,
|
|
675
|
+
disallowComments: !allowComments
|
|
676
|
+
});
|
|
677
|
+
if (errors.length > 0) {
|
|
678
|
+
const first = errors[0];
|
|
679
|
+
throw new X18nError("X18N2002", `${ctx.path}: ${printParseErrorCode(first.error)} at offset ${first.offset}`);
|
|
680
|
+
}
|
|
681
|
+
if (!root || root.type !== "object") {
|
|
682
|
+
if (text.trim() === "") return {
|
|
683
|
+
tree: {},
|
|
684
|
+
ranges: /* @__PURE__ */ new Map()
|
|
685
|
+
};
|
|
686
|
+
throw new X18nError("X18N2003", ctx.path);
|
|
687
|
+
}
|
|
688
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
689
|
+
return {
|
|
690
|
+
tree: readTree$1(root, [], ranges),
|
|
691
|
+
ranges
|
|
692
|
+
};
|
|
693
|
+
},
|
|
694
|
+
edit(text, ops, ctx) {
|
|
695
|
+
const formattingOptions = {
|
|
696
|
+
...detectIndent(text, ctx.indent ?? defaultIndent),
|
|
697
|
+
eol: detectEol(text)
|
|
698
|
+
};
|
|
699
|
+
let next = text.trim() === "" ? "{}\n" : text;
|
|
700
|
+
for (const op of ops) next = applyEdits(next, modify(next, [...op.path], op.type === "set" ? op.value : void 0, { formattingOptions }));
|
|
701
|
+
return diffText(text, next);
|
|
702
|
+
},
|
|
703
|
+
serialize(tree, ctx) {
|
|
704
|
+
const indent = ctx.indent ?? defaultIndent ?? 2;
|
|
705
|
+
return `${JSON.stringify(tree, null, indent === "tab" ? " " : indent)}\n`;
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function json(options = {}) {
|
|
710
|
+
return {
|
|
711
|
+
name: "x18n:format-json",
|
|
712
|
+
formats: [createJsonFormat("json", ["json"], true, options.indent)]
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function jsonc(options = {}) {
|
|
716
|
+
return {
|
|
717
|
+
name: "x18n:format-jsonc",
|
|
718
|
+
formats: [createJsonFormat("jsonc", ["jsonc"], true, options.indent)]
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region src/formats/json5.ts
|
|
723
|
+
function json5() {
|
|
724
|
+
return {
|
|
725
|
+
name: "x18n:format-json5",
|
|
726
|
+
formats: [{
|
|
727
|
+
id: "json5",
|
|
728
|
+
extensions: ["json5"],
|
|
729
|
+
parse(text, ctx) {
|
|
730
|
+
if (text.trim() === "") return { tree: {} };
|
|
731
|
+
let data;
|
|
732
|
+
try {
|
|
733
|
+
data = JSON5.parse(text);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
throw new X18nError("X18N2002", `${ctx.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
736
|
+
}
|
|
737
|
+
if (!isMessageTree(data)) throw new X18nError("X18N2003", ctx.path);
|
|
738
|
+
return { tree: data };
|
|
739
|
+
},
|
|
740
|
+
serialize: (tree, ctx) => `${JSON5.stringify(tree, null, ctx.indent === "tab" ? " " : ctx.indent ?? 2)}\n`
|
|
741
|
+
}]
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region src/formats/php.ts
|
|
746
|
+
const doubleEscapes = {
|
|
747
|
+
"n": "\n",
|
|
748
|
+
"t": " ",
|
|
749
|
+
"r": "\r",
|
|
750
|
+
"v": "\v",
|
|
751
|
+
"e": "\x1B",
|
|
752
|
+
"f": "\f",
|
|
753
|
+
"\\": "\\",
|
|
754
|
+
"$": "$",
|
|
755
|
+
"\"": "\""
|
|
756
|
+
};
|
|
757
|
+
function decodeDouble(raw, fail) {
|
|
758
|
+
if (/(?:^|[^\\])(?:\\\\)*(?:\$[a-z_{]|\{\$)/i.test(raw)) fail("X18N2004", "variable interpolation in a string");
|
|
759
|
+
return raw.replace(/\\(u\{[0-9a-f]+\}|x[0-9a-f]{1,2}|[0-7]{1,3}|[\s\S])/gi, (match, escape) => {
|
|
760
|
+
if (/^u\{/i.test(escape)) return String.fromCodePoint(Number.parseInt(escape.slice(2, -1), 16));
|
|
761
|
+
if (/^x[0-9a-f]/i.test(escape)) return String.fromCharCode(Number.parseInt(escape.slice(1), 16));
|
|
762
|
+
if (/^[0-7]/.test(escape)) return String.fromCharCode(Number.parseInt(escape, 8));
|
|
763
|
+
return doubleEscapes[escape] ?? match;
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
function parseFile(text, path) {
|
|
767
|
+
let pos = 0;
|
|
768
|
+
const fail = (code, message) => {
|
|
769
|
+
const line = text.slice(0, pos).split("\n").length;
|
|
770
|
+
throw new X18nError(code, `${path}:${line}: ${message}`);
|
|
771
|
+
};
|
|
772
|
+
const skip = () => {
|
|
773
|
+
for (;;) {
|
|
774
|
+
const rest = text.slice(pos);
|
|
775
|
+
const blank = /^(?:\s+|\/\/[^\n]*|#(?!\[)[^\n]*|\/\*[\s\S]*?\*\/)/.exec(rest);
|
|
776
|
+
if (!blank) return;
|
|
777
|
+
pos += blank[0].length;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
const eat = (pattern) => {
|
|
781
|
+
skip();
|
|
782
|
+
const match = pattern.exec(text.slice(pos));
|
|
783
|
+
if (match) pos += match[0].length;
|
|
784
|
+
return match?.[0];
|
|
785
|
+
};
|
|
786
|
+
const parseString = (quote) => {
|
|
787
|
+
const start = pos;
|
|
788
|
+
const match = (quote === "'" ? /^'(?:[^'\\]|\\[\s\S])*'/ : /^"(?:[^"\\]|\\[\s\S])*"/).exec(text.slice(pos));
|
|
789
|
+
if (!match) return fail("X18N2002", "unterminated string");
|
|
790
|
+
pos += match[0].length;
|
|
791
|
+
const raw = match[0].slice(1, -1);
|
|
792
|
+
const value = quote === "'" ? raw.replace(/\\([\\'])/g, "$1") : decodeDouble(raw, fail);
|
|
793
|
+
return {
|
|
794
|
+
kind: "scalar",
|
|
795
|
+
range: {
|
|
796
|
+
start,
|
|
797
|
+
end: pos
|
|
798
|
+
},
|
|
799
|
+
value,
|
|
800
|
+
quote
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
const parseArray = (start, close) => {
|
|
804
|
+
const closing = close === "]" ? /^\]/ : /^\)/;
|
|
805
|
+
const items = [];
|
|
806
|
+
while (!eat(closing)) {
|
|
807
|
+
const key = parseValue();
|
|
808
|
+
if (!eat(/^=>/)) return fail("X18N2003", "array item without a key");
|
|
809
|
+
if (key.kind !== "scalar" || typeof key.value !== "string" && typeof key.value !== "number") return fail("X18N2004", "array keys must be strings or integers");
|
|
810
|
+
items.push({
|
|
811
|
+
key: String(key.value),
|
|
812
|
+
keyRange: key.quote ? {
|
|
813
|
+
start: key.range.start + 1,
|
|
814
|
+
end: key.range.end - 1
|
|
815
|
+
} : key.range,
|
|
816
|
+
value: parseValue()
|
|
817
|
+
});
|
|
818
|
+
if (eat(/^,/)) continue;
|
|
819
|
+
skip();
|
|
820
|
+
if (text[pos] !== close) return fail("X18N2004", `expected "," or "${close}"; expressions are not static`);
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
kind: "array",
|
|
824
|
+
range: {
|
|
825
|
+
start,
|
|
826
|
+
end: pos
|
|
827
|
+
},
|
|
828
|
+
items
|
|
829
|
+
};
|
|
830
|
+
};
|
|
831
|
+
function parseValue() {
|
|
832
|
+
skip();
|
|
833
|
+
const start = pos;
|
|
834
|
+
if (eat(/^\[/)) return parseArray(start, "]");
|
|
835
|
+
if (eat(/^array\s*\(/i)) return parseArray(start, ")");
|
|
836
|
+
if (text[pos] === "'" || text[pos] === "\"") return parseString(text[pos]);
|
|
837
|
+
const literal = eat(/^(?:-?\d+(?:\.\d+)?(?:e[+-]?\d+)?|true|false|null)\b/i);
|
|
838
|
+
if (literal === void 0) return fail("X18N2004", `"${text.slice(pos, pos + 20).split("\n")[0]}" is not a static value`);
|
|
839
|
+
const lower = literal.toLowerCase();
|
|
840
|
+
const value = lower === "null" ? null : lower === "true" ? true : lower === "false" ? false : Number(literal);
|
|
841
|
+
return {
|
|
842
|
+
kind: "scalar",
|
|
843
|
+
range: {
|
|
844
|
+
start,
|
|
845
|
+
end: pos
|
|
846
|
+
},
|
|
847
|
+
value,
|
|
848
|
+
quote: void 0
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
eat(/^<\?php\b/i);
|
|
852
|
+
if (eat(/^$/) !== void 0) return void 0;
|
|
853
|
+
eat(/^declare\s*\([^)]*\)\s*;/i);
|
|
854
|
+
if (!eat(/^return\b/i)) fail("X18N2004", "expected `return [ ... ];`");
|
|
855
|
+
const root = parseValue();
|
|
856
|
+
if (root.kind !== "array") fail("X18N2003", "the file must return an array");
|
|
857
|
+
if (!eat(/^;/)) fail("X18N2004", "expected \";\" after the array; expressions are not static");
|
|
858
|
+
eat(/^\?>/);
|
|
859
|
+
if (eat(/^$/) === void 0) fail("X18N2002", "unexpected text after the array");
|
|
860
|
+
return root;
|
|
861
|
+
}
|
|
862
|
+
function readTree(value, path, ranges) {
|
|
863
|
+
const out = {};
|
|
864
|
+
if (value.kind !== "array") return out;
|
|
865
|
+
for (const item of value.items) {
|
|
866
|
+
const childPath = [...path, item.key];
|
|
867
|
+
const inner = item.value.kind === "scalar" && item.value.quote ? {
|
|
868
|
+
start: item.value.range.start + 1,
|
|
869
|
+
end: item.value.range.end - 1
|
|
870
|
+
} : item.value.range;
|
|
871
|
+
ranges.set(pathId(childPath), {
|
|
872
|
+
key: item.keyRange,
|
|
873
|
+
value: inner
|
|
874
|
+
});
|
|
875
|
+
out[item.key] = item.value.kind === "array" ? readTree(item.value, childPath, ranges) : item.value.value;
|
|
876
|
+
}
|
|
877
|
+
return out;
|
|
878
|
+
}
|
|
879
|
+
const doubleEncodings = {
|
|
880
|
+
"\n": "\\n",
|
|
881
|
+
" ": "\\t",
|
|
882
|
+
"\r": "\\r",
|
|
883
|
+
"\v": "\\v",
|
|
884
|
+
"\f": "\\f"
|
|
885
|
+
};
|
|
886
|
+
function literal(value, quote) {
|
|
887
|
+
if (typeof value !== "string") return String(value);
|
|
888
|
+
if (quote === "\"" || /[\n\r\t\v\f]/.test(value)) return `"${value.replace(/[\\"$\n\t\r\v\f]/g, (char) => doubleEncodings[char] ?? `\\${char}`)}"`;
|
|
889
|
+
return `'${value.replace(/[\\']/g, "\\$&")}'`;
|
|
890
|
+
}
|
|
891
|
+
function arrayText(tree, unit, depth) {
|
|
892
|
+
const entries = Object.entries(tree);
|
|
893
|
+
if (entries.length === 0) return "[]";
|
|
894
|
+
const pad = unit.repeat(depth + 1);
|
|
895
|
+
return `[\n${entries.map(([key, child]) => `${pad}${literal(key, "'")} => ${isMessageLeaf(child) ? literal(child, "'") : arrayText(child, unit, depth + 1)},\n`).join("")}${unit.repeat(depth)}]`;
|
|
896
|
+
}
|
|
897
|
+
function find(root, path) {
|
|
898
|
+
let node = root;
|
|
899
|
+
for (const segment of path) node = node?.kind === "array" ? node.items.findLast((item) => item.key === segment)?.value : void 0;
|
|
900
|
+
return node;
|
|
901
|
+
}
|
|
902
|
+
/** Indent unit: configured, else the file's (first indented item), else 4 spaces. */
|
|
903
|
+
function unitOf(text, indent) {
|
|
904
|
+
if (indent !== void 0) return indent === "tab" ? " " : " ".repeat(indent);
|
|
905
|
+
return /^([ \t]+)['"\]]/m.exec(text)?.[1] ?? " ";
|
|
906
|
+
}
|
|
907
|
+
function applyOp$2(text, op, path, unit) {
|
|
908
|
+
const root = parseFile(text, path);
|
|
909
|
+
const node = root && find(root, op.path);
|
|
910
|
+
if (op.type === "set" && node?.kind === "scalar") return splice(text, node.range, literal(op.value, node.quote));
|
|
911
|
+
const tree = applyMessageOps(root ? readTree(root, [], /* @__PURE__ */ new Map()) : {}, [op]);
|
|
912
|
+
return root ? splice(text, root.range, arrayText(tree, unit, 0)) : `${text.trim() === "" ? "<?php" : text.trimEnd()}\n\nreturn ${arrayText(tree, unit, 0)};\n`;
|
|
913
|
+
}
|
|
914
|
+
function php() {
|
|
915
|
+
return {
|
|
916
|
+
name: "x18n:format-php",
|
|
917
|
+
formats: [{
|
|
918
|
+
id: "php",
|
|
919
|
+
extensions: ["php"],
|
|
920
|
+
parse(text, ctx) {
|
|
921
|
+
const root = parseFile(text, ctx.path);
|
|
922
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
923
|
+
return {
|
|
924
|
+
tree: root ? readTree(root, [], ranges) : {},
|
|
925
|
+
ranges
|
|
926
|
+
};
|
|
927
|
+
},
|
|
928
|
+
edit(text, ops, ctx) {
|
|
929
|
+
const unit = unitOf(text, ctx.indent);
|
|
930
|
+
let next = text;
|
|
931
|
+
for (const op of ops) next = applyOp$2(next, op, ctx.path, unit);
|
|
932
|
+
return diffText(text, next);
|
|
933
|
+
},
|
|
934
|
+
serialize: (tree, ctx) => `<?php\n\nreturn ${arrayText(tree, unitOf("", ctx.indent), 0)};\n`
|
|
935
|
+
}]
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
//#endregion
|
|
939
|
+
//#region src/formats/po.ts
|
|
940
|
+
/** gettext joins msgctxt and msgid with EOT. */
|
|
941
|
+
const CONTEXT_SEPARATOR = "";
|
|
942
|
+
const decodings = {
|
|
943
|
+
n: "\n",
|
|
944
|
+
t: " ",
|
|
945
|
+
r: "\r",
|
|
946
|
+
a: "\x07",
|
|
947
|
+
b: "\b",
|
|
948
|
+
f: "\f",
|
|
949
|
+
v: "\v"
|
|
950
|
+
};
|
|
951
|
+
const encodings$1 = {
|
|
952
|
+
"\\": "\\\\",
|
|
953
|
+
"\"": "\\\"",
|
|
954
|
+
"\n": "\\n",
|
|
955
|
+
" ": "\\t",
|
|
956
|
+
"\r": "\\r"
|
|
957
|
+
};
|
|
958
|
+
/** Reads one `"..."` string starting at `start`; returns its value and the offset after the closing quote. */
|
|
959
|
+
function readString(text, start, end, fail) {
|
|
960
|
+
if (text[start] !== "\"") fail("expected a string");
|
|
961
|
+
let value = "";
|
|
962
|
+
for (let i = start + 1; i < end; i++) {
|
|
963
|
+
const char = text[i];
|
|
964
|
+
if (char === "\"") {
|
|
965
|
+
if (text.slice(i + 1, end).trim() !== "") fail("unexpected text after string");
|
|
966
|
+
return {
|
|
967
|
+
value,
|
|
968
|
+
end: i + 1
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (char !== "\\") {
|
|
972
|
+
value += char;
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const escape = /^(?:[0-7]{1,3}|x[0-9a-f]{1,2}|[\s\S])/i.exec(text.slice(i + 1, end))?.[0] ?? "";
|
|
976
|
+
value += /^[0-7]/.test(escape) ? String.fromCharCode(Number.parseInt(escape, 8)) : escape.length > 1 ? String.fromCharCode(Number.parseInt(escape.slice(1), 16)) : decodings[escape] ?? escape;
|
|
977
|
+
i += escape.length;
|
|
978
|
+
}
|
|
979
|
+
return fail("unterminated string");
|
|
980
|
+
}
|
|
981
|
+
function toEntry(text, draft, fail) {
|
|
982
|
+
const field = (name) => draft.fields.find((f) => f.name === name);
|
|
983
|
+
const msgid = field("msgid");
|
|
984
|
+
if (!msgid) return draft.fields.length > 0 ? fail("entry without msgid") : void 0;
|
|
985
|
+
const msgctxt = field("msgctxt");
|
|
986
|
+
const msgstr = field("msgstr") ?? field("msgstr[0]");
|
|
987
|
+
if (!msgstr) return fail(`msgid "${msgid.value}" without msgstr`);
|
|
988
|
+
if (msgid.value === "" && !msgctxt) return void 0;
|
|
989
|
+
const inner = (f) => ({
|
|
990
|
+
start: f.range.start + 1,
|
|
991
|
+
end: f.range.end - 1
|
|
992
|
+
});
|
|
993
|
+
return {
|
|
994
|
+
key: msgctxt ? `${msgctxt.value}${CONTEXT_SEPARATOR}${msgid.value}` : msgid.value,
|
|
995
|
+
value: msgstr.value,
|
|
996
|
+
range: {
|
|
997
|
+
key: inner(msgid),
|
|
998
|
+
value: inner(msgstr)
|
|
999
|
+
},
|
|
1000
|
+
source: msgstr.range,
|
|
1001
|
+
block: withBlankLines(text, {
|
|
1002
|
+
start: draft.start,
|
|
1003
|
+
end: draft.end
|
|
1004
|
+
})
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function read$1(text, path) {
|
|
1008
|
+
const entries = [];
|
|
1009
|
+
let lineNumber = 0;
|
|
1010
|
+
const fail = (message) => {
|
|
1011
|
+
throw new X18nError("X18N2002", `${path}:${lineNumber}: ${message}`);
|
|
1012
|
+
};
|
|
1013
|
+
let draft;
|
|
1014
|
+
let field;
|
|
1015
|
+
const flush = () => {
|
|
1016
|
+
const entry = draft && toEntry(text, draft, fail);
|
|
1017
|
+
if (entry) entries.push(entry);
|
|
1018
|
+
draft = void 0;
|
|
1019
|
+
field = void 0;
|
|
1020
|
+
};
|
|
1021
|
+
for (const line of splitLines(text)) {
|
|
1022
|
+
lineNumber++;
|
|
1023
|
+
const offset = line.start + /^\s*/.exec(text.slice(line.start, line.end))[0].length;
|
|
1024
|
+
const content = text.slice(offset, line.end);
|
|
1025
|
+
const keyword = /^(msgctxt|msgid_plural|msgid|msgstr(?:\[\d+\])?)\s*/.exec(content);
|
|
1026
|
+
if (content === "" || content.startsWith("#") && draft?.fields.some((f) => f.name.startsWith("msgstr"))) flush();
|
|
1027
|
+
if (content === "") continue;
|
|
1028
|
+
if (keyword && (keyword[1] === "msgctxt" || keyword[1] === "msgid") && draft?.fields.some((f) => f.name !== "msgctxt")) flush();
|
|
1029
|
+
draft ??= {
|
|
1030
|
+
start: line.start,
|
|
1031
|
+
end: line.next,
|
|
1032
|
+
fields: []
|
|
1033
|
+
};
|
|
1034
|
+
draft.end = line.next;
|
|
1035
|
+
if (content.startsWith("#")) {
|
|
1036
|
+
field = void 0;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const stringStart = keyword ? offset + keyword[0].length : offset;
|
|
1040
|
+
if (!keyword && (!field || !content.startsWith("\""))) fail(`unexpected "${content.slice(0, 20)}"`);
|
|
1041
|
+
const string = readString(text, stringStart, line.end, fail);
|
|
1042
|
+
if (keyword) {
|
|
1043
|
+
field = {
|
|
1044
|
+
name: keyword[1],
|
|
1045
|
+
range: {
|
|
1046
|
+
start: stringStart,
|
|
1047
|
+
end: string.end
|
|
1048
|
+
},
|
|
1049
|
+
value: string.value
|
|
1050
|
+
};
|
|
1051
|
+
draft.fields.push(field);
|
|
1052
|
+
} else {
|
|
1053
|
+
field.range.end = string.end;
|
|
1054
|
+
field.value += string.value;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
flush();
|
|
1058
|
+
return entries;
|
|
1059
|
+
}
|
|
1060
|
+
const quote = (value) => `"${value.replace(/[\\"\n\t\r]/g, (char) => encodings$1[char])}"`;
|
|
1061
|
+
/** One string, or gettext's style of an empty first string and one string per line. */
|
|
1062
|
+
function encode(value, eol) {
|
|
1063
|
+
if (!value.slice(0, -1).includes("\n")) return quote(value);
|
|
1064
|
+
return ["\"\"", ...value.split(/(?<=\n)/).map(quote)].join(eol);
|
|
1065
|
+
}
|
|
1066
|
+
function entryText(key, value) {
|
|
1067
|
+
const index = key.indexOf(CONTEXT_SEPARATOR);
|
|
1068
|
+
return `${index === -1 ? "" : `msgctxt ${quote(key.slice(0, index))}\n`}msgid ${encode(key.slice(index + 1), "\n")}\nmsgstr ${encode(value, "\n")}\n`;
|
|
1069
|
+
}
|
|
1070
|
+
function applyOp$1(text, op, path) {
|
|
1071
|
+
const key = op.path.join(".");
|
|
1072
|
+
const entry = read$1(text, path).findLast((e) => e.key === key);
|
|
1073
|
+
if (op.type === "delete") return entry ? splice(text, entry.block, "") : text;
|
|
1074
|
+
const value = leafText(op.value);
|
|
1075
|
+
if (entry) return splice(text, entry.source, encode(value, detectEol(text)));
|
|
1076
|
+
return appendLines(text, `${text.trim() === "" || /\n[ \t]*\r?\n$/.test(text) ? "" : "\n"}${entryText(key, value)}`);
|
|
1077
|
+
}
|
|
1078
|
+
function po() {
|
|
1079
|
+
return {
|
|
1080
|
+
name: "x18n:format-po",
|
|
1081
|
+
formats: [{
|
|
1082
|
+
id: "po",
|
|
1083
|
+
extensions: ["po", "pot"],
|
|
1084
|
+
parse(text, ctx) {
|
|
1085
|
+
const tree = {};
|
|
1086
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
1087
|
+
for (const entry of read$1(text, ctx.path)) {
|
|
1088
|
+
tree[entry.key] = entry.value;
|
|
1089
|
+
ranges.set(pathId([entry.key]), entry.range);
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
tree,
|
|
1093
|
+
ranges
|
|
1094
|
+
};
|
|
1095
|
+
},
|
|
1096
|
+
edit(text, ops, ctx) {
|
|
1097
|
+
let next = text;
|
|
1098
|
+
for (const op of ops) next = applyOp$1(next, op, ctx.path);
|
|
1099
|
+
return diffText(text, next);
|
|
1100
|
+
},
|
|
1101
|
+
serialize(tree, ctx) {
|
|
1102
|
+
return [`msgid ""\nmsgstr ""\n"Language: ${ctx.locale}\\n"\n"MIME-Version: 1.0\\n"\n"Content-Type: text/plain; charset=UTF-8\\n"\n"Content-Transfer-Encoding: 8bit\\n"\n`, ...[...walkLeaves(tree)].map((leaf) => entryText(leaf.path.join("."), leafText(leaf.value)))].join("\n");
|
|
1103
|
+
}
|
|
1104
|
+
}]
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
//#endregion
|
|
1108
|
+
//#region src/formats/properties.ts
|
|
1109
|
+
const escapes = {
|
|
1110
|
+
t: " ",
|
|
1111
|
+
n: "\n",
|
|
1112
|
+
r: "\r",
|
|
1113
|
+
f: "\f"
|
|
1114
|
+
};
|
|
1115
|
+
const isSpace = (char) => char === " " || char === " " || char === "\f";
|
|
1116
|
+
function unescape(raw, path) {
|
|
1117
|
+
let out = "";
|
|
1118
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1119
|
+
const char = raw[i];
|
|
1120
|
+
if (char !== "\\") {
|
|
1121
|
+
out += char;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const next = raw[++i];
|
|
1125
|
+
if (next === void 0) break;
|
|
1126
|
+
if (next === "\r" || next === "\n") {
|
|
1127
|
+
if (next === "\r" && raw[i + 1] === "\n") i++;
|
|
1128
|
+
while (isSpace(raw[i + 1])) i++;
|
|
1129
|
+
} else if (next === "u") {
|
|
1130
|
+
const hex = raw.slice(i + 1, i + 5);
|
|
1131
|
+
if (!/^[0-9a-f]{4}$/i.test(hex)) throw new X18nError("X18N2002", `${path}: malformed \\u escape "\\u${hex}"`);
|
|
1132
|
+
out += String.fromCharCode(Number.parseInt(hex, 16));
|
|
1133
|
+
i += 4;
|
|
1134
|
+
} else out += escapes[next] ?? next;
|
|
1135
|
+
}
|
|
1136
|
+
return out;
|
|
1137
|
+
}
|
|
1138
|
+
function read(text, path) {
|
|
1139
|
+
const entries = [];
|
|
1140
|
+
const lines = splitLines(text);
|
|
1141
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1142
|
+
let p = lines[i].start;
|
|
1143
|
+
const lineStart = p;
|
|
1144
|
+
while (isSpace(text[p])) p++;
|
|
1145
|
+
if (p >= lines[i].end || text[p] === "#" || text[p] === "!") continue;
|
|
1146
|
+
while (i < lines.length - 1 && /(?:^|[^\\])(?:\\\\)*\\$/.test(text.slice(lines[i].start, lines[i].end))) i++;
|
|
1147
|
+
const end = lines[i].end;
|
|
1148
|
+
const keyStart = p;
|
|
1149
|
+
while (p < end && !"=: \f".includes(text[p])) p += text[p] === "\\" ? 2 : 1;
|
|
1150
|
+
const keyEnd = Math.min(p, end);
|
|
1151
|
+
while (isSpace(text[p])) p++;
|
|
1152
|
+
if (text[p] === "=" || text[p] === ":") p++;
|
|
1153
|
+
while (isSpace(text[p])) p++;
|
|
1154
|
+
const valueStart = Math.min(p, end);
|
|
1155
|
+
entries.push({
|
|
1156
|
+
key: unescape(text.slice(keyStart, keyEnd), path),
|
|
1157
|
+
value: unescape(text.slice(valueStart, end), path),
|
|
1158
|
+
range: {
|
|
1159
|
+
key: {
|
|
1160
|
+
start: keyStart,
|
|
1161
|
+
end: keyEnd
|
|
1162
|
+
},
|
|
1163
|
+
value: {
|
|
1164
|
+
start: valueStart,
|
|
1165
|
+
end
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
line: {
|
|
1169
|
+
start: lineStart,
|
|
1170
|
+
end: lines[i].next
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
return entries;
|
|
1175
|
+
}
|
|
1176
|
+
const encodings = {
|
|
1177
|
+
"\\": "\\\\",
|
|
1178
|
+
" ": "\\t",
|
|
1179
|
+
"\n": "\\n",
|
|
1180
|
+
"\r": "\\r",
|
|
1181
|
+
"\f": "\\f"
|
|
1182
|
+
};
|
|
1183
|
+
const escapeText = (text) => text.replace(/[\\\n\r\t\f]/g, (char) => encodings[char]);
|
|
1184
|
+
const escapeKey = (key) => escapeText(key).replace(/[=: #!]/g, "\\$&");
|
|
1185
|
+
const escapeValue = (value) => escapeText(value).replace(/^ /, "\\ ");
|
|
1186
|
+
function applyOp(text, op, path) {
|
|
1187
|
+
const key = op.path.join(".");
|
|
1188
|
+
const entries = read(text, path);
|
|
1189
|
+
const entry = entries.findLast((e) => e.key === key);
|
|
1190
|
+
if (op.type === "delete") return entry ? splice(text, entry.line, "") : text;
|
|
1191
|
+
const value = escapeValue(leafText(op.value));
|
|
1192
|
+
if (entry) return splice(text, entry.range.value, value);
|
|
1193
|
+
const last = entries.findLast((e) => e.value !== "");
|
|
1194
|
+
const separator = last ? text.slice(last.range.key.end, last.range.value.start) : "";
|
|
1195
|
+
return appendLines(text, `${escapeKey(key)}${/[=:\s]/.test(separator) ? separator : "="}${value}\n`);
|
|
1196
|
+
}
|
|
1197
|
+
function properties() {
|
|
1198
|
+
return {
|
|
1199
|
+
name: "x18n:format-properties",
|
|
1200
|
+
formats: [{
|
|
1201
|
+
id: "properties",
|
|
1202
|
+
extensions: ["properties"],
|
|
1203
|
+
parse(text, ctx) {
|
|
1204
|
+
const tree = {};
|
|
1205
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
1206
|
+
for (const entry of read(text, ctx.path)) {
|
|
1207
|
+
tree[entry.key] = entry.value;
|
|
1208
|
+
ranges.set(pathId([entry.key]), entry.range);
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
tree,
|
|
1212
|
+
ranges
|
|
1213
|
+
};
|
|
1214
|
+
},
|
|
1215
|
+
edit(text, ops, ctx) {
|
|
1216
|
+
let next = text;
|
|
1217
|
+
for (const op of ops) next = applyOp(next, op, ctx.path);
|
|
1218
|
+
return diffText(text, next);
|
|
1219
|
+
},
|
|
1220
|
+
serialize: (tree) => [...walkLeaves(tree)].map((leaf) => `${escapeKey(leaf.path.join("."))}=${escapeValue(leafText(leaf.value))}\n`).join("")
|
|
1221
|
+
}]
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
//#endregion
|
|
1225
|
+
//#region src/formats/yaml.ts
|
|
1226
|
+
function scalarTextRange(node) {
|
|
1227
|
+
const range = node.range;
|
|
1228
|
+
if (!range) return void 0;
|
|
1229
|
+
if (isScalar(node) && (node.type === Scalar.QUOTE_DOUBLE || node.type === Scalar.QUOTE_SINGLE)) return {
|
|
1230
|
+
start: range[0] + 1,
|
|
1231
|
+
end: range[1] - 1
|
|
1232
|
+
};
|
|
1233
|
+
return {
|
|
1234
|
+
start: range[0],
|
|
1235
|
+
end: range[1]
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function readMap(node, path, ranges) {
|
|
1239
|
+
const out = {};
|
|
1240
|
+
if (!isMap(node)) return out;
|
|
1241
|
+
for (const pair of node.items) {
|
|
1242
|
+
const keyNode = pair.key;
|
|
1243
|
+
if (!isScalar(keyNode)) continue;
|
|
1244
|
+
const key = String(keyNode.value);
|
|
1245
|
+
const childPath = [...path, key];
|
|
1246
|
+
const keyRange = scalarTextRange(keyNode);
|
|
1247
|
+
const value = isNode(pair.value) ? pair.value : void 0;
|
|
1248
|
+
const valueRange = value && value.range ? {
|
|
1249
|
+
start: value.range[0],
|
|
1250
|
+
end: value.range[1]
|
|
1251
|
+
} : void 0;
|
|
1252
|
+
if (keyRange) ranges.set(pathId(childPath), {
|
|
1253
|
+
key: keyRange,
|
|
1254
|
+
value: valueRange ?? keyRange
|
|
1255
|
+
});
|
|
1256
|
+
if (value === void 0) out[key] = null;
|
|
1257
|
+
else if (isMap(value)) out[key] = readMap(value, childPath, ranges);
|
|
1258
|
+
else if (isScalar(value)) {
|
|
1259
|
+
const leaf = toLeaf(value.value);
|
|
1260
|
+
if (leaf !== void 0) out[key] = leaf;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return out;
|
|
1264
|
+
}
|
|
1265
|
+
function parse$1(text, path) {
|
|
1266
|
+
const doc = parseDocument(text, {
|
|
1267
|
+
keepSourceTokens: true,
|
|
1268
|
+
uniqueKeys: false
|
|
1269
|
+
});
|
|
1270
|
+
if (doc.errors.length > 0) throw new X18nError("X18N2002", `${path}: ${doc.errors[0].message}`);
|
|
1271
|
+
return doc;
|
|
1272
|
+
}
|
|
1273
|
+
function scalarText(value, type) {
|
|
1274
|
+
if (typeof value !== "string") return stringify(value).trimEnd();
|
|
1275
|
+
if (type === Scalar.QUOTE_DOUBLE) return JSON.stringify(value);
|
|
1276
|
+
if (type === Scalar.QUOTE_SINGLE) return `'${value.replace(/'/g, "''")}'`;
|
|
1277
|
+
return stringify(value, { lineWidth: 0 }).trimEnd();
|
|
1278
|
+
}
|
|
1279
|
+
function indentOf(text) {
|
|
1280
|
+
return /^( +)\S/m.exec(text)?.[1]?.length ?? 2;
|
|
1281
|
+
}
|
|
1282
|
+
function yaml() {
|
|
1283
|
+
return {
|
|
1284
|
+
name: "x18n:format-yaml",
|
|
1285
|
+
formats: [{
|
|
1286
|
+
id: "yaml",
|
|
1287
|
+
extensions: ["yml", "yaml"],
|
|
1288
|
+
parse(text, ctx) {
|
|
1289
|
+
const doc = parse$1(text, ctx.path);
|
|
1290
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
1291
|
+
if (doc.contents === null) return {
|
|
1292
|
+
tree: {},
|
|
1293
|
+
ranges
|
|
1294
|
+
};
|
|
1295
|
+
if (!isMap(doc.contents)) throw new X18nError("X18N2003", ctx.path);
|
|
1296
|
+
return {
|
|
1297
|
+
tree: readMap(doc.contents, [], ranges),
|
|
1298
|
+
ranges
|
|
1299
|
+
};
|
|
1300
|
+
},
|
|
1301
|
+
edit(text, ops, ctx) {
|
|
1302
|
+
let next = text;
|
|
1303
|
+
for (const op of ops) {
|
|
1304
|
+
let doc = parse$1(next, ctx.path);
|
|
1305
|
+
const current = doc.getIn(op.path, true);
|
|
1306
|
+
if (op.type === "set" && isScalar(current) && current.range && current.type !== Scalar.BLOCK_FOLDED && current.type !== Scalar.BLOCK_LITERAL) {
|
|
1307
|
+
next = applyTextEdits(next, [{
|
|
1308
|
+
range: {
|
|
1309
|
+
start: current.range[0],
|
|
1310
|
+
end: current.range[1]
|
|
1311
|
+
},
|
|
1312
|
+
text: scalarText(op.value, current.type)
|
|
1313
|
+
}]);
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
if (doc.contents === null) doc = new Document({});
|
|
1317
|
+
if (op.type === "set") doc.setIn(op.path, op.value);
|
|
1318
|
+
else doc.deleteIn(op.path);
|
|
1319
|
+
next = doc.toString({
|
|
1320
|
+
indent: indentOf(text),
|
|
1321
|
+
lineWidth: 0
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
return diffText(text, next);
|
|
1325
|
+
},
|
|
1326
|
+
serialize: (tree) => Object.keys(tree).length === 0 ? "{}\n" : stringify(tree, { lineWidth: 0 })
|
|
1327
|
+
}]
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
//#endregion
|
|
1331
|
+
export { json5 as a, ini as c, php as i, ftl as l, properties as n, json as o, po as r, jsonc as s, yaml as t, ecmascript as u };
|