@saykit/config 0.5.0 → 0.6.1

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.
@@ -1,18 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_hash = require("../../hash-51ce8YiG.cjs");
3
- //#region src/features/messages/identifier.ts
4
- const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
5
- function assignSequenceIdentifiers(message, sequence = { current: 0 }) {
6
- if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
7
- if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = `${sequence.current++}`;
8
- }
9
- if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) assignSequenceIdentifiers(child, sequence);
10
- if (message instanceof ChoiceMessage) for (const branch of message.branches) {
11
- if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = `${sequence.current++}`;
12
- assignSequenceIdentifiers(branch.value, sequence);
13
- }
14
- }
15
- //#endregion
16
3
  //#region src/features/messages/types.ts
17
4
  var Base = class {
18
5
  toICUString() {
@@ -81,6 +68,103 @@ var CompositeMessage = class extends Base {
81
68
  }
82
69
  };
83
70
  //#endregion
71
+ //#region src/features/messages/identifier.ts
72
+ const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
73
+ /**
74
+ * An ICU case, either an exact value or a key. ICU reserves its own pattern
75
+ * syntax, so a key carries no punctuation and no whitespace.
76
+ */
77
+ const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
78
+ /**
79
+ * The ICU case a branch is written as. A number names an exact value rather
80
+ * than a key, and only `plural` and `ordinal` are allowed to select on one.
81
+ *
82
+ * Digits are read literally rather than coerced, because everything JavaScript
83
+ * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
84
+ * `=0`, which would pass for a key that selects zero and quietly leave the
85
+ * author's own key out of the catalogue. Anything else stays a key, where the
86
+ * whitespace or punctuation that made it numeric-looking is caught.
87
+ */
88
+ function getBranchCase(identifier) {
89
+ const key = String(identifier);
90
+ return /^\d+$/u.test(key) ? `=${+key}` : key;
91
+ }
92
+ /**
93
+ * Reject a branch key ICU cannot express, while the key is still attached to a
94
+ * file and a line.
95
+ *
96
+ * A hyphenated string union is ordinary application code and typechecks,
97
+ * builds, and extracts to a catalogue entry that looks perfectly normal — the
98
+ * only sign of trouble is a parse error at format time, in a message whose
99
+ * source is long gone.
100
+ */
101
+ function validateBranchIdentifier(kind, identifier) {
102
+ if (typeof identifier !== "string") return;
103
+ const branch = getBranchCase(identifier);
104
+ if (!BRANCH_PATTERN.test(branch)) {
105
+ const suggestion = suggestBranchIdentifier(identifier);
106
+ throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
107
+ }
108
+ if (kind === "select" && branch.startsWith("=")) throw new Error(`Invalid select branch key '${identifier}', a number selects an exact value, which only 'plural' and 'ordinal' accept`);
109
+ }
110
+ /**
111
+ * The nearest identifier-safe form of a key, so the error names the fix as well
112
+ * as the problem. The constraint comes from ICU rather than from anything the
113
+ * author wrote, and camel case is how the rest of the codebase already spells a
114
+ * name of more than one word.
115
+ */
116
+ function suggestBranchIdentifier(identifier) {
117
+ const suggestion = identifier.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word, index) => index === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
118
+ if (!BRANCH_PATTERN.test(suggestion) || !Number.isNaN(+suggestion)) return void 0;
119
+ return suggestion;
120
+ }
121
+ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalent = () => false) {
122
+ const reserved = collectAssignedIdentifiers(message, equivalent);
123
+ function next() {
124
+ let identifier = `${sequence.current++}`;
125
+ while (reserved.has(identifier)) identifier = `${sequence.current++}`;
126
+ return identifier;
127
+ }
128
+ function walk(message) {
129
+ if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
130
+ if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
131
+ }
132
+ if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
133
+ if (message instanceof ChoiceMessage) for (const branch of message.branches) {
134
+ if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = next();
135
+ walk(branch.value);
136
+ }
137
+ }
138
+ walk(message);
139
+ }
140
+ /**
141
+ * Collect the identifiers already assigned before this pass runs, so generated
142
+ * sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
143
+ *
144
+ * A name is claimed by what produced it, not by the name alone. Two that differ
145
+ * each compile to their own prop, and a translator moving them around a sentence
146
+ * has to be able to tell them apart, so they are a build error. Repeats are fine
147
+ * when nothing distinguishes them: the same variable interpolated twice is one
148
+ * value, and two identical elements are one tag.
149
+ */
150
+ function collectAssignedIdentifiers(message, equivalent) {
151
+ const tags = /* @__PURE__ */ new Map();
152
+ const values = /* @__PURE__ */ new Map();
153
+ function claim(claimed, identifier, expression, conflict) {
154
+ if (claimed.has(identifier) && !equivalent(claimed.get(identifier), expression)) throw new Error(conflict);
155
+ claimed.set(identifier, expression);
156
+ }
157
+ function walk(message) {
158
+ if (message instanceof ElementMessage && typeof message.identifier === "string") claim(tags, message.identifier, message.expression, `Duplicate element tag '${message.identifier}', give each element in a message its own tag unless they are identical`);
159
+ if ((message instanceof ArgumentMessage || message instanceof ChoiceMessage) && typeof message.identifier === "string") claim(values, message.identifier, message.expression, `Duplicate placeholder name '${message.identifier}', give each value in a message its own name unless they are identical`);
160
+ if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
161
+ if (message instanceof ChoiceMessage) for (const branch of message.branches) walk(branch.value);
162
+ }
163
+ walk(message);
164
+ for (const tag of tags.keys()) if (values.has(tag)) throw new Error(`Element tag '${tag}' collides with an argument of the same name`);
165
+ return /* @__PURE__ */ new Set([...tags.keys(), ...values.keys()]);
166
+ }
167
+ //#endregion
84
168
  //#region src/features/messages/convert.ts
85
169
  function convertMessageToIcu(message) {
86
170
  function internalConvertMessageToIcu(message) {
@@ -88,12 +172,13 @@ function convertMessageToIcu(message) {
88
172
  case message instanceof LiteralMessage: return String(message.text);
89
173
  case message instanceof ArgumentMessage: return `{${String(message.identifier)}}`;
90
174
  case message instanceof ElementMessage: {
175
+ if (message.children.length === 0) return `<${String(message.identifier)}/>`;
91
176
  const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
92
177
  return `<${String(message.identifier)}>${children}</${String(message.identifier)}>`;
93
178
  }
94
179
  case message instanceof ChoiceMessage: {
95
180
  const branches = message.branches.map(({ identifier, value }) => ({
96
- identifier: Number.isNaN(+String(identifier)) ? String(identifier) : `=${+String(identifier)}`,
181
+ identifier: getBranchCase(identifier),
97
182
  value: internalConvertMessageToIcu(value)
98
183
  })).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
99
184
  const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
@@ -115,3 +200,5 @@ exports.LiteralMessage = LiteralMessage;
115
200
  exports.assignSequenceIdentifiers = assignSequenceIdentifiers;
116
201
  exports.convertMessageToIcu = convertMessageToIcu;
117
202
  exports.generateHash = require_hash.generateHash;
203
+ exports.getBranchCase = getBranchCase;
204
+ exports.validateBranchIdentifier = validateBranchIdentifier;
@@ -1,8 +1,35 @@
1
1
  //#region src/features/messages/identifier.d.ts
2
2
  declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
3
+ /**
4
+ * The ICU case a branch is written as. A number names an exact value rather
5
+ * than a key, and only `plural` and `ordinal` are allowed to select on one.
6
+ *
7
+ * Digits are read literally rather than coerced, because everything JavaScript
8
+ * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
9
+ * `=0`, which would pass for a key that selects zero and quietly leave the
10
+ * author's own key out of the catalogue. Anything else stays a key, where the
11
+ * whitespace or punctuation that made it numeric-looking is caught.
12
+ */
13
+ declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
14
+ /**
15
+ * Reject a branch key ICU cannot express, while the key is still attached to a
16
+ * file and a line.
17
+ *
18
+ * A hyphenated string union is ordinary application code and typechecks,
19
+ * builds, and extracts to a catalogue entry that looks perfectly normal — the
20
+ * only sign of trouble is a parse error at format time, in a message whose
21
+ * source is long gone.
22
+ */
23
+ declare function validateBranchIdentifier(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): void;
24
+ /**
25
+ * Decides whether two placeholders sharing a name are the same placeholder, and
26
+ * so may share it. Only the syntax that produced them can answer that, so the
27
+ * caller supplies the comparison; by default nothing is interchangeable.
28
+ */
29
+ type PlaceholderEquivalence = (a: any, b: any) => boolean;
3
30
  declare function assignSequenceIdentifiers(message: Message, sequence?: {
4
31
  current: number;
5
- }): void;
32
+ }, equivalent?: PlaceholderEquivalence): void;
6
33
  //#endregion
7
34
  //#region src/features/messages/types.d.ts
8
35
  declare abstract class Base {
@@ -60,4 +87,4 @@ declare function convertMessageToIcu(message: Message): string;
60
87
  //#region src/features/messages/hash.d.ts
61
88
  declare function generateHash(input: string, context?: string): string;
62
89
  //#endregion
63
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, assignSequenceIdentifiers, convertMessageToIcu, generateHash };
90
+ export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
@@ -1,8 +1,35 @@
1
1
  //#region src/features/messages/identifier.d.ts
2
2
  declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
3
+ /**
4
+ * The ICU case a branch is written as. A number names an exact value rather
5
+ * than a key, and only `plural` and `ordinal` are allowed to select on one.
6
+ *
7
+ * Digits are read literally rather than coerced, because everything JavaScript
8
+ * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
9
+ * `=0`, which would pass for a key that selects zero and quietly leave the
10
+ * author's own key out of the catalogue. Anything else stays a key, where the
11
+ * whitespace or punctuation that made it numeric-looking is caught.
12
+ */
13
+ declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
14
+ /**
15
+ * Reject a branch key ICU cannot express, while the key is still attached to a
16
+ * file and a line.
17
+ *
18
+ * A hyphenated string union is ordinary application code and typechecks,
19
+ * builds, and extracts to a catalogue entry that looks perfectly normal — the
20
+ * only sign of trouble is a parse error at format time, in a message whose
21
+ * source is long gone.
22
+ */
23
+ declare function validateBranchIdentifier(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): void;
24
+ /**
25
+ * Decides whether two placeholders sharing a name are the same placeholder, and
26
+ * so may share it. Only the syntax that produced them can answer that, so the
27
+ * caller supplies the comparison; by default nothing is interchangeable.
28
+ */
29
+ type PlaceholderEquivalence = (a: any, b: any) => boolean;
3
30
  declare function assignSequenceIdentifiers(message: Message, sequence?: {
4
31
  current: number;
5
- }): void;
32
+ }, equivalent?: PlaceholderEquivalence): void;
6
33
  //#endregion
7
34
  //#region src/features/messages/types.d.ts
8
35
  declare abstract class Base {
@@ -60,4 +87,4 @@ declare function convertMessageToIcu(message: Message): string;
60
87
  //#region src/features/messages/hash.d.ts
61
88
  declare function generateHash(input: string, context?: string): string;
62
89
  //#endregion
63
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, assignSequenceIdentifiers, convertMessageToIcu, generateHash };
90
+ export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
@@ -1,17 +1,4 @@
1
1
  import { t as generateHash } from "../../hash-DvzpieJD.mjs";
2
- //#region src/features/messages/identifier.ts
3
- const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
4
- function assignSequenceIdentifiers(message, sequence = { current: 0 }) {
5
- if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
6
- if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = `${sequence.current++}`;
7
- }
8
- if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) assignSequenceIdentifiers(child, sequence);
9
- if (message instanceof ChoiceMessage) for (const branch of message.branches) {
10
- if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = `${sequence.current++}`;
11
- assignSequenceIdentifiers(branch.value, sequence);
12
- }
13
- }
14
- //#endregion
15
2
  //#region src/features/messages/types.ts
16
3
  var Base = class {
17
4
  toICUString() {
@@ -80,6 +67,103 @@ var CompositeMessage = class extends Base {
80
67
  }
81
68
  };
82
69
  //#endregion
70
+ //#region src/features/messages/identifier.ts
71
+ const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
72
+ /**
73
+ * An ICU case, either an exact value or a key. ICU reserves its own pattern
74
+ * syntax, so a key carries no punctuation and no whitespace.
75
+ */
76
+ const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
77
+ /**
78
+ * The ICU case a branch is written as. A number names an exact value rather
79
+ * than a key, and only `plural` and `ordinal` are allowed to select on one.
80
+ *
81
+ * Digits are read literally rather than coerced, because everything JavaScript
82
+ * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
83
+ * `=0`, which would pass for a key that selects zero and quietly leave the
84
+ * author's own key out of the catalogue. Anything else stays a key, where the
85
+ * whitespace or punctuation that made it numeric-looking is caught.
86
+ */
87
+ function getBranchCase(identifier) {
88
+ const key = String(identifier);
89
+ return /^\d+$/u.test(key) ? `=${+key}` : key;
90
+ }
91
+ /**
92
+ * Reject a branch key ICU cannot express, while the key is still attached to a
93
+ * file and a line.
94
+ *
95
+ * A hyphenated string union is ordinary application code and typechecks,
96
+ * builds, and extracts to a catalogue entry that looks perfectly normal — the
97
+ * only sign of trouble is a parse error at format time, in a message whose
98
+ * source is long gone.
99
+ */
100
+ function validateBranchIdentifier(kind, identifier) {
101
+ if (typeof identifier !== "string") return;
102
+ const branch = getBranchCase(identifier);
103
+ if (!BRANCH_PATTERN.test(branch)) {
104
+ const suggestion = suggestBranchIdentifier(identifier);
105
+ throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
106
+ }
107
+ if (kind === "select" && branch.startsWith("=")) throw new Error(`Invalid select branch key '${identifier}', a number selects an exact value, which only 'plural' and 'ordinal' accept`);
108
+ }
109
+ /**
110
+ * The nearest identifier-safe form of a key, so the error names the fix as well
111
+ * as the problem. The constraint comes from ICU rather than from anything the
112
+ * author wrote, and camel case is how the rest of the codebase already spells a
113
+ * name of more than one word.
114
+ */
115
+ function suggestBranchIdentifier(identifier) {
116
+ const suggestion = identifier.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word, index) => index === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
117
+ if (!BRANCH_PATTERN.test(suggestion) || !Number.isNaN(+suggestion)) return void 0;
118
+ return suggestion;
119
+ }
120
+ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalent = () => false) {
121
+ const reserved = collectAssignedIdentifiers(message, equivalent);
122
+ function next() {
123
+ let identifier = `${sequence.current++}`;
124
+ while (reserved.has(identifier)) identifier = `${sequence.current++}`;
125
+ return identifier;
126
+ }
127
+ function walk(message) {
128
+ if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
129
+ if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
130
+ }
131
+ if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
132
+ if (message instanceof ChoiceMessage) for (const branch of message.branches) {
133
+ if (branch.identifier === AUTO_INCREMENT_IDENTIFIER) branch.identifier = next();
134
+ walk(branch.value);
135
+ }
136
+ }
137
+ walk(message);
138
+ }
139
+ /**
140
+ * Collect the identifiers already assigned before this pass runs, so generated
141
+ * sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
142
+ *
143
+ * A name is claimed by what produced it, not by the name alone. Two that differ
144
+ * each compile to their own prop, and a translator moving them around a sentence
145
+ * has to be able to tell them apart, so they are a build error. Repeats are fine
146
+ * when nothing distinguishes them: the same variable interpolated twice is one
147
+ * value, and two identical elements are one tag.
148
+ */
149
+ function collectAssignedIdentifiers(message, equivalent) {
150
+ const tags = /* @__PURE__ */ new Map();
151
+ const values = /* @__PURE__ */ new Map();
152
+ function claim(claimed, identifier, expression, conflict) {
153
+ if (claimed.has(identifier) && !equivalent(claimed.get(identifier), expression)) throw new Error(conflict);
154
+ claimed.set(identifier, expression);
155
+ }
156
+ function walk(message) {
157
+ if (message instanceof ElementMessage && typeof message.identifier === "string") claim(tags, message.identifier, message.expression, `Duplicate element tag '${message.identifier}', give each element in a message its own tag unless they are identical`);
158
+ if ((message instanceof ArgumentMessage || message instanceof ChoiceMessage) && typeof message.identifier === "string") claim(values, message.identifier, message.expression, `Duplicate placeholder name '${message.identifier}', give each value in a message its own name unless they are identical`);
159
+ if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
160
+ if (message instanceof ChoiceMessage) for (const branch of message.branches) walk(branch.value);
161
+ }
162
+ walk(message);
163
+ for (const tag of tags.keys()) if (values.has(tag)) throw new Error(`Element tag '${tag}' collides with an argument of the same name`);
164
+ return /* @__PURE__ */ new Set([...tags.keys(), ...values.keys()]);
165
+ }
166
+ //#endregion
83
167
  //#region src/features/messages/convert.ts
84
168
  function convertMessageToIcu(message) {
85
169
  function internalConvertMessageToIcu(message) {
@@ -87,12 +171,13 @@ function convertMessageToIcu(message) {
87
171
  case message instanceof LiteralMessage: return String(message.text);
88
172
  case message instanceof ArgumentMessage: return `{${String(message.identifier)}}`;
89
173
  case message instanceof ElementMessage: {
174
+ if (message.children.length === 0) return `<${String(message.identifier)}/>`;
90
175
  const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
91
176
  return `<${String(message.identifier)}>${children}</${String(message.identifier)}>`;
92
177
  }
93
178
  case message instanceof ChoiceMessage: {
94
179
  const branches = message.branches.map(({ identifier, value }) => ({
95
- identifier: Number.isNaN(+String(identifier)) ? String(identifier) : `=${+String(identifier)}`,
180
+ identifier: getBranchCase(identifier),
96
181
  value: internalConvertMessageToIcu(value)
97
182
  })).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
98
183
  const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
@@ -105,4 +190,4 @@ function convertMessageToIcu(message) {
105
190
  return internalConvertMessageToIcu(message).trim();
106
191
  }
107
192
  //#endregion
108
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, generateHash };
193
+ export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/config",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "CLI and configuration tooling for saykit",
5
5
  "keywords": [
6
6
  "cli",