@pie-players/pie-players-shared 1.0.0-beta.6 → 1.0.0-beta.8

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.
Files changed (37) hide show
  1. package/README.md +3 -1
  2. package/dist/i18n/catalogs.d.ts +20 -13
  3. package/dist/i18n/catalogs.js +22 -23
  4. package/dist/i18n/content-language.d.ts +46 -4
  5. package/dist/i18n/content-language.js +83 -5
  6. package/dist/i18n/index.d.ts +3 -2
  7. package/dist/i18n/index.js +2 -2
  8. package/dist/i18n/language-tags.d.ts +73 -4
  9. package/dist/i18n/language-tags.js +176 -6
  10. package/dist/i18n/messages/en-US.d.ts +23 -0
  11. package/dist/i18n/messages/en-US.js +25 -0
  12. package/dist/i18n/provider.d.ts +3 -6
  13. package/dist/i18n/provider.js +5 -15
  14. package/dist/i18n/types.d.ts +6 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +1 -0
  17. package/dist/loaders/ElementLoader.d.ts +1 -1
  18. package/dist/loaders/element-loader.d.ts +4 -1
  19. package/dist/loaders/element-loader.js +74 -21
  20. package/dist/loaders/iife-adapter.d.ts +7 -0
  21. package/dist/loaders/iife-adapter.js +1 -1
  22. package/dist/security/private-hosts.d.ts +27 -0
  23. package/dist/security/private-hosts.js +85 -0
  24. package/dist/security/validate-style-url.js +2 -2
  25. package/dist/tools/term-lookup.d.ts +15 -0
  26. package/dist/tools/term-lookup.js +16 -0
  27. package/dist/types/index.d.ts +47 -27
  28. package/dist/types/interchange.d.ts +116 -0
  29. package/dist/types/interchange.js +1 -0
  30. package/dist/ui/focus-trap.js +3 -1
  31. package/dist/ui/fontawesome.d.ts +51 -0
  32. package/dist/ui/fontawesome.js +122 -0
  33. package/dist/ui/ime.d.ts +6 -0
  34. package/dist/ui/ime.js +10 -0
  35. package/package.json +15 -6
  36. package/dist/i18n/messages/nl-NL.d.ts +0 -636
  37. package/dist/i18n/messages/nl-NL.js +0 -636
@@ -301,6 +301,12 @@ declare const enUS: {
301
301
  lanthanide: string;
302
302
  actinide: string;
303
303
  };
304
+ /** Standard-state phase, keyed by the data file's lowercased `phase` value. */
305
+ phaseValue: {
306
+ solid: string;
307
+ liquid: string;
308
+ gas: string;
309
+ };
304
310
  };
305
311
  ruler: {
306
312
  name: string;
@@ -431,6 +437,12 @@ declare const enUS: {
431
437
  description: string;
432
438
  buttonA11y: string;
433
439
  tooltip: string;
440
+ /** A choice's toggle; `{choice}` is the choice's own accessible name. */
441
+ toggleA11y: string;
442
+ /** Appended to an eliminated choice's label for assistive technology. */
443
+ eliminatedA11y: string;
444
+ /** Stands in for `{choice}` when a choice has no text of its own. */
445
+ unlabeledChoice: string;
434
446
  };
435
447
  highlighter: {
436
448
  name: string;
@@ -456,6 +468,17 @@ declare const enUS: {
456
468
  highlightPinkA11y: string;
457
469
  highlightBlueA11y: string;
458
470
  highlightGreenA11y: string;
471
+ selectionAvailableA11y: string;
472
+ highlightedYellowA11y: string;
473
+ highlightedPinkA11y: string;
474
+ highlightedBlueA11y: string;
475
+ highlightedGreenA11y: string;
476
+ underlinedA11y: string;
477
+ removedA11y: string;
478
+ clearedA11y: {
479
+ one: string;
480
+ other: string;
481
+ };
459
482
  };
460
483
  theme: {
461
484
  name: string;
@@ -306,6 +306,12 @@ const enUS = {
306
306
  lanthanide: "Lanthanide",
307
307
  actinide: "Actinide",
308
308
  },
309
+ /** Standard-state phase, keyed by the data file's lowercased `phase` value. */
310
+ phaseValue: {
311
+ solid: "Solid",
312
+ liquid: "Liquid",
313
+ gas: "Gas",
314
+ },
309
315
  },
310
316
  ruler: {
311
317
  name: "Ruler",
@@ -448,6 +454,12 @@ const enUS = {
448
454
  description: "Strike through answer choices",
449
455
  buttonA11y: "Strike Through, eliminate answer choices",
450
456
  tooltip: "Strike Through",
457
+ /** A choice's toggle; `{choice}` is the choice's own accessible name. */
458
+ toggleA11y: "Toggle elimination for {choice}",
459
+ /** Appended to an eliminated choice's label for assistive technology. */
460
+ eliminatedA11y: "(eliminated)",
461
+ /** Stands in for `{choice}` when a choice has no text of its own. */
462
+ unlabeledChoice: "Unlabeled choice",
451
463
  },
452
464
  highlighter: {
453
465
  name: "Highlighter",
@@ -476,6 +488,19 @@ const enUS = {
476
488
  highlightPinkA11y: "Pink highlight",
477
489
  highlightBlueA11y: "Blue highlight",
478
490
  highlightGreenA11y: "Green highlight",
491
+ // Announced in the strip's live region. A colour is authored per phrase:
492
+ // a colour adjective agrees with its noun in some languages.
493
+ selectionAvailableA11y: "Annotation toolbar available for “{text}”. Press Shift+F10 for annotation tools.",
494
+ highlightedYellowA11y: "“{text}” highlighted in yellow",
495
+ highlightedPinkA11y: "“{text}” highlighted in pink",
496
+ highlightedBlueA11y: "“{text}” highlighted in blue",
497
+ highlightedGreenA11y: "“{text}” highlighted in green",
498
+ underlinedA11y: "“{text}” underlined",
499
+ removedA11y: "Removed annotation from “{text}”",
500
+ clearedA11y: {
501
+ one: "{count} annotation cleared",
502
+ other: "{count} annotations cleared",
503
+ },
479
504
  },
480
505
  theme: {
481
506
  name: "Theme",
@@ -122,12 +122,9 @@ export declare class SimpleI18n implements I18nServiceApi {
122
122
  /** The resolved catalog for a locale, for a host that wants to inspect it. */
123
123
  getBundle(locale?: LocaleCode): TranslationBundle | undefined;
124
124
  /**
125
- * Which shipped catalog serves a requested tag.
126
- *
127
- * RFC 4647 lookup first, so `nl-BE` reaches `nl-NL` through `nl`; then
128
- * primary-subtag widening, so a bare `nl` reaches the regional `nl-NL`
129
- * catalog we actually ship. POSIX `nl_NL` normalizes on the way in, which is
130
- * the form the Learnosity transform emits.
125
+ * Which shipped catalog serves a requested tag, under
126
+ * `findClosestLanguageMatch`. POSIX `nl_NL` normalizes on the way in, which
127
+ * is the form the Learnosity transform emits.
131
128
  */
132
129
  private resolveLocaleTag;
133
130
  private loadCatalog;
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module @pie-players/pie-players-shared/i18n/provider
11
11
  */
12
- import { canonicalLanguageTag, findBestLanguageMatch, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
12
+ import { canonicalLanguageTag, findClosestLanguageMatch, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
13
13
  import enUS from "./messages/en-US.js";
14
14
  /** The locale every fallback chain terminates at. */
15
15
  export const DEFAULT_LOCALE = "en-US";
@@ -302,24 +302,14 @@ export class SimpleI18n {
302
302
  return { locale: tag, translations, direction: localeDirection(tag) };
303
303
  }
304
304
  /**
305
- * Which shipped catalog serves a requested tag.
306
- *
307
- * RFC 4647 lookup first, so `nl-BE` reaches `nl-NL` through `nl`; then
308
- * primary-subtag widening, so a bare `nl` reaches the regional `nl-NL`
309
- * catalog we actually ship. POSIX `nl_NL` normalizes on the way in, which is
310
- * the form the Learnosity transform emits.
305
+ * Which shipped catalog serves a requested tag, under
306
+ * `findClosestLanguageMatch`. POSIX `nl_NL` normalizes on the way in, which
307
+ * is the form the Learnosity transform emits.
311
308
  */
312
309
  resolveLocaleTag(locale) {
313
310
  if (!locale)
314
311
  return undefined;
315
- const available = this.getAvailableLocales();
316
- const exactOrPrefix = findBestLanguageMatch(locale, available);
317
- if (exactOrPrefix)
318
- return exactOrPrefix;
319
- const requestedLanguage = normalizeLanguageTag(locale).split("-")[0];
320
- if (!requestedLanguage)
321
- return undefined;
322
- return available.find((candidate) => normalizeLanguageTag(candidate).split("-")[0] === requestedLanguage);
312
+ return findClosestLanguageMatch(locale, this.getAvailableLocales());
323
313
  }
324
314
  async loadCatalog(locale) {
325
315
  const load = this.config.loadCatalog;
@@ -16,8 +16,13 @@ import type enUS from "./messages/en-US.js";
16
16
  * syntax — the Learnosity transform emits POSIX `nl_NL`, QTI catalog cards carry
17
17
  * `xml:lang` — and the resolver normalizes both onto these, so this list is the
18
18
  * set of catalogs, not the set of accepted inputs.
19
+ *
20
+ * `en-US` is the only bundled catalog: the statically resident source of truth
21
+ * every other locale falls back through. Every other locale this repository
22
+ * ships lives in the optional generated tier
23
+ * (`@pie-players/pie-i18n-generated`), which a host composes into its loader.
19
24
  */
20
- export type BundledLocaleCode = "en-US" | "nl-NL";
25
+ export type BundledLocaleCode = "en-US";
21
26
  /**
22
27
  * Any locale tag. A host may supply a locale we ship no catalog for and provide
23
28
  * its own messages through {@link I18nConfig.customMessages}, so this is
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export * from "./ui/pointer-rotate.js";
16
16
  export * from "./ui/overlay-containment.js";
17
17
  export * from "./ui/overlay-placement.js";
18
18
  export * from "./ui/focus-trap.js";
19
+ export * from "./ui/ime.js";
19
20
  export * from "./ui/first-focusable.js";
20
21
  export * from "./ui/safe-storage.js";
21
22
  export * from "./ui/scope-css.js";
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export * from "./ui/pointer-rotate.js";
15
15
  export * from "./ui/overlay-containment.js";
16
16
  export * from "./ui/overlay-placement.js";
17
17
  export * from "./ui/focus-trap.js";
18
+ export * from "./ui/ime.js";
18
19
  export * from "./ui/first-focusable.js";
19
20
  export * from "./ui/safe-storage.js";
20
21
  export * from "./ui/scope-css.js";
@@ -41,4 +41,4 @@ export type ElementMap = {
41
41
  * // }
42
42
  * ```
43
43
  */
44
- export declare function aggregateElements(items: ItemEntity[]): ElementMap;
44
+ export declare function aggregateElements(items: ReadonlyArray<Pick<ItemEntity, "config">>): ElementMap;
@@ -88,7 +88,10 @@ export type EnsureRegisteredOptions = {
88
88
  *
89
89
  * The primitive is the sole authority on registration state:
90
90
  * - Empty element map resolves immediately (no backend call).
91
- * - Already-registered tags resolve immediately (no backend call).
91
+ * - Already-registered tags resolve immediately (no backend call). A request
92
+ * that loads other tags through an IIFE or ESM backend that loads controllers
93
+ * also loads its registered tags with no controller, so their controllers
94
+ * fill in.
92
95
  * - Concurrent identical requests share one backend call.
93
96
  * - After the adapter's `load` settles, a post-load verification pass
94
97
  * (bounded `customElements.whenDefined`) checks every tag. Any missing
@@ -26,7 +26,7 @@ import { parsePackageName } from "../pie/utils.js";
26
26
  import { parseVersionedTagName } from "../pie/versioned-tag.js";
27
27
  import { AdapterFailure, } from "./element-loader-types.js";
28
28
  import { clearPackageMetadataRequests, createEsmBackend, } from "./esm-adapter.js";
29
- import { createIifeBackend } from "./iife-adapter.js";
29
+ import { createIifeBackend, findIifeElementMapConflict, } from "./iife-adapter.js";
30
30
  import { assertElementPackagesAllowed, } from "./element-package-policy.js";
31
31
  export { AdapterFailure } from "./element-loader-types.js";
32
32
  /**
@@ -125,9 +125,10 @@ function registeredPackageName(tag) {
125
125
  }
126
126
  const DEFAULT_WHEN_DEFINED_TIMEOUT_MS = 5000;
127
127
  const DEFAULT_LOAD_TIMEOUT_MS = DEFAULT_IIFE_BUNDLE_RETRY_CONFIG.timeoutMs;
128
- // Module-scoped in-flight cache. Key: backend signature + sorted elements.
129
- // Keeps concurrent identical requests collapsed to one backend call.
128
+ // Requests share a backend call by both the original map and the unresolved
129
+ // subset, so incremental registration does not change an in-flight identity.
130
130
  const inFlightRequests = new Map();
131
+ const inFlightKeysByPromise = new Map();
131
132
  // An ESM backend keeps page state that later loads build on (the import maps it
132
133
  // injected, the shared dependency versions they fixed), so every load with the
133
134
  // same config reuses one backend. IIFE backends are created per call.
@@ -139,7 +140,10 @@ const resolvedBackendOverrides = new Map();
139
140
  *
140
141
  * The primitive is the sole authority on registration state:
141
142
  * - Empty element map resolves immediately (no backend call).
142
- * - Already-registered tags resolve immediately (no backend call).
143
+ * - Already-registered tags resolve immediately (no backend call). A request
144
+ * that loads other tags through an IIFE or ESM backend that loads controllers
145
+ * also loads its registered tags with no controller, so their controllers
146
+ * fill in.
143
147
  * - Concurrent identical requests share one backend call.
144
148
  * - After the adapter's `load` settles, a post-load verification pass
145
149
  * (bounded `customElements.whenDefined`) checks every tag. Any missing
@@ -153,30 +157,54 @@ export async function ensureRegistered(elements, options) {
153
157
  // Enforce policy before the already-registered fast path: a matching tag in
154
158
  // the registry must not turn a disallowed authored package into valid input.
155
159
  assertElementPackagesAllowed(elements, options.elementPackagePolicy);
156
- const tags = Object.keys(elements);
160
+ // Preserve the original request identity while tags register mid-flight.
161
+ const originalDedupKey = buildDedupKey(options.backend, elements);
162
+ const originalRequest = inFlightRequests.get(originalDedupKey);
163
+ if (originalRequest)
164
+ return originalRequest;
165
+ if (Object.keys(elements).every(isRegistered))
166
+ return;
167
+ const loadsControllers = backendLoadsControllers(options.backend);
168
+ const unresolvedElements = {};
169
+ for (const [tag, packageSpec] of Object.entries(elements)) {
170
+ if (!isRegistered(tag) || (loadsControllers && awaitsController(tag))) {
171
+ unresolvedElements[tag] = packageSpec;
172
+ }
173
+ }
174
+ const tags = Object.keys(unresolvedElements);
157
175
  const timeoutMs = options.whenDefinedTimeoutMs ?? DEFAULT_WHEN_DEFINED_TIMEOUT_MS;
158
176
  const loadTimeoutMs = options.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS;
159
- // Fast path: everything already registered. Skip backend and dedup
160
- // bookkeeping entirely.
161
- if (allAlreadyRegistered(tags))
162
- return;
163
- const dedupKey = buildDedupKey(options.backend, elements);
177
+ const dedupKey = buildDedupKey(options.backend, unresolvedElements);
164
178
  const existing = inFlightRequests.get(dedupKey);
165
- if (existing)
179
+ if (existing) {
180
+ aliasInFlightRequest(originalDedupKey, existing);
166
181
  return existing;
182
+ }
167
183
  const backend = resolveBackend(options.backend);
168
184
  const doc = resolveDoc(options.doc);
169
- const promise = runEnsureRegistered(elements, tags, backend, { doc, whenDefinedTimeoutMs: timeoutMs }, timeoutMs, loadTimeoutMs);
170
- inFlightRequests.set(dedupKey, promise);
185
+ const loadElements = isIifeConfig(options.backend)
186
+ ? iifeLoadElements(elements, unresolvedElements)
187
+ : unresolvedElements;
188
+ const promise = runEnsureRegistered(loadElements, tags, backend, { doc, whenDefinedTimeoutMs: timeoutMs }, timeoutMs, loadTimeoutMs);
189
+ aliasInFlightRequest(originalDedupKey, promise);
190
+ aliasInFlightRequest(dedupKey, promise);
171
191
  try {
172
192
  await promise;
173
193
  }
174
194
  finally {
175
- if (inFlightRequests.get(dedupKey) === promise) {
176
- inFlightRequests.delete(dedupKey);
195
+ for (const key of inFlightKeysByPromise.get(promise) ?? []) {
196
+ if (inFlightRequests.get(key) === promise)
197
+ inFlightRequests.delete(key);
177
198
  }
199
+ inFlightKeysByPromise.delete(promise);
178
200
  }
179
201
  }
202
+ function aliasInFlightRequest(key, promise) {
203
+ inFlightRequests.set(key, promise);
204
+ const keys = inFlightKeysByPromise.get(promise) ?? new Set();
205
+ keys.add(key);
206
+ inFlightKeysByPromise.set(promise, keys);
207
+ }
180
208
  async function runEnsureRegistered(elements, tags, backend, context, timeoutMs, loadTimeoutMs) {
181
209
  let adapterError;
182
210
  try {
@@ -273,13 +301,13 @@ function resolveBackend(backend) {
273
301
  if (isDirectBackend(backend))
274
302
  return backend;
275
303
  if ("kind" in backend) {
304
+ const backendKey = backendKeyOf(backend);
305
+ const override = resolvedBackendOverrides.get(backendKey);
306
+ if (override)
307
+ return override;
276
308
  if (backend.kind === "iife")
277
309
  return createIifeBackend(backend);
278
310
  if (backend.kind === "esm") {
279
- const backendKey = backendKeyOf(backend);
280
- const override = resolvedBackendOverrides.get(backendKey);
281
- if (override)
282
- return override;
283
311
  const existing = resolvedEsmBackends.get(backendKey);
284
312
  if (existing)
285
313
  return existing;
@@ -415,8 +443,32 @@ function getOpaqueObjectRefId(value) {
415
443
  }
416
444
  return id;
417
445
  }
418
- function allAlreadyRegistered(tags) {
419
- return tags.every((tag) => isRegistered(tag));
446
+ function isIifeConfig(backend) {
447
+ return !isDirectBackend(backend) && "kind" in backend && backend.kind === "iife";
448
+ }
449
+ /**
450
+ * An IIFE bundle URL names its packages, so the full map keeps it the URL a
451
+ * host already fetched. Two specs of one package cannot share a bundle, so a
452
+ * conflict narrows the map: first to the unresolved tags, then to the undefined.
453
+ */
454
+ function iifeLoadElements(elements, unresolvedElements) {
455
+ if (!findIifeElementMapConflict(elements))
456
+ return elements;
457
+ if (!findIifeElementMapConflict(unresolvedElements))
458
+ return unresolvedElements;
459
+ return Object.fromEntries(Object.entries(unresolvedElements).filter(([tag]) => !isRegistered(tag)));
460
+ }
461
+ function backendLoadsControllers(backend) {
462
+ if (isDirectBackend(backend) || !("kind" in backend))
463
+ return false;
464
+ if (backend.kind === "iife")
465
+ return backend.needsControllers ?? true;
466
+ if (backend.kind === "esm")
467
+ return backend.loadControllers ?? true;
468
+ return false;
469
+ }
470
+ function awaitsController(tag) {
471
+ return typeof window !== "undefined" && !pieRegistry()[tag]?.controller;
420
472
  }
421
473
  function isRegistered(tag) {
422
474
  if (typeof customElements === "undefined")
@@ -471,6 +523,7 @@ function extractAdapterReason(err, tag) {
471
523
  export const __testing = {
472
524
  resetDedupState() {
473
525
  inFlightRequests.clear();
526
+ inFlightKeysByPromise.clear();
474
527
  resolvedEsmBackends.clear();
475
528
  resolvedBackendOverrides.clear();
476
529
  clearPackageMetadataRequests();
@@ -21,6 +21,7 @@
21
21
  import type { InstrumentationProvider } from "../instrumentation/types.js";
22
22
  import { type IifeBundleRetryConfig } from "../loader-config.js";
23
23
  import { BundleType } from "../pie/types.js";
24
+ import type { ElementMap } from "./ElementLoader.js";
24
25
  import { type ElementLoaderBackend } from "./element-loader-types.js";
25
26
  /**
26
27
  * Bundle-build retry status emitted while a bundle is being built or after
@@ -77,4 +78,10 @@ export type IifeBackendConfig = {
77
78
  instrumentationProvider?: InstrumentationProvider;
78
79
  };
79
80
  export type IifeBackend = ElementLoaderBackend & {};
81
+ /**
82
+ * The legacy IIFE bundle global is keyed by unversioned package name. Two
83
+ * distinct specs for one package therefore cannot coexist in a single bundle:
84
+ * one constructor would silently be reused for both requested versioned tags.
85
+ */
86
+ export declare function findIifeElementMapConflict(elements: ElementMap): string | undefined;
80
87
  export declare function createIifeBackend(config: IifeBackendConfig): IifeBackend;
@@ -31,7 +31,7 @@ import { AdapterFailure, } from "./element-loader-types.js";
31
31
  * distinct specs for one package therefore cannot coexist in a single bundle:
32
32
  * one constructor would silently be reused for both requested versioned tags.
33
33
  */
34
- function findIifeElementMapConflict(elements) {
34
+ export function findIifeElementMapConflict(elements) {
35
35
  const specsByPackageName = new Map();
36
36
  for (const packageSpecValue of Object.values(elements)) {
37
37
  const packageSpec = String(packageSpecValue);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Hostname classification for a URL a server fetches on someone else's behalf,
3
+ * such as an SSML `<audio src>` a TTS provider will request or an asset URL an
4
+ * upstream service returns. Every such check reads this module, so no two
5
+ * copies of it can drift apart.
6
+ *
7
+ * IP literals are classified by `ipaddr.js`, whose ranges follow the IANA IPv4
8
+ * and IPv6 Special-Purpose Address Registries (RFC 6890). A host is private
9
+ * unless it is a public unicast address or a name: loopback, private, link-local,
10
+ * unique-local, carrier-grade NAT, multicast, broadcast, documentation and
11
+ * reserved ranges, and the IPv6 forms that embed an IPv4 address (NAT64, 6to4,
12
+ * Teredo), all count. `ipaddr.js` also reads the numeric IPv4 forms (`2130706433`,
13
+ * `0x7f000001`, `0177.0.0.1`) and unmaps IPv4-mapped IPv6.
14
+ *
15
+ * Only the literal is checked. A public name that resolves to a private address
16
+ * passes; guarding that takes a check of the resolved address at connect time.
17
+ *
18
+ * @module
19
+ */
20
+ /** Whether `hostname` is a cloud instance-metadata endpoint. */
21
+ export declare function isCloudMetadataHostname(hostname: string): boolean;
22
+ /**
23
+ * Whether `hostname` is anything other than a public unicast address or a
24
+ * public name: `localhost` and its subdomains (RFC 6761), or an IP literal in
25
+ * any special-purpose range.
26
+ */
27
+ export declare function isPrivateHostname(hostname: string): boolean;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Hostname classification for a URL a server fetches on someone else's behalf,
3
+ * such as an SSML `<audio src>` a TTS provider will request or an asset URL an
4
+ * upstream service returns. Every such check reads this module, so no two
5
+ * copies of it can drift apart.
6
+ *
7
+ * IP literals are classified by `ipaddr.js`, whose ranges follow the IANA IPv4
8
+ * and IPv6 Special-Purpose Address Registries (RFC 6890). A host is private
9
+ * unless it is a public unicast address or a name: loopback, private, link-local,
10
+ * unique-local, carrier-grade NAT, multicast, broadcast, documentation and
11
+ * reserved ranges, and the IPv6 forms that embed an IPv4 address (NAT64, 6to4,
12
+ * Teredo), all count. `ipaddr.js` also reads the numeric IPv4 forms (`2130706433`,
13
+ * `0x7f000001`, `0177.0.0.1`) and unmaps IPv4-mapped IPv6.
14
+ *
15
+ * Only the literal is checked. A public name that resolves to a private address
16
+ * passes; guarding that takes a check of the resolved address at connect time.
17
+ *
18
+ * @module
19
+ */
20
+ import ipaddr from "ipaddr.js";
21
+ /**
22
+ * Cloud instance-metadata hostnames. They hand out credentials. No registry
23
+ * lists them; each is the provider's documented name.
24
+ */
25
+ const CLOUD_METADATA_HOSTNAMES = new Set([
26
+ "metadata.google.internal",
27
+ "metadata.azure.internal",
28
+ "metadata.packet.net",
29
+ "metadata.oci.oraclecloud.com",
30
+ ]);
31
+ /** Cloud instance-metadata addresses: the link-local one most clouds use, and AWS's IPv6 one. */
32
+ const CLOUD_METADATA_ADDRESSES = ["169.254.169.254", "fd00:ec2::254"].map((address) => ipaddr.parse(address));
33
+ /** Lower-cased, without brackets or a trailing dot. */
34
+ function normalizeHostname(hostname) {
35
+ return hostname
36
+ .replace(/^\[|\]$/g, "")
37
+ .replace(/\.$/, "")
38
+ .toLowerCase();
39
+ }
40
+ /**
41
+ * The address `host` is a literal for, IPv4-mapped (`::ffff:a.b.c.d`) and
42
+ * IPv4-compatible (`::a.b.c.d`) IPv6 reduced to IPv4, or `null` for a name.
43
+ */
44
+ function parseAddress(host) {
45
+ if (!ipaddr.isValid(host))
46
+ return null;
47
+ const address = ipaddr.process(host);
48
+ if (address.kind() !== "ipv6")
49
+ return address;
50
+ // `ipaddr.js` classifies deprecated IPv4-compatible addresses as unicast;
51
+ // read the IPv4 address in their low 32 bits instead.
52
+ const { parts } = address;
53
+ const isIpv4Compatible = parts.slice(0, 6).every((part) => part === 0) &&
54
+ (parts[6] !== 0 || parts[7] > 1);
55
+ return isIpv4Compatible
56
+ ? new ipaddr.IPv4([
57
+ parts[6] >> 8,
58
+ parts[6] & 0xff,
59
+ parts[7] >> 8,
60
+ parts[7] & 0xff,
61
+ ])
62
+ : address;
63
+ }
64
+ /** Whether `hostname` is a cloud instance-metadata endpoint. */
65
+ export function isCloudMetadataHostname(hostname) {
66
+ const host = normalizeHostname(hostname);
67
+ if (CLOUD_METADATA_HOSTNAMES.has(host))
68
+ return true;
69
+ const address = parseAddress(host);
70
+ return (address !== null &&
71
+ CLOUD_METADATA_ADDRESSES.some((metadata) => metadata.kind() === address.kind() &&
72
+ metadata.toNormalizedString() === address.toNormalizedString()));
73
+ }
74
+ /**
75
+ * Whether `hostname` is anything other than a public unicast address or a
76
+ * public name: `localhost` and its subdomains (RFC 6761), or an IP literal in
77
+ * any special-purpose range.
78
+ */
79
+ export function isPrivateHostname(hostname) {
80
+ const host = normalizeHostname(hostname);
81
+ if (host === "localhost" || host.endsWith(".localhost"))
82
+ return true;
83
+ const address = parseAddress(host);
84
+ return address !== null && address.range() !== "unicast";
85
+ }
@@ -30,11 +30,11 @@ export function validateExternalStyleUrl(url, options = {}) {
30
30
  ? new URL(url, options.baseUrl)
31
31
  : new URL(url);
32
32
  }
33
- catch (err) {
33
+ catch {
34
34
  return {
35
35
  ok: false,
36
36
  reason: "invalid-url",
37
- message: `External stylesheet URL could not be parsed: ${String(err)}`,
37
+ message: "External stylesheet URL could not be parsed.",
38
38
  };
39
39
  }
40
40
  if (resolvedUrl.protocol !== "http:" && resolvedUrl.protocol !== "https:") {
@@ -13,6 +13,7 @@
13
13
  * corpus behind one is licensed per programme, so a host supplies either an endpoint
14
14
  * or a resolver of its own.
15
15
  */
16
+ import type { TextDirection } from "../i18n/types.js";
16
17
  /** What a host's term service is asked for. */
17
18
  export interface TermLookupRequest {
18
19
  /** The term, already normalised by {@link normalizeTerm}. */
@@ -187,3 +188,17 @@ export declare function termPanelStatusMessage<TItem>(state: TermPanelState<TIte
187
188
  countLabel: (count: number) => string;
188
189
  emptyLabel: (term: string) => string;
189
190
  }): string;
191
+ /**
192
+ * `lang` and `dir` for a lookup's results: the language the lookup asked in.
193
+ *
194
+ * Results are in the corpus's language, which is neither the panel's interface
195
+ * locale nor necessarily the content the term came from: a Spanish gloss for an
196
+ * English passage. Stamped on the results, an Arabic definition in an English panel
197
+ * reads and lays out as Arabic, and the panel's language never reaches the content.
198
+ * With no language named the corpus's is unknown, so the stamp is `lang=""` and
199
+ * `dir="auto"`, the one undeclared content gets.
200
+ */
201
+ export declare function termResultLanguage(language: string | undefined): {
202
+ lang: string;
203
+ dir: TextDirection | "auto";
204
+ };
@@ -13,6 +13,8 @@
13
13
  * corpus behind one is licensed per programme, so a host supplies either an endpoint
14
14
  * or a resolver of its own.
15
15
  */
16
+ import { toLanguageTag } from "../i18n/language-tags.js";
17
+ import { localeDirection } from "../i18n/provider.js";
16
18
  /** Longest term worth sending. Past this it is prose, not a headword. */
17
19
  export const MAX_LOOKUP_TERM_LENGTH = 80;
18
20
  /** Words a term may span. Generous enough for `carbon dioxide` and `mother in law`. */
@@ -282,3 +284,17 @@ export function termPanelStatusMessage(state, labels) {
282
284
  return labels.emptyLabel(state.term);
283
285
  return "";
284
286
  }
287
+ /**
288
+ * `lang` and `dir` for a lookup's results: the language the lookup asked in.
289
+ *
290
+ * Results are in the corpus's language, which is neither the panel's interface
291
+ * locale nor necessarily the content the term came from: a Spanish gloss for an
292
+ * English passage. Stamped on the results, an Arabic definition in an English panel
293
+ * reads and lays out as Arabic, and the panel's language never reaches the content.
294
+ * With no language named the corpus's is unknown, so the stamp is `lang=""` and
295
+ * `dir="auto"`, the one undeclared content gets.
296
+ */
297
+ export function termResultLanguage(language) {
298
+ const tag = toLanguageTag(language);
299
+ return tag ? { lang: tag, dir: localeDirection(tag) } : { lang: "", dir: "auto" };
300
+ }