@x18n/builtins 0.0.14 → 0.1.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/dist/checks.d.mts CHANGED
@@ -3,6 +3,7 @@ import { Check, Plugin } from "@x18n/core";
3
3
  export declare const missingKey: Check;
4
4
  export declare const missingTranslation: Check;
5
5
  export declare const staleTranslation: Check;
6
+ export declare const reviewChangesRequested: Check;
6
7
  export declare const emptyTranslation: Check;
7
8
  export declare const redundantTranslation: Check;
8
9
  export declare const keyConflict: Check;
package/dist/checks.mjs CHANGED
@@ -88,6 +88,30 @@ const staleTranslation = {
88
88
  };
89
89
  }
90
90
  };
91
+ const reviewChangesRequested = {
92
+ id: "review-changes-requested",
93
+ defaultSeverity: "info",
94
+ description: "A message with an open \"request change\" review.",
95
+ *run(ctx) {
96
+ for (const thread of ctx.reviews({ open: true })) {
97
+ if (thread.state !== "changes-requested") continue;
98
+ const request = thread.comments.findLast((comment) => comment.type === "request-change");
99
+ const by = request.author?.name ? ` by ${request.author.name}` : "";
100
+ yield {
101
+ key: thread.key,
102
+ locale: thread.locale,
103
+ message: `Change requested${by} for "${thread.key}" in ${thread.locale}${request.text ? `: ${request.text}` : "."}`,
104
+ fixes: [{
105
+ title: "Resolve the review",
106
+ ops: [{
107
+ type: "resolve-review",
108
+ id: request.id
109
+ }]
110
+ }]
111
+ };
112
+ }
113
+ }
114
+ };
91
115
  const emptyTranslation = {
92
116
  id: "empty-translation",
93
117
  defaultSeverity: "warning",
@@ -361,6 +385,7 @@ const allChecks = [
361
385
  keyConflict,
362
386
  missingTranslation,
363
387
  staleTranslation,
388
+ reviewChangesRequested,
364
389
  emptyTranslation,
365
390
  redundantTranslation,
366
391
  unusedKey,
@@ -379,4 +404,4 @@ function recommended() {
379
404
  };
380
405
  }
381
406
  //#endregion
382
- export { allChecks, dynamicKey, emptyTranslation, glossary, hardString, identicalToSource, invalidSyntax, keyConflict, keyStyle, localeCodeConflict, missingKey, missingTranslation, placeholderMismatch, recommended, redundantTranslation, staleTranslation, unusedKey };
407
+ export { allChecks, dynamicKey, emptyTranslation, glossary, hardString, identicalToSource, invalidSyntax, keyConflict, keyStyle, localeCodeConflict, missingKey, missingTranslation, placeholderMismatch, recommended, redundantTranslation, reviewChangesRequested, staleTranslation, unusedKey };
@@ -1,5 +1,6 @@
1
- import { a as splice } from "../shared-BWaXUmwA.mjs";
2
- import { applyMessageOps, diffText, errors, isMessageLeaf, pathId } from "@x18n/core";
1
+ import { r as indentUnit } from "../shared-C7ceptwi.mjs";
2
+ import { n as literalText, t as editLiteral } from "../literal-gIdB1VGy.mjs";
3
+ import { applyMessageOps, diffText, errors, pathId } from "@x18n/core";
3
4
  import { parseSync } from "oxc-parser";
4
5
  //#region src/formats/ecmascript/index.ts
5
6
  /** Type-only wrappers, `defineX({ ... })` helpers that return their argument, and loaders that only return an object (`defineI18nLocale(() => ({ ... }))`). */
@@ -71,46 +72,71 @@ function readLeaf(node, text) {
71
72
  quote: void 0
72
73
  };
73
74
  }
74
- function readObject(node, path, module, text, fail) {
75
- const out = {};
75
+ function readObject(node, path, ranges, text, fail) {
76
+ const tree = {};
77
+ const items = [];
76
78
  for (const property of node.properties) {
77
79
  const { key, range, value } = readProperty(property, fail);
78
80
  const childPath = [...path, key];
79
- const id = pathId(childPath);
81
+ const item = {
82
+ key,
83
+ range: {
84
+ start: property.start,
85
+ end: property.end
86
+ }
87
+ };
80
88
  if (value.type === "ObjectExpression") {
81
- module.ranges.set(id, {
89
+ ranges.set(pathId(childPath), {
82
90
  key: range,
83
91
  value: {
84
92
  start: value.start,
85
93
  end: value.end
86
94
  }
87
95
  });
88
- out[key] = readObject(value, childPath, module, text, fail);
96
+ const child = readObject(value, childPath, ranges, text, fail);
97
+ tree[key] = child.tree;
98
+ items.push({
99
+ ...item,
100
+ value: child.literal
101
+ });
89
102
  continue;
90
103
  }
91
104
  const leaf = readLeaf(value, text);
92
105
  if (!leaf) return fail("X18N2004", value, `${value.type} is not a static value`);
93
- const quoted = leaf.quote !== void 0;
94
- module.ranges.set(id, {
106
+ ranges.set(pathId(childPath), {
95
107
  key: range,
96
- value: quoted ? {
97
- start: value.start + 1,
98
- end: value.end - 1
99
- } : {
108
+ value: leaf.quote === void 0 ? {
100
109
  start: value.start,
101
110
  end: value.end
111
+ } : {
112
+ start: value.start + 1,
113
+ end: value.end - 1
102
114
  }
103
115
  });
104
- module.scalars.set(id, {
105
- range: {
106
- start: value.start,
107
- end: value.end
108
- },
109
- quote: leaf.quote
116
+ tree[key] = leaf.value;
117
+ items.push({
118
+ ...item,
119
+ value: {
120
+ kind: "scalar",
121
+ range: {
122
+ start: value.start,
123
+ end: value.end
124
+ },
125
+ quote: leaf.quote
126
+ }
110
127
  });
111
- out[key] = leaf.value;
112
128
  }
113
- return out;
129
+ return {
130
+ tree,
131
+ literal: {
132
+ kind: "object",
133
+ range: {
134
+ start: node.start,
135
+ end: node.end
136
+ },
137
+ items
138
+ }
139
+ };
114
140
  }
115
141
  function readModule(text, path) {
116
142
  const fail = (code, node, reason) => {
@@ -129,14 +155,15 @@ function readModule(text, path) {
129
155
  reason: result.errors[0].message
130
156
  });
131
157
  const root = findRoot(result.program, fail);
132
- const found = {
133
- ranges: /* @__PURE__ */ new Map(),
134
- scalars: /* @__PURE__ */ new Map()
158
+ const ranges = /* @__PURE__ */ new Map();
159
+ if (!root) return {
160
+ tree: {},
161
+ ranges,
162
+ literal: void 0
135
163
  };
136
164
  return {
137
- root,
138
- tree: root ? readObject(root, [], found, text, fail) : {},
139
- ...found
165
+ ...readObject(root, [], ranges, text, fail),
166
+ ranges
140
167
  };
141
168
  }
142
169
  const singleEncodings = {
@@ -153,29 +180,23 @@ function literal(value, quote) {
153
180
  if (quote === "`") return `\`${value.replace(/\\|`|\$\{/g, (match) => `\\${match}`)}\``;
154
181
  return `'${value.replace(/[\\'\n\r\u2028\u2029]/g, (char) => singleEncodings[char])}'`;
155
182
  }
156
- function objectText(tree, unit, depth) {
157
- const entries = Object.entries(tree);
158
- if (entries.length === 0) return "{}";
159
- const pad = unit.repeat(depth + 1);
160
- return `{\n${entries.map(([key, child]) => {
161
- const name = /^[a-z_$][\w$]*$/i.test(key) ? key : literal(key, "'");
162
- return `${pad}${name}: ${isMessageLeaf(child) ? literal(child, "'") : objectText(child, unit, depth + 1)},\n`;
163
- }).join("")}${unit.repeat(depth)}}`;
164
- }
165
- /** Indent unit: configured, else the file's (first indented line), else 2 spaces. */
166
- function unitOf(text, indent) {
167
- if (indent !== void 0) return indent === "tab" ? " " : " ".repeat(indent);
168
- return /^([ \t]+)\S/m.exec(text)?.[1] ?? " ";
183
+ function syntaxOf(text, indent) {
184
+ return {
185
+ open: "{",
186
+ close: "}",
187
+ separator: ": ",
188
+ key: (key) => /^[a-z_$][\w$]*$/i.test(key) ? key : literal(key, "'"),
189
+ leaf: (value, quote) => literal(value, quote ?? "'"),
190
+ unit: indentUnit(text, indent, " "),
191
+ trailingComma: true
192
+ };
169
193
  }
170
- function moduleText(tree, path, unit) {
171
- return `${path.endsWith(".cjs") ? "module.exports =" : "export default"} ${objectText(tree, unit, 0)}\n`;
194
+ function moduleText(tree, path, syntax) {
195
+ return `${path.endsWith(".cjs") ? "module.exports =" : "export default"} ${literalText(tree, syntax)}\n`;
172
196
  }
173
- function applyOp(text, op, path, unit) {
174
- const module = readModule(text, path);
175
- const scalar = module.scalars.get(pathId(op.path));
176
- if (op.type === "set" && scalar) return splice(text, scalar.range, literal(op.value, scalar.quote));
177
- const tree = applyMessageOps(module.tree, [op]);
178
- return module.root ? splice(text, module.root, objectText(tree, unit, 0)) : moduleText(tree, path, unit);
197
+ function applyOp(text, op, path, syntax) {
198
+ const root = readModule(text, path).literal;
199
+ return root ? editLiteral(text, root, op, syntax) : moduleText(applyMessageOps({}, [op]), path, syntax);
179
200
  }
180
201
  function ecmascript() {
181
202
  return {
@@ -198,12 +219,12 @@ function ecmascript() {
198
219
  };
199
220
  },
200
221
  edit(text, ops, ctx) {
201
- const unit = unitOf(text, ctx.indent);
222
+ const syntax = syntaxOf(text, ctx.indent);
202
223
  let next = text;
203
- for (const op of ops) next = applyOp(next, op, ctx.path, unit);
224
+ for (const op of ops) next = applyOp(next, op, ctx.path, syntax);
204
225
  return diffText(text, next);
205
226
  },
206
- serialize: (tree, ctx) => moduleText(tree, ctx.path, unitOf("", ctx.indent))
227
+ serialize: (tree, ctx) => moduleText(tree, ctx.path, syntaxOf("", ctx.indent))
207
228
  }]
208
229
  };
209
230
  }
@@ -1,4 +1,4 @@
1
- import { a as splice, c as withBlankLines, i as leafText, n as detectEol, o as splitLines, t as appendLines } from "../shared-BWaXUmwA.mjs";
1
+ import { a as splice, c as withBlankLines, i as leafText, n as detectEol, o as splitLines, t as appendLines } from "../shared-C7ceptwi.mjs";
2
2
  import { diffText, errors, pathId, walkLeaves } from "@x18n/core";
3
3
  //#region src/formats/ftl/index.ts
4
4
  const trimmedEnd = (text, line) => line.start + text.slice(line.start, line.end).trimEnd().length;
@@ -1,4 +1,4 @@
1
- import { a as splice, i as leafText, n as detectEol, o as splitLines, t as appendLines } from "../shared-BWaXUmwA.mjs";
1
+ import { a as splice, i as leafText, n as detectEol, o as splitLines, t as appendLines } from "../shared-C7ceptwi.mjs";
2
2
  import { buildTree, diffText, errors, isMessageLeaf, pathId } from "@x18n/core";
3
3
  //#region src/formats/ini/index.ts
4
4
  /** Reads the value after `=`; `start` is its first non-blank offset. */
@@ -1,4 +1,4 @@
1
- import { t as createJsonFormat } from "../json-Db-8qSbE.mjs";
1
+ import { t as createJsonFormat } from "../json-CGNSGS97.mjs";
2
2
  //#region src/formats/json/index.ts
3
3
  function json(options = {}) {
4
4
  return {
@@ -1,6 +1,115 @@
1
- import { errors, isMessageTree } from "@x18n/core";
1
+ import { r as indentUnit } from "../shared-C7ceptwi.mjs";
2
+ import { t as editLiteral } from "../literal-gIdB1VGy.mjs";
3
+ import { diffText, errors, isMessageTree, pathId } from "@x18n/core";
2
4
  import JSON5 from "json5";
3
5
  //#region src/formats/json5/index.ts
6
+ const stringToken = /^(?:"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')/;
7
+ function parse(text, path) {
8
+ let data;
9
+ try {
10
+ data = JSON5.parse(text);
11
+ } catch (error) {
12
+ throw errors.X18N2002({
13
+ path,
14
+ reason: error instanceof Error ? error.message : String(error),
15
+ cause: error
16
+ });
17
+ }
18
+ if (!isMessageTree(data)) throw errors.X18N2003({ path });
19
+ return data;
20
+ }
21
+ /** Where the keys and values of valid JSON5 that holds a message tree are. */
22
+ function scan(text) {
23
+ const ranges = /* @__PURE__ */ new Map();
24
+ let pos = 0;
25
+ const skip = () => {
26
+ pos += /^(?:\s|\/\/.*|\/\*[\s\S]*?\*\/)*/.exec(text.slice(pos))[0].length;
27
+ };
28
+ const token = (pattern) => {
29
+ const start = pos;
30
+ pos += pattern.exec(text.slice(pos))[0].length;
31
+ return {
32
+ start,
33
+ end: pos
34
+ };
35
+ };
36
+ const inner = (range, quoted) => quoted ? {
37
+ start: range.start + 1,
38
+ end: range.end - 1
39
+ } : range;
40
+ const scalar = () => {
41
+ const quote = text[pos] === "\"" || text[pos] === "'" ? text[pos] : void 0;
42
+ return {
43
+ kind: "scalar",
44
+ range: token(quote ? stringToken : /^[^\s,}/]+/),
45
+ quote
46
+ };
47
+ };
48
+ const object = (path) => {
49
+ const start = pos++;
50
+ const items = [];
51
+ skip();
52
+ while (text[pos] !== "}") {
53
+ const quoted = text[pos] === "\"" || text[pos] === "'";
54
+ const keyRange = token(quoted ? stringToken : /^[^\s:/]+/);
55
+ const key = Object.keys(JSON5.parse(`{${text.slice(keyRange.start, keyRange.end)}:0}`))[0];
56
+ skip();
57
+ pos++;
58
+ skip();
59
+ const value = text[pos] === "{" ? object([...path, key]) : scalar();
60
+ ranges.set(pathId([...path, key]), {
61
+ key: inner(keyRange, quoted),
62
+ value: inner(value.range, value.kind === "scalar" && value.quote !== void 0)
63
+ });
64
+ items.push({
65
+ key,
66
+ range: {
67
+ start: keyRange.start,
68
+ end: value.range.end
69
+ },
70
+ value
71
+ });
72
+ skip();
73
+ if (text[pos] === ",") pos++;
74
+ skip();
75
+ }
76
+ pos++;
77
+ return {
78
+ kind: "object",
79
+ range: {
80
+ start,
81
+ end: pos
82
+ },
83
+ items
84
+ };
85
+ };
86
+ skip();
87
+ return {
88
+ root: object([]),
89
+ ranges
90
+ };
91
+ }
92
+ function firstQuote(object) {
93
+ for (const { value } of object.items) {
94
+ const quote = value.kind === "scalar" ? value.quote : firstQuote(value);
95
+ if (quote) return quote;
96
+ }
97
+ }
98
+ /** New keys and strings follow the file: quoted keys if its first key is quoted, and the quote of its first string. */
99
+ function syntaxOf(text, root, indent) {
100
+ const first = root.items[0];
101
+ const keyQuote = first && /^["']/.test(text[first.range.start]) ? text[first.range.start] : void 0;
102
+ const quote = firstQuote(root) ?? "\"";
103
+ return {
104
+ open: "{",
105
+ close: "}",
106
+ separator: ": ",
107
+ key: (key) => keyQuote ? JSON5.stringify(key, { quote: keyQuote }) : /^[a-z_$][\w$]*$/i.test(key) ? key : JSON5.stringify(key, { quote }),
108
+ leaf: (value, current) => JSON5.stringify(value, { quote: current ?? quote }),
109
+ unit: indentUnit(text, indent, " "),
110
+ trailingComma: false
111
+ };
112
+ }
4
113
  function json5() {
5
114
  return {
6
115
  name: "x18n:format-json5",
@@ -8,19 +117,23 @@ function json5() {
8
117
  id: "json5",
9
118
  extensions: ["json5"],
10
119
  parse(text, ctx) {
11
- if (text.trim() === "") return { tree: {} };
12
- let data;
13
- try {
14
- data = JSON5.parse(text);
15
- } catch (error) {
16
- throw errors.X18N2002({
17
- path: ctx.path,
18
- reason: error instanceof Error ? error.message : String(error),
19
- cause: error
20
- });
120
+ if (text.trim() === "") return {
121
+ tree: {},
122
+ ranges: /* @__PURE__ */ new Map()
123
+ };
124
+ return {
125
+ tree: parse(text, ctx.path),
126
+ ranges: scan(text).ranges
127
+ };
128
+ },
129
+ edit(text, ops, ctx) {
130
+ let next = text.trim() === "" ? "{}\n" : text;
131
+ for (const op of ops) {
132
+ parse(next, ctx.path);
133
+ const { root } = scan(next);
134
+ next = editLiteral(next, root, op, syntaxOf(next, root, ctx.indent));
21
135
  }
22
- if (!isMessageTree(data)) throw errors.X18N2003({ path: ctx.path });
23
- return { tree: data };
136
+ return diffText(text, next);
24
137
  },
25
138
  serialize: (tree, ctx) => `${JSON5.stringify(tree, null, ctx.indent === "tab" ? " " : ctx.indent ?? 2)}\n`
26
139
  }]
@@ -1,4 +1,4 @@
1
- import { t as createJsonFormat } from "../json-Db-8qSbE.mjs";
1
+ import { t as createJsonFormat } from "../json-CGNSGS97.mjs";
2
2
  //#region src/formats/jsonc/index.ts
3
3
  function jsonc(options = {}) {
4
4
  return {
@@ -1,5 +1,5 @@
1
- import { a as splice } from "../shared-BWaXUmwA.mjs";
2
- import { applyMessageOps, diffText, errors, isMessageLeaf, pathId } from "@x18n/core";
1
+ import { n as literalText, t as editLiteral } from "../literal-gIdB1VGy.mjs";
2
+ import { applyMessageOps, diffText, errors, pathId } from "@x18n/core";
3
3
  //#region src/formats/php/index.ts
4
4
  const doubleEscapes = {
5
5
  "n": "\n",
@@ -68,20 +68,25 @@ function parseFile(text, path) {
68
68
  const key = parseValue();
69
69
  if (!eat(/^=>/)) return fail("X18N2003", "array item without a key");
70
70
  if (key.kind !== "scalar" || typeof key.value !== "string" && typeof key.value !== "number") return fail("X18N2004", "array keys must be strings or integers");
71
+ const value = parseValue();
71
72
  items.push({
72
73
  key: String(key.value),
74
+ range: {
75
+ start: key.range.start,
76
+ end: value.range.end
77
+ },
73
78
  keyRange: key.quote ? {
74
79
  start: key.range.start + 1,
75
80
  end: key.range.end - 1
76
81
  } : key.range,
77
- value: parseValue()
82
+ value
78
83
  });
79
84
  if (eat(/^,/)) continue;
80
85
  skip();
81
86
  if (text[pos] !== close) return fail("X18N2004", `expected "," or "${close}"; expressions are not static`);
82
87
  }
83
88
  return {
84
- kind: "array",
89
+ kind: "object",
85
90
  range: {
86
91
  start,
87
92
  end: pos
@@ -114,7 +119,7 @@ function parseFile(text, path) {
114
119
  eat(/^declare\s*\([^)]*\)\s*;/i);
115
120
  if (!eat(/^return\b/i)) fail("X18N2004", "expected `return [ ... ];`");
116
121
  const root = parseValue();
117
- if (root.kind !== "array") fail("X18N2003", "the file must return an array");
122
+ if (root.kind !== "object") fail("X18N2003", "the file must return an array");
118
123
  if (!eat(/^;/)) fail("X18N2004", "expected \";\" after the array; expressions are not static");
119
124
  eat(/^\?>/);
120
125
  if (eat(/^$/) === void 0) fail("X18N2002", "unexpected text after the array");
@@ -122,7 +127,7 @@ function parseFile(text, path) {
122
127
  }
123
128
  function readTree(value, path, ranges) {
124
129
  const out = {};
125
- if (value.kind !== "array") return out;
130
+ if (value.kind !== "object") return out;
126
131
  for (const item of value.items) {
127
132
  const childPath = [...path, item.key];
128
133
  const inner = item.value.kind === "scalar" && item.value.quote ? {
@@ -133,7 +138,7 @@ function readTree(value, path, ranges) {
133
138
  key: item.keyRange,
134
139
  value: inner
135
140
  });
136
- out[item.key] = item.value.kind === "array" ? readTree(item.value, childPath, ranges) : item.value.value;
141
+ out[item.key] = item.value.kind === "object" ? readTree(item.value, childPath, ranges) : item.value.value;
137
142
  }
138
143
  return out;
139
144
  }
@@ -149,28 +154,21 @@ function literal(value, quote) {
149
154
  if (quote === "\"" || /[\n\r\t\v\f]/.test(value)) return `"${value.replace(/[\\"$\n\t\r\v\f]/g, (char) => doubleEncodings[char] ?? `\\${char}`)}"`;
150
155
  return `'${value.replace(/[\\']/g, "\\$&")}'`;
151
156
  }
152
- function arrayText(tree, unit, depth) {
153
- const entries = Object.entries(tree);
154
- if (entries.length === 0) return "[]";
155
- const pad = unit.repeat(depth + 1);
156
- return `[\n${entries.map(([key, child]) => `${pad}${literal(key, "'")} => ${isMessageLeaf(child) ? literal(child, "'") : arrayText(child, unit, depth + 1)},\n`).join("")}${unit.repeat(depth)}]`;
157
- }
158
- function find(root, path) {
159
- let node = root;
160
- for (const segment of path) node = node?.kind === "array" ? node.items.findLast((item) => item.key === segment)?.value : void 0;
161
- return node;
162
- }
163
- /** Indent unit: configured, else the file's (first indented item), else 4 spaces. */
164
- function unitOf(text, indent) {
165
- if (indent !== void 0) return indent === "tab" ? " " : " ".repeat(indent);
166
- return /^([ \t]+)['"\]]/m.exec(text)?.[1] ?? " ";
157
+ /** The indent unit is the configured one, else the file's (first indented item), else 4 spaces. */
158
+ function syntaxOf(text, indent) {
159
+ return {
160
+ open: "[",
161
+ close: "]",
162
+ separator: " => ",
163
+ key: (key) => literal(key, "'"),
164
+ leaf: (value, quote) => literal(value, quote),
165
+ unit: indent === void 0 ? /^([ \t]+)['"\]]/m.exec(text)?.[1] ?? " " : indent === "tab" ? " " : " ".repeat(indent),
166
+ trailingComma: true
167
+ };
167
168
  }
168
- function applyOp(text, op, path, unit) {
169
+ function applyOp(text, op, path, syntax) {
169
170
  const root = parseFile(text, path);
170
- const node = root && find(root, op.path);
171
- if (op.type === "set" && node?.kind === "scalar") return splice(text, node.range, literal(op.value, node.quote));
172
- const tree = applyMessageOps(root ? readTree(root, [], /* @__PURE__ */ new Map()) : {}, [op]);
173
- return root ? splice(text, root.range, arrayText(tree, unit, 0)) : `${text.trim() === "" ? "<?php" : text.trimEnd()}\n\nreturn ${arrayText(tree, unit, 0)};\n`;
171
+ return root ? editLiteral(text, root, op, syntax) : `${text.trim() === "" ? "<?php" : text.trimEnd()}\n\nreturn ${literalText(applyMessageOps({}, [op]), syntax)};\n`;
174
172
  }
175
173
  function php() {
176
174
  return {
@@ -187,12 +185,12 @@ function php() {
187
185
  };
188
186
  },
189
187
  edit(text, ops, ctx) {
190
- const unit = unitOf(text, ctx.indent);
188
+ const syntax = syntaxOf(text, ctx.indent);
191
189
  let next = text;
192
- for (const op of ops) next = applyOp(next, op, ctx.path, unit);
190
+ for (const op of ops) next = applyOp(next, op, ctx.path, syntax);
193
191
  return diffText(text, next);
194
192
  },
195
- serialize: (tree, ctx) => `<?php\n\nreturn ${arrayText(tree, unitOf("", ctx.indent), 0)};\n`
193
+ serialize: (tree, ctx) => `<?php\n\nreturn ${literalText(tree, syntaxOf("", ctx.indent))};\n`
196
194
  }]
197
195
  };
198
196
  }
@@ -1,4 +1,4 @@
1
- import { a as splice, c as withBlankLines, i as leafText, n as detectEol, o as splitLines, t as appendLines } from "../shared-BWaXUmwA.mjs";
1
+ import { a as splice, c as withBlankLines, i as leafText, n as detectEol, o as splitLines, t as appendLines } from "../shared-C7ceptwi.mjs";
2
2
  import { diffText, errors, pathId, walkLeaves } from "@x18n/core";
3
3
  //#region src/formats/po/index.ts
4
4
  /** gettext joins msgctxt and msgid with EOT. */
@@ -1,4 +1,4 @@
1
- import { a as splice, i as leafText, o as splitLines, t as appendLines } from "../shared-BWaXUmwA.mjs";
1
+ import { a as splice, i as leafText, o as splitLines, t as appendLines } from "../shared-C7ceptwi.mjs";
2
2
  import { diffText, errors, pathId, walkLeaves } from "@x18n/core";
3
3
  //#region src/formats/properties/index.ts
4
4
  const escapes = {
@@ -1,4 +1,4 @@
1
- import { a as splice, i as leafText, n as detectEol } from "../shared-BWaXUmwA.mjs";
1
+ import { a as splice, i as leafText, n as detectEol } from "../shared-C7ceptwi.mjs";
2
2
  import { diffText, errors, pathId, walkLeaves } from "@x18n/core";
3
3
  //#region src/formats/resx/index.ts
4
4
  const entities = {
@@ -1,4 +1,4 @@
1
- import { s as toLeaf } from "../shared-BWaXUmwA.mjs";
1
+ import { a as splice, s as toLeaf } from "../shared-C7ceptwi.mjs";
2
2
  import { applyTextEdits, diffText, errors, pathId } from "@x18n/core";
3
3
  import { Document, Scalar, isMap, isNode, isScalar, parseDocument, stringify } from "yaml";
4
4
  //#region src/formats/yaml/index.ts
@@ -58,6 +58,26 @@ function scalarText(value, type) {
58
58
  if (type === Scalar.QUOTE_SINGLE) return `'${value.replace(/'/g, "''")}'`;
59
59
  return stringify(value, { lineWidth: 0 }).trimEnd();
60
60
  }
61
+ /**
62
+ * Deletes a pair of a block map by its lines, so the comments around it stay where they are. The comment at the end of
63
+ * its last line goes with it. Undefined when the Document API must do it.
64
+ */
65
+ function removePair(text, doc, path) {
66
+ const map = path.length === 1 ? doc.contents : doc.getIn(path.slice(0, -1), true);
67
+ if (!isMap(map)) return text;
68
+ const pair = map.items.findLast((item) => isScalar(item.key) && String(item.key.value) === path.at(-1));
69
+ const key = pair?.key;
70
+ if (!pair || !isScalar(key)) return text;
71
+ if (map.flow || !key.range || map.items.length === 1 && path.length > 1) return void 0;
72
+ const start = text.lastIndexOf("\n", key.range[0] - 1) + 1;
73
+ if (!/^[ \t]*$/.test(text.slice(start, key.range[0]))) return void 0;
74
+ const valueEnd = isNode(pair.value) && pair.value.range ? pair.value.range[1] : key.range[1];
75
+ const end = text[valueEnd - 1] === "\n" ? valueEnd : valueEnd + /^[ \t]*(?:#.*)?(?:\r?\n)?/.exec(text.slice(valueEnd))[0].length;
76
+ return splice(text, {
77
+ start,
78
+ end
79
+ }, "");
80
+ }
61
81
  function indentOf(text) {
62
82
  return /^( +)\S/m.exec(text)?.[1]?.length ?? 2;
63
83
  }
@@ -95,6 +115,13 @@ function yaml() {
95
115
  }]);
96
116
  continue;
97
117
  }
118
+ if (op.type === "delete") {
119
+ const removed = removePair(next, doc, op.path);
120
+ if (removed !== void 0) {
121
+ next = removed;
122
+ continue;
123
+ }
124
+ }
98
125
  if (doc.contents === null) doc = new Document({});
99
126
  if (op.type === "set") doc.setIn(op.path, op.value);
100
127
  else doc.deleteIn(op.path);
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { recommended } from "./checks.mjs";
2
2
  import { i as pattern } from "./finders-CqTGh4OJ.mjs";
3
3
  import { manifests } from "./manifests.mjs";
4
+ import { reviews } from "./reviews.mjs";
4
5
  import { formats } from "@x18n/builtins/formats";
5
6
  import { presets } from "@x18n/builtins/presets";
6
7
  import { reporters } from "@x18n/builtins/reporters";
@@ -35,6 +36,7 @@ async function createBuiltins() {
35
36
  ...scannerPlugins,
36
37
  regex(),
37
38
  recommended(),
39
+ reviews(),
38
40
  reporterPlugin
39
41
  ],
40
42
  presets