@ctcstack/ctcdocs-core 0.5.1 → 0.6.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.
@@ -100,6 +100,15 @@ export interface NavigationConfiguration {
100
100
  * slugs are reserved either way, so turning this on or off moves no address.
101
101
  */
102
102
  readonly sectionIndexPages: boolean;
103
+ /**
104
+ * The Unicode scripts a letter in a Drive name may belong to, such as
105
+ * `["Latin"]`, or `null` when any script is accepted. A name becomes a
106
+ * heading, a sidebar label and, once, a permanent address, so a deployment
107
+ * whose editors write in one alphabet can refuse a letter typed on the other
108
+ * keyboard layout before it reaches any of them. Whatever this allows, a
109
+ * single word never mixes Latin, Cyrillic and Greek letters (ADR-020).
110
+ */
111
+ readonly nameScripts: readonly string[] | null;
103
112
  }
104
113
  export interface SiteConfiguration {
105
114
  readonly brand: BrandConfiguration;
@@ -89,6 +89,36 @@ function flag(source, key, path) {
89
89
  function optionalFlag(source, key, path, fallback) {
90
90
  return source[key] === undefined ? fallback : flag(source, key, path);
91
91
  }
92
+ /**
93
+ * A list of distinct Unicode script names, as a `\p{Script=…}` property
94
+ * escape spells them. Each name is proved by compiling that escape, so a typo
95
+ * fails here rather than letting every letter through, or none.
96
+ */
97
+ function optionalScriptList(source, key, path) {
98
+ const value = source[key];
99
+ if (value === undefined) {
100
+ return null;
101
+ }
102
+ if (!Array.isArray(value) || value.length === 0) {
103
+ fail(path, 'must be a non-empty array of Unicode script names');
104
+ }
105
+ const scripts = value.map((entry, index) => {
106
+ if (typeof entry !== 'string' || !/^[A-Za-z_]+$/u.test(entry)) {
107
+ fail(`${path}[${index}]`, 'must be a Unicode script name such as "Latin"');
108
+ }
109
+ try {
110
+ new RegExp(`\\p{Script=${entry}}`, 'u');
111
+ }
112
+ catch {
113
+ fail(`${path}[${index}]`, 'must be a Unicode script name such as "Latin"');
114
+ }
115
+ return entry;
116
+ });
117
+ if (new Set(scripts).size !== scripts.length) {
118
+ fail(path, 'must not repeat a script');
119
+ }
120
+ return Object.freeze(scripts);
121
+ }
92
122
  /**
93
123
  * A count of things shown on a page. Zero is rejected along with fractions and
94
124
  * negatives: a block configured to show nothing is hidden by the switch that
@@ -190,6 +220,7 @@ export function parseSiteConfiguration(input) {
190
220
  navigation: {
191
221
  landingDocumentTitles: titleList(navigationSource, 'landingDocumentTitles', 'navigation.landingDocumentTitles'),
192
222
  sectionIndexPages: flag(navigationSource, 'sectionIndexPages', 'navigation.sectionIndexPages'),
223
+ nameScripts: optionalScriptList(navigationSource, 'nameScripts', 'navigation.nameScripts'),
193
224
  },
194
225
  sync: {
195
226
  commitBotName: text(syncSource, 'commitBotName', 'sync.commitBotName'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctcstack/ctcdocs-core",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Configuration schema, project layout, and generated-path allowlist shared by the CTCDocs platform packages.",
5
5
  "keywords": [
6
6
  "ctcdocs",
@@ -125,6 +125,15 @@ export interface NavigationConfiguration {
125
125
  * slugs are reserved either way, so turning this on or off moves no address.
126
126
  */
127
127
  readonly sectionIndexPages: boolean;
128
+ /**
129
+ * The Unicode scripts a letter in a Drive name may belong to, such as
130
+ * `["Latin"]`, or `null` when any script is accepted. A name becomes a
131
+ * heading, a sidebar label and, once, a permanent address, so a deployment
132
+ * whose editors write in one alphabet can refuse a letter typed on the other
133
+ * keyboard layout before it reaches any of them. Whatever this allows, a
134
+ * single word never mixes Latin, Cyrillic and Greek letters (ADR-020).
135
+ */
136
+ readonly nameScripts: readonly string[] | null;
128
137
  }
129
138
 
130
139
  export interface SiteConfiguration {
@@ -242,6 +251,46 @@ function optionalFlag(
242
251
  return source[key] === undefined ? fallback : flag(source, key, path);
243
252
  }
244
253
 
254
+ /**
255
+ * A list of distinct Unicode script names, as a `\p{Script=…}` property
256
+ * escape spells them. Each name is proved by compiling that escape, so a typo
257
+ * fails here rather than letting every letter through, or none.
258
+ */
259
+ function optionalScriptList(
260
+ source: Record<string, unknown>,
261
+ key: string,
262
+ path: string,
263
+ ): readonly string[] | null {
264
+ const value = source[key];
265
+ if (value === undefined) {
266
+ return null;
267
+ }
268
+ if (!Array.isArray(value) || value.length === 0) {
269
+ fail(path, 'must be a non-empty array of Unicode script names');
270
+ }
271
+ const scripts = value.map((entry, index) => {
272
+ if (typeof entry !== 'string' || !/^[A-Za-z_]+$/u.test(entry)) {
273
+ fail(
274
+ `${path}[${index}]`,
275
+ 'must be a Unicode script name such as "Latin"',
276
+ );
277
+ }
278
+ try {
279
+ new RegExp(`\\p{Script=${entry}}`, 'u');
280
+ } catch {
281
+ fail(
282
+ `${path}[${index}]`,
283
+ 'must be a Unicode script name such as "Latin"',
284
+ );
285
+ }
286
+ return entry;
287
+ });
288
+ if (new Set(scripts).size !== scripts.length) {
289
+ fail(path, 'must not repeat a script');
290
+ }
291
+ return Object.freeze(scripts);
292
+ }
293
+
245
294
  /**
246
295
  * A count of things shown on a page. Zero is rejected along with fractions and
247
296
  * negatives: a block configured to show nothing is hidden by the switch that
@@ -396,6 +445,11 @@ export function parseSiteConfiguration(input: unknown): SiteConfiguration {
396
445
  'sectionIndexPages',
397
446
  'navigation.sectionIndexPages',
398
447
  ),
448
+ nameScripts: optionalScriptList(
449
+ navigationSource,
450
+ 'nameScripts',
451
+ 'navigation.nameScripts',
452
+ ),
399
453
  },
400
454
  sync: {
401
455
  commitBotName: text(syncSource, 'commitBotName', 'sync.commitBotName'),