@saykit/config 0.7.0 → 0.8.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.
@@ -1,5 +1,33 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_hash = require("../../hash-51ce8YiG.cjs");
3
+ //#region src/features/messages/escape.ts
4
+ /**
5
+ * Quoting for text that has to survive a message format unchanged.
6
+ *
7
+ * This lives beside the converter rather than in a transform because it belongs
8
+ * to the format being written, not to the syntax being read. A literal is the
9
+ * same literal whether it came from a template literal or from JSX, and the
10
+ * moment there is a second target format its reserved characters — and the way
11
+ * it spells an escape — are its own.
12
+ */
13
+ /**
14
+ * Escape literal text so ICU MessageFormat 1 reads it as the text it is.
15
+ *
16
+ * MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
17
+ * doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
18
+ * only opens a quoted run when a character that needs quoting follows it —
19
+ * anything else and the apostrophe is itself literal. So `'#'` is a real escape
20
+ * inside a plural and the three characters `'#'` anywhere else, which is why
21
+ * `hash` has to be told rather than assumed.
22
+ *
23
+ * @param text Literal text as the author wrote it
24
+ * @param hash Whether `#` stands for a number here, i.e. whether some `plural`
25
+ * or `ordinal` encloses this text
26
+ */
27
+ function escapeIcuLiteral(text, hash = false) {
28
+ return text.replace(/'/gu, "''").replace(hash ? /[{}#]+/gu : /[{}]+/gu, (run) => `'${run}'`);
29
+ }
30
+ //#endregion
3
31
  //#region src/features/messages/types.ts
4
32
  var Base = class {
5
33
  toICUString() {
@@ -135,9 +163,28 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
135
163
  while (reserved.has(identifier)) identifier = `${sequence.current++}`;
136
164
  return identifier;
137
165
  }
166
+ const numbered = {
167
+ tags: [],
168
+ values: []
169
+ };
170
+ /**
171
+ * The number this expression already has, or a fresh one. An anonymous
172
+ * placeholder is named by its position, so the same value written twice would
173
+ * otherwise arrive as two names — `{0} x {1}` for one length, which asks a
174
+ * translator to keep two holes in step and the caller to supply one value
175
+ * under two props. This is the rule explicit names already follow, where a
176
+ * repeat is allowed precisely when nothing distinguishes it.
177
+ */
178
+ function number(assigned, expression) {
179
+ const seen = assigned.find(([e]) => equivalent(e, expression));
180
+ if (seen) return seen[1];
181
+ const identifier = next();
182
+ assigned.push([expression, identifier]);
183
+ return identifier;
184
+ }
138
185
  function walk(message) {
139
186
  if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
140
- if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
187
+ if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = number(message instanceof ElementMessage ? numbered.tags : numbered.values, message.expression);
141
188
  }
142
189
  if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
143
190
  if (message instanceof ChoiceMessage) for (const branch of message.branches) {
@@ -177,10 +224,11 @@ function collectAssignedIdentifiers(message, equivalent) {
177
224
  //#endregion
178
225
  //#region src/features/messages/convert.ts
179
226
  function convertMessageToIcu(message) {
180
- function internalConvertMessageToIcu(message) {
227
+ function internalConvertMessageToIcu(message, hash) {
181
228
  switch (true) {
182
- case message instanceof LiteralMessage: return String(message.text);
229
+ case message instanceof LiteralMessage: return escapeIcuLiteral(String(message.text), hash !== void 0);
183
230
  case message instanceof ArgumentMessage: {
231
+ if (!message.format && hash !== void 0 && String(message.identifier) === hash) return "#";
184
232
  const parts = [String(message.identifier)];
185
233
  if (message.format) parts.push(message.format.type);
186
234
  if (message.format?.style) parts.push(message.format.style);
@@ -188,23 +236,24 @@ function convertMessageToIcu(message) {
188
236
  }
189
237
  case message instanceof ElementMessage: {
190
238
  if (message.children.length === 0) return `<${String(message.identifier)}/>`;
191
- const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
239
+ const children = message.children.map((m) => internalConvertMessageToIcu(m, hash)).join("");
192
240
  return `<${String(message.identifier)}>${children}</${String(message.identifier)}>`;
193
241
  }
194
242
  case message instanceof ChoiceMessage: {
243
+ const scope = message.kind === "select" ? hash : String(message.identifier);
195
244
  const branches = message.branches.map(({ identifier, value }) => ({
196
245
  identifier: getBranchCase(message.kind, identifier),
197
- value: internalConvertMessageToIcu(value)
246
+ value: internalConvertMessageToIcu(value, scope)
198
247
  })).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
199
248
  const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
200
249
  const offset = message.offset === void 0 || message.kind === "select" ? "" : ` offset:${message.offset}`;
201
250
  return `{${String(message.identifier)}, ${format},${offset}\n${branches}}`;
202
251
  }
203
- case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
252
+ case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m, hash)).join("");
204
253
  default: throw new Error("Unknown message type", { cause: message });
205
254
  }
206
255
  }
207
- return internalConvertMessageToIcu(message).trim();
256
+ return internalConvertMessageToIcu(message, void 0).trim();
208
257
  }
209
258
  //#endregion
210
259
  //#region src/features/messages/format.ts
@@ -282,6 +331,7 @@ exports.ElementMessage = ElementMessage;
282
331
  exports.LiteralMessage = LiteralMessage;
283
332
  exports.assignSequenceIdentifiers = assignSequenceIdentifiers;
284
333
  exports.convertMessageToIcu = convertMessageToIcu;
334
+ exports.escapeIcuLiteral = escapeIcuLiteral;
285
335
  exports.generateHash = require_hash.generateHash;
286
336
  exports.getBranchCase = getBranchCase;
287
337
  exports.isArgumentType = isArgumentType;
@@ -147,7 +147,33 @@ type Message = LiteralMessage | ArgumentMessage | ElementMessage | ChoiceMessage
147
147
  //#region src/features/messages/convert.d.ts
148
148
  declare function convertMessageToIcu(message: Message): string;
149
149
  //#endregion
150
+ //#region src/features/messages/escape.d.ts
151
+ /**
152
+ * Quoting for text that has to survive a message format unchanged.
153
+ *
154
+ * This lives beside the converter rather than in a transform because it belongs
155
+ * to the format being written, not to the syntax being read. A literal is the
156
+ * same literal whether it came from a template literal or from JSX, and the
157
+ * moment there is a second target format its reserved characters — and the way
158
+ * it spells an escape — are its own.
159
+ */
160
+ /**
161
+ * Escape literal text so ICU MessageFormat 1 reads it as the text it is.
162
+ *
163
+ * MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
164
+ * doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
165
+ * only opens a quoted run when a character that needs quoting follows it —
166
+ * anything else and the apostrophe is itself literal. So `'#'` is a real escape
167
+ * inside a plural and the three characters `'#'` anywhere else, which is why
168
+ * `hash` has to be told rather than assumed.
169
+ *
170
+ * @param text Literal text as the author wrote it
171
+ * @param hash Whether `#` stands for a number here, i.e. whether some `plural`
172
+ * or `ordinal` encloses this text
173
+ */
174
+ declare function escapeIcuLiteral(text: string, hash?: boolean): string;
175
+ //#endregion
150
176
  //#region src/features/messages/hash.d.ts
151
177
  declare function generateHash(input: string, context?: string): string;
152
178
  //#endregion
153
- export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
179
+ export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
@@ -147,7 +147,33 @@ type Message = LiteralMessage | ArgumentMessage | ElementMessage | ChoiceMessage
147
147
  //#region src/features/messages/convert.d.ts
148
148
  declare function convertMessageToIcu(message: Message): string;
149
149
  //#endregion
150
+ //#region src/features/messages/escape.d.ts
151
+ /**
152
+ * Quoting for text that has to survive a message format unchanged.
153
+ *
154
+ * This lives beside the converter rather than in a transform because it belongs
155
+ * to the format being written, not to the syntax being read. A literal is the
156
+ * same literal whether it came from a template literal or from JSX, and the
157
+ * moment there is a second target format its reserved characters — and the way
158
+ * it spells an escape — are its own.
159
+ */
160
+ /**
161
+ * Escape literal text so ICU MessageFormat 1 reads it as the text it is.
162
+ *
163
+ * MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
164
+ * doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
165
+ * only opens a quoted run when a character that needs quoting follows it —
166
+ * anything else and the apostrophe is itself literal. So `'#'` is a real escape
167
+ * inside a plural and the three characters `'#'` anywhere else, which is why
168
+ * `hash` has to be told rather than assumed.
169
+ *
170
+ * @param text Literal text as the author wrote it
171
+ * @param hash Whether `#` stands for a number here, i.e. whether some `plural`
172
+ * or `ordinal` encloses this text
173
+ */
174
+ declare function escapeIcuLiteral(text: string, hash?: boolean): string;
175
+ //#endregion
150
176
  //#region src/features/messages/hash.d.ts
151
177
  declare function generateHash(input: string, context?: string): string;
152
178
  //#endregion
153
- export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
179
+ export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
@@ -1,4 +1,32 @@
1
1
  import { t as generateHash } from "../../hash-DvzpieJD.mjs";
2
+ //#region src/features/messages/escape.ts
3
+ /**
4
+ * Quoting for text that has to survive a message format unchanged.
5
+ *
6
+ * This lives beside the converter rather than in a transform because it belongs
7
+ * to the format being written, not to the syntax being read. A literal is the
8
+ * same literal whether it came from a template literal or from JSX, and the
9
+ * moment there is a second target format its reserved characters — and the way
10
+ * it spells an escape — are its own.
11
+ */
12
+ /**
13
+ * Escape literal text so ICU MessageFormat 1 reads it as the text it is.
14
+ *
15
+ * MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
16
+ * doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
17
+ * only opens a quoted run when a character that needs quoting follows it —
18
+ * anything else and the apostrophe is itself literal. So `'#'` is a real escape
19
+ * inside a plural and the three characters `'#'` anywhere else, which is why
20
+ * `hash` has to be told rather than assumed.
21
+ *
22
+ * @param text Literal text as the author wrote it
23
+ * @param hash Whether `#` stands for a number here, i.e. whether some `plural`
24
+ * or `ordinal` encloses this text
25
+ */
26
+ function escapeIcuLiteral(text, hash = false) {
27
+ return text.replace(/'/gu, "''").replace(hash ? /[{}#]+/gu : /[{}]+/gu, (run) => `'${run}'`);
28
+ }
29
+ //#endregion
2
30
  //#region src/features/messages/types.ts
3
31
  var Base = class {
4
32
  toICUString() {
@@ -134,9 +162,28 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
134
162
  while (reserved.has(identifier)) identifier = `${sequence.current++}`;
135
163
  return identifier;
136
164
  }
165
+ const numbered = {
166
+ tags: [],
167
+ values: []
168
+ };
169
+ /**
170
+ * The number this expression already has, or a fresh one. An anonymous
171
+ * placeholder is named by its position, so the same value written twice would
172
+ * otherwise arrive as two names — `{0} x {1}` for one length, which asks a
173
+ * translator to keep two holes in step and the caller to supply one value
174
+ * under two props. This is the rule explicit names already follow, where a
175
+ * repeat is allowed precisely when nothing distinguishes it.
176
+ */
177
+ function number(assigned, expression) {
178
+ const seen = assigned.find(([e]) => equivalent(e, expression));
179
+ if (seen) return seen[1];
180
+ const identifier = next();
181
+ assigned.push([expression, identifier]);
182
+ return identifier;
183
+ }
137
184
  function walk(message) {
138
185
  if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
139
- if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
186
+ if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = number(message instanceof ElementMessage ? numbered.tags : numbered.values, message.expression);
140
187
  }
141
188
  if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
142
189
  if (message instanceof ChoiceMessage) for (const branch of message.branches) {
@@ -176,10 +223,11 @@ function collectAssignedIdentifiers(message, equivalent) {
176
223
  //#endregion
177
224
  //#region src/features/messages/convert.ts
178
225
  function convertMessageToIcu(message) {
179
- function internalConvertMessageToIcu(message) {
226
+ function internalConvertMessageToIcu(message, hash) {
180
227
  switch (true) {
181
- case message instanceof LiteralMessage: return String(message.text);
228
+ case message instanceof LiteralMessage: return escapeIcuLiteral(String(message.text), hash !== void 0);
182
229
  case message instanceof ArgumentMessage: {
230
+ if (!message.format && hash !== void 0 && String(message.identifier) === hash) return "#";
183
231
  const parts = [String(message.identifier)];
184
232
  if (message.format) parts.push(message.format.type);
185
233
  if (message.format?.style) parts.push(message.format.style);
@@ -187,23 +235,24 @@ function convertMessageToIcu(message) {
187
235
  }
188
236
  case message instanceof ElementMessage: {
189
237
  if (message.children.length === 0) return `<${String(message.identifier)}/>`;
190
- const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
238
+ const children = message.children.map((m) => internalConvertMessageToIcu(m, hash)).join("");
191
239
  return `<${String(message.identifier)}>${children}</${String(message.identifier)}>`;
192
240
  }
193
241
  case message instanceof ChoiceMessage: {
242
+ const scope = message.kind === "select" ? hash : String(message.identifier);
194
243
  const branches = message.branches.map(({ identifier, value }) => ({
195
244
  identifier: getBranchCase(message.kind, identifier),
196
- value: internalConvertMessageToIcu(value)
245
+ value: internalConvertMessageToIcu(value, scope)
197
246
  })).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
198
247
  const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
199
248
  const offset = message.offset === void 0 || message.kind === "select" ? "" : ` offset:${message.offset}`;
200
249
  return `{${String(message.identifier)}, ${format},${offset}\n${branches}}`;
201
250
  }
202
- case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
251
+ case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m, hash)).join("");
203
252
  default: throw new Error("Unknown message type", { cause: message });
204
253
  }
205
254
  }
206
- return internalConvertMessageToIcu(message).trim();
255
+ return internalConvertMessageToIcu(message, void 0).trim();
207
256
  }
208
257
  //#endregion
209
258
  //#region src/features/messages/format.ts
@@ -271,4 +320,4 @@ function validateArgumentStyle(type, style) {
271
320
  throw new Error(`Invalid ${type} style '${style}', expected ${expected}` + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
272
321
  }
273
322
  //#endregion
274
- export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
323
+ export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/config",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI and configuration tooling for saykit",
5
5
  "keywords": [
6
6
  "cli",