@x18n/core 0.1.8 → 0.1.9

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/index.d.mts CHANGED
@@ -650,6 +650,29 @@ interface ResourceTransform {
650
650
  /** Descriptions that the stored tree holds beside the messages, such as `{ "name": { "message": "...", "description": "..." } }`, keyed by `pathId(storedPath)`. */
651
651
  readonly describe?: ((tree: MessageTree, ctx: ResourceTransformContext) => ReadonlyMap<string, string>) | undefined;
652
652
  }
653
+ /** A review edit of a plan, after it is applied to the review data. */
654
+ interface ReviewEdit {
655
+ /** The review type of a new comment, or the op that resolved or applied one. */
656
+ readonly type: ReviewType | "resolve-review" | "apply-suggestion";
657
+ readonly key: KeyString;
658
+ readonly locale: LocaleCode;
659
+ /** The new comment, or the comment that was resolved or applied. */
660
+ readonly comment: ReviewComment;
661
+ /** The thread of the message after the edit, oldest first. */
662
+ readonly comments: readonly ReviewComment[];
663
+ }
664
+ interface ReviewEditContext {
665
+ /** A message before the plan. */
666
+ readonly message: (key: KeyString, locale: LocaleCode) => Message | undefined;
667
+ readonly resource: (id: ResourceId) => Resource | undefined;
668
+ /** The text of a project file as the plan leaves it so far, including earlier writes of `reviewEdit` hooks. */
669
+ readonly text: (path: string) => string | undefined;
670
+ }
671
+ /** The full new content of a file, which core puts into the plan. */
672
+ interface FileWrite {
673
+ readonly path: string;
674
+ readonly content: string;
675
+ }
653
676
  interface PromptDraft {
654
677
  readonly rules: string[];
655
678
  readonly notes: string[];
@@ -688,6 +711,8 @@ interface Plugin {
688
711
  readonly derivedKeys?: readonly string[] | undefined;
689
712
  readonly transformResource?: ResourceTransform | undefined;
690
713
  readonly translatePrompt?: ((draft: PromptDraft, ctx: PromptContext) => void) | undefined;
714
+ /** Called for each review edit of a plan, in op order. The writes it returns go into the same plan. */
715
+ readonly reviewEdit?: ((edit: ReviewEdit, ctx: ReviewEditContext) => readonly FileWrite[] | undefined) | undefined;
691
716
  /** Display data for a locale code. The first plugin that returns a value wins. */
692
717
  readonly localeInfo?: ((code: LocaleCode) => Partial<LocaleInfo> | undefined) | undefined;
693
718
  }
@@ -917,7 +942,7 @@ interface SerializableConfig {
917
942
  } | undefined;
918
943
  /** Turn every warning into an error. */
919
944
  readonly strict?: boolean | undefined;
920
- /** Keys (globs) excluded from all checks. */
945
+ /** Keys (globs) excluded from all checks. `x18n.config.ts` also takes regular expressions. */
921
946
  readonly ignoreKeys?: readonly string[] | undefined;
922
947
  readonly glossary?: readonly GlossaryEntry[] | undefined;
923
948
  /** History providers for stale translations. `false` turns every provider off. Options of a provider sit under its key, such as `git: { maxDepth, since }` (or `git: false`). */
@@ -940,9 +965,13 @@ interface HistorySettings extends HistoryOptions {
940
965
  }
941
966
  type PresetOption = Preset | PluginSpecifier | false | undefined;
942
967
  type PluginOption = Plugin | Preset | PluginSpecifier | false | undefined;
968
+ /** A key glob (`*` inside a segment, `**` across segments), or a regular expression tested against the whole key. */
969
+ type KeyPattern = string | RegExp;
943
970
  /** The config accepted by `defineConfig`: the serializable part plus plugin objects. */
944
- interface UserConfig extends Omit<SerializableConfig, "presets" | "plugins" | "checks" | "history"> {
971
+ interface UserConfig extends Omit<SerializableConfig, "presets" | "plugins" | "checks" | "history" | "ignoreKeys"> {
945
972
  readonly presets?: "auto" | readonly PresetOption[] | undefined;
973
+ /** Keys excluded from all checks: globs, or regular expressions tested against the whole key (`ns:a.b` when namespaced). */
974
+ readonly ignoreKeys?: readonly KeyPattern[] | undefined;
946
975
  /** History providers for stale translations: `false` turns them off; provider options such as `git` sit under their key; `providers` adds providers asked first. */
947
976
  readonly history?: boolean | (HistoryOptions & {
948
977
  readonly providers?: readonly HistoryProvider[] | undefined;
@@ -998,7 +1027,7 @@ interface ResolvedConfig {
998
1027
  readonly [checkId: string]: CheckSetting;
999
1028
  };
1000
1029
  readonly strict: boolean;
1001
- readonly ignoreKeys: readonly string[];
1030
+ readonly ignoreKeys: readonly KeyPattern[];
1002
1031
  readonly glossary: readonly GlossaryEntry[];
1003
1032
  readonly history: false | HistorySettings;
1004
1033
  readonly review: {
@@ -1283,8 +1312,11 @@ export declare function replaceKeyPrefix(key: KeyString, from: KeyString, to: Ke
1283
1312
  * `{a,b}` alternates, `[...]` is a character class.
1284
1313
  */
1285
1314
  export declare function globToRegExp(glob: string, separator?: string): RegExp;
1286
- /** Matcher over key globs: `*` stays inside a segment, `**` spans segments. With literal keys (`separator: false`) `*` spans the whole key. */
1287
- export declare function keyMatcher(globs: readonly string[], separator?: KeySeparator): (key: KeyString) => boolean;
1315
+ /**
1316
+ * Matcher over key patterns. In a glob `*` stays inside a segment and `**` spans segments; with literal keys
1317
+ * (`separator: false`) `*` spans the whole key. A regular expression is tested against the whole key as is.
1318
+ */
1319
+ export declare function keyMatcher(patterns: readonly KeyPattern[], separator?: KeySeparator): (key: KeyString) => boolean;
1288
1320
  /** Matcher over posix file paths. */
1289
1321
  export declare function pathMatcher(globs: readonly string[]): (path: string) => boolean;
1290
1322
  //#endregion
@@ -1914,4 +1946,4 @@ export declare function rangeContains(range: TextRange, offset: number): boolean
1914
1946
  /** Fast non-cryptographic string hash (cyrb53), as hex. */
1915
1947
  export declare function hashText(text: string): string;
1916
1948
  //#endregion
1917
- export type { Analysis, Answers, ApplyResult, ArgSource, BaselineSummary, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FinderContext, Fix, Format, FormatContext, FormatEntry, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeptUnusedKey, KeyContext, KeyFile, KeyId, KeyInfo, KeyQuery, KeySeparator, KeyString, KeyTarget, LazyFormat, LazyScanner, LibraryOptions, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageState, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParamsSource, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginLoader, PluginOption, PluginRegistry, PluginSpecifier, PluralForm, Preset, PresetDetection, PresetEntry, PresetExplanation, PresetFactory, PresetLoader, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptIssue, PromptItem, PromptOptions, RegexScannerSpec, RelatedValue, ReportInput, ReportSummary, Reporter, ReporterContext, ReporterLoader, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ReviewAuthor, ReviewComment, ReviewData, ReviewFile, ReviewFilter, ReviewOptions, ReviewState, ReviewStore, ReviewThread, ReviewType, ScanContext, ScanResult, Scanner, ScannerEntry, SchemaIssue, Scope, SearchMatch, SearchOptions, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, ThreadComment, TreeLeaf, UnusedDeletion, UnusedKeyDeletion, UnusedQuery, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
1949
+ export type { Analysis, Answers, ApplyResult, ArgSource, BaselineSummary, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FileWrite, FinderContext, Fix, Format, FormatContext, FormatEntry, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeptUnusedKey, KeyContext, KeyFile, KeyId, KeyInfo, KeyPattern, KeyQuery, KeySeparator, KeyString, KeyTarget, LazyFormat, LazyScanner, LibraryOptions, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageState, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParamsSource, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginLoader, PluginOption, PluginRegistry, PluginSpecifier, PluralForm, Preset, PresetDetection, PresetEntry, PresetExplanation, PresetFactory, PresetLoader, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptIssue, PromptItem, PromptOptions, RegexScannerSpec, RelatedValue, ReportInput, ReportSummary, Reporter, ReporterContext, ReporterLoader, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ReviewAuthor, ReviewComment, ReviewData, ReviewEdit, ReviewEditContext, ReviewFile, ReviewFilter, ReviewOptions, ReviewState, ReviewStore, ReviewThread, ReviewType, ScanContext, ScanResult, Scanner, ScannerEntry, SchemaIssue, Scope, SearchMatch, SearchOptions, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, ThreadComment, TreeLeaf, UnusedDeletion, UnusedKeyDeletion, UnusedQuery, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
package/dist/index.mjs CHANGED
@@ -379,10 +379,13 @@ function globToRegExp(glob, separator = "/") {
379
379
  }
380
380
  return new RegExp(`^${out}$`);
381
381
  }
382
- /** Matcher over key globs: `*` stays inside a segment, `**` spans segments. With literal keys (`separator: false`) `*` spans the whole key. */
383
- function keyMatcher(globs, separator = ".") {
384
- if (globs.length === 0) return () => false;
385
- const regexes = globs.map((glob) => globToRegExp(glob, separator === false ? "\0" : separator));
382
+ /**
383
+ * Matcher over key patterns. In a glob `*` stays inside a segment and `**` spans segments; with literal keys
384
+ * (`separator: false`) `*` spans the whole key. A regular expression is tested against the whole key as is.
385
+ */
386
+ function keyMatcher(patterns, separator = ".") {
387
+ if (patterns.length === 0) return () => false;
388
+ const regexes = patterns.map((pattern) => pattern instanceof RegExp ? new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, "")) : globToRegExp(pattern, separator === false ? "\0" : separator));
386
389
  return (key) => regexes.some((re) => re.test(key));
387
390
  }
388
391
  /** Matcher over posix file paths. */
@@ -621,11 +624,15 @@ function deleteIn(tree, path) {
621
624
  delete parents[i - 1][path[i - 1]];
622
625
  }
623
626
  }
627
+ /** Applies ops to the tree itself. Parent objects left empty are removed. */
628
+ function applyMessageOpsInPlace(tree, ops) {
629
+ for (const op of ops) if (op.type === "set") setIn(tree, op.path, op.value);
630
+ else deleteIn(tree, op.path);
631
+ }
624
632
  /** Applies ops to a copy of the tree. Parent objects left empty are removed. */
625
633
  function applyMessageOps(tree, ops) {
626
634
  const out = cloneTree(tree);
627
- for (const op of ops) if (op.type === "set") setIn(out, op.path, op.value);
628
- else deleteIn(out, op.path);
635
+ applyMessageOpsInPlace(out, ops);
629
636
  return out;
630
637
  }
631
638
  /** Sorts keys at every level. */
@@ -1736,6 +1743,10 @@ function nodeAt(tree, path) {
1736
1743
  }
1737
1744
  return node;
1738
1745
  }
1746
+ function leafAt(tree, path) {
1747
+ const node = nodeAt(tree, path);
1748
+ return isMessageLeaf(node) ? node : void 0;
1749
+ }
1739
1750
  /** True for flat files (`{ "a.b": "x" }`), false for nested ones, undefined when it cannot tell. */
1740
1751
  function isFlatTree(tree, separator) {
1741
1752
  const entries = Object.entries(tree);
@@ -1795,6 +1806,7 @@ var PlanBuilder = class {
1795
1806
  config;
1796
1807
  /** Source values set by this plan, for the source records of targets set after them. */
1797
1808
  plannedSource = /* @__PURE__ */ new Map();
1809
+ reviewEdits = [];
1798
1810
  draft;
1799
1811
  constructor(options, state, answers) {
1800
1812
  this.options = options;
@@ -1822,7 +1834,7 @@ var PlanBuilder = class {
1822
1834
  if (!entry) {
1823
1835
  entry = {
1824
1836
  target,
1825
- tree: target.state?.rawTree ?? {},
1837
+ tree: cloneTree(target.state?.rawTree ?? {}),
1826
1838
  ops: [],
1827
1839
  keyRenames: [],
1828
1840
  formatOnly: false
@@ -1866,7 +1878,8 @@ var PlanBuilder = class {
1866
1878
  const stored = this.options.toStored(entry.target, relative);
1867
1879
  const separator = this.separatorOf(entry.target);
1868
1880
  if (separator === false) return stored;
1869
- return stored.length === relative.length && (isFlatTree(entry.tree, separator) ?? this.config.keyStyle === "flat") ? [stored.join(separator)] : stored;
1881
+ if (stored.length === relative.length) entry.flat ??= isFlatTree(entry.tree, separator);
1882
+ return stored.length === relative.length && (entry.flat ?? this.config.keyStyle === "flat") ? [stored.join(separator)] : stored;
1870
1883
  }
1871
1884
  createTarget(key, locale) {
1872
1885
  const { resources, sourceLocale, namespaced } = this.state;
@@ -1979,18 +1992,30 @@ var PlanBuilder = class {
1979
1992
  /** ---- Message ops ---- */
1980
1993
  pushOp(entry, op) {
1981
1994
  entry.ops.push(op);
1982
- entry.tree = applyMessageOps(entry.tree, [op]);
1995
+ this.applyOps(entry, [op]);
1996
+ }
1997
+ /** Applies ops to the entry's tree in place; drops `flat` when a top-level object appears or goes, or a top-level key goes. */
1998
+ applyOps(entry, ops) {
1999
+ for (const op of ops) {
2000
+ const top = op.path[0];
2001
+ const before = entry.tree[top];
2002
+ applyMessageOpsInPlace(entry.tree, [op]);
2003
+ const after = entry.tree[top];
2004
+ if ((before !== void 0 && !isMessageLeaf(before)) !== (after !== void 0 && !isMessageLeaf(after)) || before !== void 0 && after === void 0) entry.flat = void 0;
2005
+ }
1983
2006
  }
1984
2007
  setValue(key, locale, value) {
1985
2008
  this.touched.add(key);
1986
2009
  const existing = this.options.message(key, locale);
1987
2010
  if (existing) {
1988
- this.pushOp(this.workFor(this.targetOf(this.writable(existing.resource))), {
2011
+ const entry = this.workFor(this.targetOf(this.writable(existing.resource)));
2012
+ const before = leafAt(entry.tree, existing.storedPath);
2013
+ this.pushOp(entry, {
1989
2014
  type: "set",
1990
2015
  path: existing.storedPath,
1991
2016
  value
1992
2017
  });
1993
- return;
2018
+ return before;
1994
2019
  }
1995
2020
  const target = this.pickTarget(key, locale);
1996
2021
  if (target.state) this.writable(target.state.resource.id);
@@ -2003,6 +2028,7 @@ var PlanBuilder = class {
2003
2028
  path: stored,
2004
2029
  value
2005
2030
  });
2031
+ return isMessageLeaf(occupied) ? occupied : void 0;
2006
2032
  }
2007
2033
  deleteMessage(m) {
2008
2034
  const s = this.writable(m.resource);
@@ -2046,7 +2072,7 @@ var PlanBuilder = class {
2046
2072
  text: last
2047
2073
  });
2048
2074
  const leaves = [...walkLeaves(node, stored)];
2049
- entry.tree = applyMessageOps(entry.tree, [...leaves.map((leaf) => ({
2075
+ this.applyOps(entry, [...leaves.map((leaf) => ({
2050
2076
  type: "delete",
2051
2077
  path: leaf.path
2052
2078
  })), ...leaves.map((leaf) => ({
@@ -2061,45 +2087,54 @@ var PlanBuilder = class {
2061
2087
  moveKey(from, to, renamed = /* @__PURE__ */ new Set()) {
2062
2088
  this.touched.add(from);
2063
2089
  this.touched.add(to);
2064
- for (const [locale, list] of this.options.messages(from) ?? []) {
2065
- const m = list[0];
2090
+ for (const list of this.options.messages(from)?.values() ?? []) for (const m of [...list].reverse()) {
2066
2091
  if (renamed.has(m.resource)) continue;
2067
- const s = this.writable(m.resource);
2068
- const relative = this.relativePath(to, s.resource);
2069
- const own = this.options.resolveKey(to, locale);
2070
- if (!relative || own && own.path !== s.resource.path) {
2071
- this.deleteMessage(m);
2072
- this.setValue(to, locale, m.value);
2073
- continue;
2074
- }
2075
- const entry = this.workFor(this.targetOf(s));
2076
- const separator = this.separatorOf(s.resource);
2077
- const flat = separator !== false && m.storedPath.length === 1 && m.storedPath[0].includes(separator);
2078
- const next = flat ? [relative.join(separator)] : this.options.toStored(s.resource, relative);
2079
- const sameParent = next.length === m.storedPath.length && next.slice(0, -1).every((segment, i) => segment === m.storedPath[i]);
2080
- const last = next.at(-1);
2081
- if (sameParent && m.range && (flat ? safeFlat : safeSegment).test(last)) {
2082
- entry.keyRenames.push({
2083
- range: m.range.key,
2084
- text: last
2085
- });
2086
- entry.tree = applyMessageOps(entry.tree, [{
2087
- type: "delete",
2088
- path: m.storedPath
2089
- }, {
2090
- type: "set",
2091
- path: next,
2092
- value: m.value
2093
- }]);
2094
- continue;
2092
+ const replaced = this.moveMessage(m, to);
2093
+ if (replaced !== void 0 && replaced !== m.value) {
2094
+ const path = this.stateById.get(m.resource)?.resource.path ?? m.resource;
2095
+ this.warnings.push(`Copies of "${from}" in ${m.locale} have different values; "${to}" keeps the one from ${path}.`);
2095
2096
  }
2097
+ }
2098
+ }
2099
+ moveMessage(m, to) {
2100
+ const s = this.writable(m.resource);
2101
+ const relative = this.relativePath(to, s.resource);
2102
+ const own = this.options.resolveKey(to, m.locale);
2103
+ if (!relative || own && own.path !== s.resource.path) {
2096
2104
  this.deleteMessage(m);
2097
- this.pushOp(entry, {
2105
+ return this.setValue(to, m.locale, m.value);
2106
+ }
2107
+ const entry = this.workFor(this.targetOf(s));
2108
+ const separator = this.separatorOf(s.resource);
2109
+ const flat = separator !== false && m.storedPath.length === 1 && m.storedPath[0].includes(separator);
2110
+ const next = flat ? [relative.join(separator)] : this.options.toStored(s.resource, relative);
2111
+ const sameParent = next.length === m.storedPath.length && next.slice(0, -1).every((segment, i) => segment === m.storedPath[i]);
2112
+ const last = next.at(-1);
2113
+ if (sameParent && m.range && (flat ? safeFlat : safeSegment).test(last)) {
2114
+ const replaced = leafAt(entry.tree, next);
2115
+ entry.keyRenames.push({
2116
+ range: m.range.key,
2117
+ text: last
2118
+ });
2119
+ this.applyOps(entry, [{
2120
+ type: "delete",
2121
+ path: m.storedPath
2122
+ }, {
2098
2123
  type: "set",
2099
- path: this.storedPathFor(entry, relative),
2124
+ path: next,
2100
2125
  value: m.value
2101
- });
2126
+ }]);
2127
+ return replaced;
2102
2128
  }
2129
+ this.deleteMessage(m);
2130
+ const path = this.storedPathFor(entry, relative);
2131
+ const replaced = leafAt(entry.tree, path);
2132
+ this.pushOp(entry, {
2133
+ type: "set",
2134
+ path,
2135
+ value: m.value
2136
+ });
2137
+ return replaced;
2103
2138
  }
2104
2139
  /** The raw text a usage needs to refer to `next`, given its scope. */
2105
2140
  rawFor(usage, next) {
@@ -2239,6 +2274,17 @@ var PlanBuilder = class {
2239
2274
  ...op.review === "suggestion" && { suggestion: op.suggestion },
2240
2275
  ...op.review === "approve" && current !== void 0 && { value: valueText(current) }
2241
2276
  });
2277
+ this.reviewEdited(draft, op.review, op.id);
2278
+ }
2279
+ reviewEdited(draft, type, id) {
2280
+ const { key, locale, comment } = findReview(draft.data, id);
2281
+ this.reviewEdits.push({
2282
+ type,
2283
+ key,
2284
+ locale,
2285
+ comment,
2286
+ comments: draft.data.threads[key][locale]
2287
+ });
2242
2288
  }
2243
2289
  applySuggestion(id) {
2244
2290
  const draft = this.requireReviews();
@@ -2249,6 +2295,7 @@ var PlanBuilder = class {
2249
2295
  this.setValue(key, locale, comment.suggestion);
2250
2296
  this.recordSource(key, locale, true);
2251
2297
  draft.resolve(id);
2298
+ this.reviewEdited(draft, "apply-suggestion", id);
2252
2299
  }
2253
2300
  resolveReview(id) {
2254
2301
  const draft = this.requireReviews();
@@ -2256,6 +2303,7 @@ var PlanBuilder = class {
2256
2303
  if (!found) throw errors.X18N5004({ id });
2257
2304
  this.touched.add(found.key);
2258
2305
  draft.resolve(id);
2306
+ this.reviewEdited(draft, "resolve-review", id);
2259
2307
  }
2260
2308
  markFresh(op) {
2261
2309
  this.requireReviews();
@@ -2415,6 +2463,7 @@ var PlanBuilder = class {
2415
2463
  });
2416
2464
  texts.set(path, text);
2417
2465
  }
2466
+ this.reviewWrites(changes, texts);
2418
2467
  const review = this.reviewChange();
2419
2468
  if (review) {
2420
2469
  changes.push(review);
@@ -2425,6 +2474,41 @@ var PlanBuilder = class {
2425
2474
  texts
2426
2475
  };
2427
2476
  }
2477
+ /** Runs the `reviewEdit` hooks and puts their writes into the changes. */
2478
+ reviewWrites(changes, texts) {
2479
+ const hooks = this.options.registry.plugins.flatMap((plugin) => plugin.reviewEdit ? [plugin.reviewEdit] : []);
2480
+ if (hooks.length === 0 || this.reviewEdits.length === 0) return;
2481
+ const written = /* @__PURE__ */ new Map();
2482
+ const text = (path) => {
2483
+ if (written.has(path)) return written.get(path);
2484
+ const change = changes.find((c) => c.path === path);
2485
+ if (!change) return this.options.text(path);
2486
+ return change.type === "create" ? change.content : change.type === "edit" ? applyTextEdits(texts.get(path) ?? "", change.edits) : void 0;
2487
+ };
2488
+ const ctx = {
2489
+ message: this.options.message,
2490
+ resource: (id) => this.stateById.get(id)?.resource,
2491
+ text
2492
+ };
2493
+ for (const edit of this.reviewEdits) for (const hook of hooks) for (const write of hook(edit, ctx) ?? []) written.set(write.path, write.content);
2494
+ for (const [path, content] of written) {
2495
+ const index = changes.findIndex((c) => c.path === path);
2496
+ const before = index === -1 ? this.options.text(path) : texts.get(path);
2497
+ if (index === -1 && before === content) continue;
2498
+ const change = before === void 0 ? {
2499
+ type: "create",
2500
+ path,
2501
+ content
2502
+ } : {
2503
+ type: "edit",
2504
+ path,
2505
+ edits: diffText(before, content)
2506
+ };
2507
+ if (index === -1) changes.push(change);
2508
+ else changes[index] = change;
2509
+ texts.set(path, before);
2510
+ }
2511
+ }
2428
2512
  reviewChange() {
2429
2513
  const reviews = this.state.reviews;
2430
2514
  if (!reviews || !this.draft?.changed) return void 0;
@@ -2443,6 +2527,7 @@ var PlanBuilder = class {
2443
2527
  };
2444
2528
  function createPlanner(options) {
2445
2529
  const texts = /* @__PURE__ */ new Map();
2530
+ let textsRevision;
2446
2531
  /** Gives new reviews an id and a time, so planning again (after decisions) writes the same comments. */
2447
2532
  function withReviewIds(ops, data) {
2448
2533
  const used = /* @__PURE__ */ new Set();
@@ -2506,6 +2591,10 @@ function createPlanner(options) {
2506
2591
  return {
2507
2592
  plan(ops, answers = {}) {
2508
2593
  const result = planWith(ops, answers);
2594
+ if (result.plan.revision !== textsRevision) {
2595
+ texts.clear();
2596
+ textsRevision = result.plan.revision;
2597
+ }
2509
2598
  texts.set(result.plan.id, result.texts);
2510
2599
  return result.plan;
2511
2600
  },
@@ -3832,7 +3921,12 @@ function defaultDraft() {
3832
3921
  };
3833
3922
  }
3834
3923
  function serializablePart(config) {
3835
- const { presets, plugins, pluginConfigs, history, ...rest } = config;
3924
+ const { presets, plugins, pluginConfigs, history, ignoreKeys: patterns, ...fields } = config;
3925
+ const ignoreKeys = patterns?.filter((pattern) => !(pattern instanceof RegExp));
3926
+ const rest = ignoreKeys === void 0 ? fields : {
3927
+ ...fields,
3928
+ ignoreKeys
3929
+ };
3836
3930
  if (typeof history !== "object") return {
3837
3931
  ...rest,
3838
3932
  history
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x18n/core",
3
3
  "type": "module",
4
- "version": "0.1.8",
4
+ "version": "0.1.9",
5
5
  "description": "Runtime-neutral engine of x18n: domain model, plugin API and project analysis",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",