@expofp/config 3.11.10
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/README.md +35 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/lib/apply-intents.d.ts +10 -0
- package/dist/lib/apply-intents.js +24 -0
- package/dist/lib/config-store.d.ts +6 -0
- package/dist/lib/config-store.js +12 -0
- package/dist/lib/debug-settings.d.ts +16 -0
- package/dist/lib/debug-settings.js +44 -0
- package/dist/lib/json-schema.d.ts +67 -0
- package/dist/lib/json-schema.js +136 -0
- package/dist/lib/legacy-url.d.ts +167 -0
- package/dist/lib/legacy-url.js +322 -0
- package/dist/lib/load-config.d.ts +26 -0
- package/dist/lib/load-config.js +286 -0
- package/dist/lib/local-storage-codec.d.ts +44 -0
- package/dist/lib/local-storage-codec.js +76 -0
- package/dist/lib/normalize-fp-svg.d.ts +26 -0
- package/dist/lib/normalize-fp-svg.js +36 -0
- package/dist/lib/normalize-legacy-data.d.ts +13 -0
- package/dist/lib/normalize-legacy-data.js +90 -0
- package/dist/lib/rebooking.d.ts +19 -0
- package/dist/lib/rebooking.js +48 -0
- package/dist/lib/resource-urls.d.ts +52 -0
- package/dist/lib/resource-urls.js +196 -0
- package/dist/lib/serialize-config-resources.d.ts +22 -0
- package/dist/lib/serialize-config-resources.js +20 -0
- package/dist/lib/strip-defaults.d.ts +27 -0
- package/dist/lib/strip-defaults.js +55 -0
- package/dist/lib/url-codec.d.ts +46 -0
- package/dist/lib/url-codec.js +165 -0
- package/dist/lib/url-intents.d.ts +30 -0
- package/dist/lib/url-intents.js +58 -0
- package/dist/lib/validate-flag.d.ts +10 -0
- package/dist/lib/validate-flag.js +36 -0
- package/dist/lib/validate-timing.d.ts +3 -0
- package/dist/lib/validate-timing.js +52 -0
- package/dist/lib/visit-resources.d.ts +85 -0
- package/dist/lib/visit-resources.js +198 -0
- package/package.json +36 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* localStorage codec — value ⇄ key/value store (`localStorage`), with coercion
|
|
3
|
+
* driven by an ordinary Zod schema (no `z.coerce` anywhere):
|
|
4
|
+
*
|
|
5
|
+
* parseFromStorage(schema, storage) store → validated value
|
|
6
|
+
* writeToStorage(value, storage) value → store
|
|
7
|
+
* clearStorage(schema, storage) remove the schema's keys
|
|
8
|
+
*
|
|
9
|
+
* Key layout — one `efp-`-prefixed key per top-level field, devtools-readable
|
|
10
|
+
* and consistent with the app's existing keys (`efp-locale`, `efp-telemetry`):
|
|
11
|
+
* • SCALAR fields (boolean/number/string/enum) → plain string value:
|
|
12
|
+
* efp-enableExperimentalMaplibre = "true"
|
|
13
|
+
* efp-debugPattern = "map:*"
|
|
14
|
+
* • OBJECT / ARRAY fields → JSON under one key:
|
|
15
|
+
* efp-bookmarks = ["a","b"]
|
|
16
|
+
*
|
|
17
|
+
* On decode, each scalar is coerced to its declared type by introspecting the
|
|
18
|
+
* schema (`z.toJSONSchema`); objects/arrays are JSON-parsed. Only prefixed keys
|
|
19
|
+
* are ever read or written — the rest of the store is never touched. The
|
|
20
|
+
* top-level schema must be an object.
|
|
21
|
+
*/
|
|
22
|
+
import type * as z from 'zod';
|
|
23
|
+
/** The subset of the DOM `Storage` API used here — `window.localStorage` satisfies it. */
|
|
24
|
+
export interface StorageLike {
|
|
25
|
+
getItem(key: string): string | null;
|
|
26
|
+
setItem(key: string, value: string): void;
|
|
27
|
+
removeItem(key: string): void;
|
|
28
|
+
}
|
|
29
|
+
export interface StorageCodecOptions {
|
|
30
|
+
/** Key prefix. Default `'efp-'`. */
|
|
31
|
+
prefix?: string;
|
|
32
|
+
}
|
|
33
|
+
/** The SDK's localStorage key prefix (`efp-locale`, `efp-telemetry`, …). */
|
|
34
|
+
export declare const DEFAULT_STORAGE_PREFIX = "efp-";
|
|
35
|
+
export declare function parseFromStorage<S extends z.ZodType>(schema: S, storage: StorageLike, options?: StorageCodecOptions): z.infer<S>;
|
|
36
|
+
export declare function safeParseFromStorage<S extends z.ZodType>(schema: S, storage: StorageLike, options?: StorageCodecOptions): z.ZodSafeParseResult<z.core.output<S>>;
|
|
37
|
+
export declare function writeToStorage(value: object, storage: StorageLike, options?: StorageCodecOptions): void;
|
|
38
|
+
/** Remove every key this schema owns (its prefixed keys) from the store. */
|
|
39
|
+
export declare function clearStorage(schema: z.ZodType, storage: StorageLike, options?: StorageCodecOptions): void;
|
|
40
|
+
/** A tiny in-memory `StorageLike` for tests, SSR, or when there is no `localStorage`. */
|
|
41
|
+
export declare function createMemoryStorage(initial?: Record<string, string>): StorageLike & {
|
|
42
|
+
dump(): Record<string, string>;
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=local-storage-codec.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { coerceScalar, toJsonSchema } from './json-schema.js';
|
|
2
|
+
/** The SDK's localStorage key prefix (`efp-locale`, `efp-telemetry`, …). */
|
|
3
|
+
export const DEFAULT_STORAGE_PREFIX = 'efp-';
|
|
4
|
+
/* ── Decode: store → value ─────────────────────────────────────────────────── */
|
|
5
|
+
export function parseFromStorage(schema, storage, options = {}) {
|
|
6
|
+
return schema.parse(readRecord(schema, storage, options.prefix ?? DEFAULT_STORAGE_PREFIX));
|
|
7
|
+
}
|
|
8
|
+
export function safeParseFromStorage(schema, storage, options = {}) {
|
|
9
|
+
return schema.safeParse(readRecord(schema, storage, options.prefix ?? DEFAULT_STORAGE_PREFIX));
|
|
10
|
+
}
|
|
11
|
+
function readRecord(schema, storage, prefix) {
|
|
12
|
+
const record = {};
|
|
13
|
+
for (const [field, node] of Object.entries(toJsonSchema(schema).properties ?? {})) {
|
|
14
|
+
const raw = storage.getItem(prefix + field);
|
|
15
|
+
if (raw === null) {
|
|
16
|
+
continue; // absent → let the schema apply its default / optionality
|
|
17
|
+
}
|
|
18
|
+
record[field] = readValue(node, raw);
|
|
19
|
+
}
|
|
20
|
+
return record;
|
|
21
|
+
}
|
|
22
|
+
function readValue(node, raw) {
|
|
23
|
+
if (node.type === 'object' || node.type === 'array' || node.anyOf || node.oneOf) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return raw; // malformed JSON → hand the string to schema.parse for a clear error
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return coerceScalar(raw, node.type);
|
|
32
|
+
}
|
|
33
|
+
/* ── Encode: value → store (schema-agnostic) ───────────────────────────────── */
|
|
34
|
+
export function writeToStorage(value, storage, options = {}) {
|
|
35
|
+
const prefix = options.prefix ?? DEFAULT_STORAGE_PREFIX;
|
|
36
|
+
for (const [field, item] of Object.entries(value)) {
|
|
37
|
+
const key = prefix + field;
|
|
38
|
+
if (item === undefined || item === null) {
|
|
39
|
+
storage.removeItem(key);
|
|
40
|
+
}
|
|
41
|
+
else if (typeof item === 'object') {
|
|
42
|
+
storage.setItem(key, JSON.stringify(item)); // objects & arrays → JSON
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
storage.setItem(key, String(item)); // scalars → plain string
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Remove every key this schema owns (its prefixed keys) from the store. */
|
|
50
|
+
export function clearStorage(schema, storage, options = {}) {
|
|
51
|
+
const prefix = options.prefix ?? DEFAULT_STORAGE_PREFIX;
|
|
52
|
+
for (const field of Object.keys(toJsonSchema(schema).properties ?? {})) {
|
|
53
|
+
storage.removeItem(prefix + field);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/* ── Helpers ───────────────────────────────────────────────────────────────── */
|
|
57
|
+
/** A tiny in-memory `StorageLike` for tests, SSR, or when there is no `localStorage`. */
|
|
58
|
+
export function createMemoryStorage(initial = {}) {
|
|
59
|
+
const map = new Map(Object.entries(initial));
|
|
60
|
+
return {
|
|
61
|
+
getItem: (key) => (map.has(key) ? (map.get(key)) : null),
|
|
62
|
+
setItem: (key, value) => {
|
|
63
|
+
map.set(key, value);
|
|
64
|
+
},
|
|
65
|
+
removeItem: (key) => {
|
|
66
|
+
map.delete(key);
|
|
67
|
+
},
|
|
68
|
+
dump: () => {
|
|
69
|
+
const out = {};
|
|
70
|
+
map.forEach((value, key) => {
|
|
71
|
+
out[key] = value;
|
|
72
|
+
});
|
|
73
|
+
return out;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FpSvgLayerJs } from '@expofp/schema';
|
|
2
|
+
/**
|
|
3
|
+
* Fold the layer-suffixed drawing globals onto the canonical keys, in place.
|
|
4
|
+
*
|
|
5
|
+
* `fp.svg.<layerName>.js` files come in two converter generations: older ones
|
|
6
|
+
* chain-assign every drawing global under both names (`window['__fpHall'] =
|
|
7
|
+
* window['__fp'] = …`), newer ones emit ONLY the suffixed globals
|
|
8
|
+
* (`window['__fpBooth'] = …` — the chain had every layer script clobber the
|
|
9
|
+
* shared `window.__fp`). The schemas and the floorplan know only the plain
|
|
10
|
+
* `__fp`/`__fpPaths` keys, so the loader folds the alias onto them on the
|
|
11
|
+
* payload's own copy, before the resource walk — the `svgAsset` href rewrite
|
|
12
|
+
* then covers a folded drawing like a plain one. The folded values skip the
|
|
13
|
+
* (opt-in) schema validation, which ran before the fold — the same trust the
|
|
14
|
+
* untouched alias keys always had.
|
|
15
|
+
*
|
|
16
|
+
* The eager `fp.svg.js` needs no fold: every runtime generation reads its
|
|
17
|
+
* plain `__fp`, so converters keep emitting it there. A resolved offline
|
|
18
|
+
* `fp.svg.<layerName>.json` may still carry only alias keys (serialization
|
|
19
|
+
* re-mints cold refs without the fold), and folds here at load like a live
|
|
20
|
+
* script.
|
|
21
|
+
*
|
|
22
|
+
* Legacy-format shim: retires with the window-assigning `.js` payloads once
|
|
23
|
+
* the platform serves resolved JSON documents.
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeFpSvgLayerAliases(payload: FpSvgLayerJs, layerName: string): void;
|
|
26
|
+
//# sourceMappingURL=normalize-fp-svg.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold the layer-suffixed drawing globals onto the canonical keys, in place.
|
|
3
|
+
*
|
|
4
|
+
* `fp.svg.<layerName>.js` files come in two converter generations: older ones
|
|
5
|
+
* chain-assign every drawing global under both names (`window['__fpHall'] =
|
|
6
|
+
* window['__fp'] = …`), newer ones emit ONLY the suffixed globals
|
|
7
|
+
* (`window['__fpBooth'] = …` — the chain had every layer script clobber the
|
|
8
|
+
* shared `window.__fp`). The schemas and the floorplan know only the plain
|
|
9
|
+
* `__fp`/`__fpPaths` keys, so the loader folds the alias onto them on the
|
|
10
|
+
* payload's own copy, before the resource walk — the `svgAsset` href rewrite
|
|
11
|
+
* then covers a folded drawing like a plain one. The folded values skip the
|
|
12
|
+
* (opt-in) schema validation, which ran before the fold — the same trust the
|
|
13
|
+
* untouched alias keys always had.
|
|
14
|
+
*
|
|
15
|
+
* The eager `fp.svg.js` needs no fold: every runtime generation reads its
|
|
16
|
+
* plain `__fp`, so converters keep emitting it there. A resolved offline
|
|
17
|
+
* `fp.svg.<layerName>.json` may still carry only alias keys (serialization
|
|
18
|
+
* re-mints cold refs without the fold), and folds here at load like a live
|
|
19
|
+
* script.
|
|
20
|
+
*
|
|
21
|
+
* Legacy-format shim: retires with the window-assigning `.js` payloads once
|
|
22
|
+
* the platform serves resolved JSON documents.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeFpSvgLayerAliases(payload, layerName) {
|
|
25
|
+
if (payload.__fp === undefined) {
|
|
26
|
+
const alias = payload[`__fp${layerName}`];
|
|
27
|
+
if (typeof alias === 'string')
|
|
28
|
+
payload.__fp = alias;
|
|
29
|
+
}
|
|
30
|
+
if (payload.__fpPaths === undefined) {
|
|
31
|
+
const alias = payload[`__fpPaths${layerName}`];
|
|
32
|
+
// the alias carries the same converter output as __fpPaths, just unvalidated
|
|
33
|
+
if (Array.isArray(alias))
|
|
34
|
+
payload.__fpPaths = alias;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-merge normalization of the legacy event data.
|
|
3
|
+
*
|
|
4
|
+
* Ports the floorplan's historical `data/validate-data.ts`, which mutated
|
|
5
|
+
* `window.__data` at module load. The config is now the single — and frozen —
|
|
6
|
+
* copy of that data, so the same fixes run here, inside `loadConfig`, after
|
|
7
|
+
* every layer is merged and before `deepFreeze`. The rules are kept verbatim
|
|
8
|
+
* (including the expo-specific ones marked for removal); only the opt-in
|
|
9
|
+
* console diagnostics were dropped — the schema parse validates instead.
|
|
10
|
+
*/
|
|
11
|
+
import { type Config } from '@expofp/schema';
|
|
12
|
+
export declare function normalizeLegacyData(config: Config, legacyDataUrlBase?: string): void;
|
|
13
|
+
//# sourceMappingURL=normalize-legacy-data.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { withVariantUrl } from './resource-urls.js';
|
|
2
|
+
/** One-off promo for the `expo` event — exhibitor ids forced to advertise. */
|
|
3
|
+
const EXPO_EXPO_ADS = [2567, 2704, 2681, 2592, 2740, 2709, 2482, 2609, 2734, 2696, 2840, 2566, 2736];
|
|
4
|
+
export function normalizeLegacyData(config, legacyDataUrlBase) {
|
|
5
|
+
const expo = config.expo;
|
|
6
|
+
const booths = (config.booths ?? []);
|
|
7
|
+
const exhibitors = (config.exhibitors ?? []);
|
|
8
|
+
// data.js may carry explicit nulls (the layer merge skips only `undefined`),
|
|
9
|
+
// and the effective config's arrays are non-nullable — the fallbacks must
|
|
10
|
+
// land back on the config, not just in the locals above.
|
|
11
|
+
config.booths = booths;
|
|
12
|
+
config.exhibitors = exhibitors;
|
|
13
|
+
config.categories ??= [];
|
|
14
|
+
// free plans force the ad and disable featured placement
|
|
15
|
+
if (config.free) {
|
|
16
|
+
config.noFeatured = true;
|
|
17
|
+
config.expoFpAd = true;
|
|
18
|
+
}
|
|
19
|
+
// an unexpired trial marks the event as a trial
|
|
20
|
+
if (config.trialExpiresAt) {
|
|
21
|
+
const expires = new Date(config.trialExpiresAt).getTime();
|
|
22
|
+
if (Date.now() < expires) {
|
|
23
|
+
config.isTrial = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// temporary workaround for invalid data.js
|
|
27
|
+
if (exhibitors.length > 0) {
|
|
28
|
+
for (const booth of booths) {
|
|
29
|
+
delete booth.reserved;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// exhibitor logos: always the object form on the effective config — the
|
|
33
|
+
// legacy string form (already absolutized) converts here, the platform's
|
|
34
|
+
// pre-scaled __small/__tiny siblings becoming explicit variant URLs
|
|
35
|
+
for (const exhibitor of exhibitors) {
|
|
36
|
+
if (typeof exhibitor.logo === 'string') {
|
|
37
|
+
exhibitor.logo = exhibitor.logo
|
|
38
|
+
? {
|
|
39
|
+
url: exhibitor.logo,
|
|
40
|
+
smallUrl: withVariantUrl(exhibitor.logo, '__small'),
|
|
41
|
+
tinyUrl: withVariantUrl(exhibitor.logo, '__tiny'),
|
|
42
|
+
}
|
|
43
|
+
: { url: exhibitor.logo };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// some data fixes (expo-specific will be removed)
|
|
47
|
+
if (!config.logo && expo === 'expo')
|
|
48
|
+
config.logo = legacyAssetUrl('../logo.svg', legacyDataUrlBase);
|
|
49
|
+
if (!config.logo)
|
|
50
|
+
config.logo = legacyAssetUrl(`../${expo}-logo.png`, legacyDataUrlBase);
|
|
51
|
+
if (!config.homeUrl && expo === 'jtrade19')
|
|
52
|
+
config.homeUrl = 'https://www.jtrade.co.uk/';
|
|
53
|
+
// this is permanent
|
|
54
|
+
if (!config.boothTerm)
|
|
55
|
+
config.boothTerm = 'Booth';
|
|
56
|
+
if (!config.exhibitorTermPlural)
|
|
57
|
+
config.exhibitorTermPlural = 'Exhibitors';
|
|
58
|
+
if (expo === 'expo') {
|
|
59
|
+
exhibitors
|
|
60
|
+
.filter((x) => x.logo?.url && EXPO_EXPO_ADS.includes(x.id))
|
|
61
|
+
.forEach((x) => (x.advertise = true));
|
|
62
|
+
}
|
|
63
|
+
// convert obsolete fields
|
|
64
|
+
for (const booth of booths) {
|
|
65
|
+
if (!booth.special) {
|
|
66
|
+
if (typeof booth.onHold === 'undefined')
|
|
67
|
+
booth.onHold = booth.isOnHold;
|
|
68
|
+
if (typeof booth.availColor === 'undefined')
|
|
69
|
+
booth.availColor = booth.availableColor;
|
|
70
|
+
if (typeof booth.type === 'undefined')
|
|
71
|
+
booth.type = booth.boothTypeName;
|
|
72
|
+
}
|
|
73
|
+
booth.exhibitors = booth.exhibitors ?? [];
|
|
74
|
+
}
|
|
75
|
+
for (const exhibitor of exhibitors) {
|
|
76
|
+
if (typeof exhibitor.featured === 'undefined')
|
|
77
|
+
exhibitor.featured = exhibitor.isFeatured;
|
|
78
|
+
if (typeof exhibitor.email === 'undefined')
|
|
79
|
+
exhibitor.email = exhibitor.publicEmail;
|
|
80
|
+
exhibitor.categories = exhibitor.categories ?? [];
|
|
81
|
+
}
|
|
82
|
+
// disable featured placement for free plans
|
|
83
|
+
if (config.noFeatured) {
|
|
84
|
+
exhibitors.forEach((e) => (e.featured = false));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The historical logo fallbacks are data-dir-relative paths — rebase them onto the data directory. */
|
|
88
|
+
function legacyAssetUrl(path, base) {
|
|
89
|
+
return base ? `${base.replace(/\/+$/, '')}/${path}` : path;
|
|
90
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebooking layer of `loadConfig`.
|
|
3
|
+
*
|
|
4
|
+
* When a rebooking token is present (page query `?rt=…`, retained in
|
|
5
|
+
* `sessionStorage` across in-session navigations), fetch the per-exhibitor
|
|
6
|
+
* rebooking state from the platform and merge it into the not-yet-frozen
|
|
7
|
+
* config: `exhibitors[].rebookingState` / `rebookingNote` plus the
|
|
8
|
+
* `isRebooking` flag. Ported from the floorplan loader's init-time merge so
|
|
9
|
+
* the frozen config stays the single, complete copy of the event data. The
|
|
10
|
+
* floorplan keeps its own `tools/rebookingUrl.ts` helpers for UI *actions*
|
|
11
|
+
* against the same API (e.g. `set-rebooking-state`).
|
|
12
|
+
*
|
|
13
|
+
* Without a token — every ordinary visitor, and Node callers that pass no
|
|
14
|
+
* `url` — this is a synchronous no-op apart from `isRebooking = false`.
|
|
15
|
+
*/
|
|
16
|
+
import { type Config } from '@expofp/schema';
|
|
17
|
+
/** Merge rebooking state into `config` when a token is present. Never throws. */
|
|
18
|
+
export declare function applyRebookingData(config: Config, url: string | undefined): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=rebooking.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const TOKEN_KEY = 'rt';
|
|
2
|
+
const REBOOKING_API_ORIGIN = 'https://app.expofp.com/';
|
|
3
|
+
/** Merge rebooking state into `config` when a token is present. Never throws. */
|
|
4
|
+
export async function applyRebookingData(config, url) {
|
|
5
|
+
try {
|
|
6
|
+
const token = getRebookingToken(url);
|
|
7
|
+
if (!token) {
|
|
8
|
+
config.isRebooking = false;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
retainRebookingToken(token);
|
|
12
|
+
const apiUrl = new URL('api/rebooking-data', REBOOKING_API_ORIGIN);
|
|
13
|
+
apiUrl.searchParams.set(TOKEN_KEY, token);
|
|
14
|
+
apiUrl.searchParams.set('expoKey', config.expo);
|
|
15
|
+
const resp = await fetch(apiUrl.href);
|
|
16
|
+
if (resp.ok) {
|
|
17
|
+
const rebooking = (await resp.json());
|
|
18
|
+
mergeRebookingState(config, rebooking.exhibitors ?? []);
|
|
19
|
+
}
|
|
20
|
+
config.isRebooking = resp.ok;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// parity with the old loader merge: report and continue without rebooking
|
|
24
|
+
console.error(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Token from the page query (same decode the floorplan helpers use), else the retained one. */
|
|
28
|
+
function getRebookingToken(url) {
|
|
29
|
+
const fromQuery = url ? new URLSearchParams(decodeURIComponent(url)).get(TOKEN_KEY) : null;
|
|
30
|
+
if (fromQuery)
|
|
31
|
+
return fromQuery;
|
|
32
|
+
return typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(TOKEN_KEY) : null;
|
|
33
|
+
}
|
|
34
|
+
function retainRebookingToken(token) {
|
|
35
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
36
|
+
sessionStorage.setItem(TOKEN_KEY, token);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function mergeRebookingState(config, incoming) {
|
|
40
|
+
const exhibitors = config.exhibitors ?? [];
|
|
41
|
+
for (const rebooking of incoming) {
|
|
42
|
+
const exhibitor = exhibitors.find((x) => x.id === rebooking.id);
|
|
43
|
+
if (exhibitor) {
|
|
44
|
+
exhibitor.rebookingState = rebooking.rebookingState;
|
|
45
|
+
exhibitor.rebookingNote = rebooking.rebookingNote;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal URL/text helpers for the schema-tagged resource kinds. Nothing here
|
|
3
|
+
* is public API — consumers only ever see the effects: `resolve(ref, {
|
|
4
|
+
* schema })` settles documents with absolute resource URLs, and
|
|
5
|
+
* `visitResources` enumerates/rewrites them.
|
|
6
|
+
*/
|
|
7
|
+
/** Prepend `baseUrl` to a relative path so it becomes a full URL; absolute paths pass through. */
|
|
8
|
+
export declare function toAbsoluteUrl(path: string, baseUrl: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Scheme-qualified (`https:`, `data:`, `blob:`…), protocol-relative
|
|
11
|
+
* (`//host/…`), or rooted (`/…` — already resolved, just host-less). Note a
|
|
12
|
+
* page-relative offline path (`host-slug/data/…`, no leading slash) is NOT
|
|
13
|
+
* absolute: it is resolved against the page at runtime, so document loads must
|
|
14
|
+
* not rebase it (see `documentRef`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function isAbsoluteUrl(path: string): boolean;
|
|
17
|
+
/** The ref's own directory — the base its document's relative paths resolve against. */
|
|
18
|
+
export declare function refDirectory(ref: string): string;
|
|
19
|
+
/** Every distinct `<image>` asset href in one SVG document string. */
|
|
20
|
+
export declare function collectSvgImageHrefs(svg: string): string[];
|
|
21
|
+
/** Rewrite every `<image>` asset href in one SVG document string. */
|
|
22
|
+
export declare function rewriteSvgImageHrefs(svg: string, rewrite: (href: string) => string): string;
|
|
23
|
+
/** Every distinct non-`data:` `url(…)` in one CSS text. */
|
|
24
|
+
export declare function collectCssUrls(css: string): string[];
|
|
25
|
+
/** Rewrite every non-`data:` `url(…)` in one CSS text. */
|
|
26
|
+
export declare function rewriteCssUrls(css: string, rewrite: (url: string) => string): string;
|
|
27
|
+
export interface InlinedCss {
|
|
28
|
+
css: string;
|
|
29
|
+
/** Import URLs left in place (conditional/unresolvable) — keep them verbatim, don't localize. */
|
|
30
|
+
external: Set<string>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Replace every unconditional `@import` with the imported stylesheet's text.
|
|
34
|
+
*
|
|
35
|
+
* An offline copy cannot keep stylesheets as fetched files: a URL like
|
|
36
|
+
* `https://fonts.googleapis.com/css2?family=…` saves to an extension-less
|
|
37
|
+
* path that static servers refuse to serve as `text/css` (stylesheet loads
|
|
38
|
+
* enforce MIME), and the file's inner `url(…)`s would stay remote. Splicing
|
|
39
|
+
* the text into the importing CSS instead lets the ordinary `url(…)`
|
|
40
|
+
* localization pass handle every asset the import pulled in.
|
|
41
|
+
*
|
|
42
|
+
* Inner URLs of an imported sheet resolve against the *sheet's* URL, so they
|
|
43
|
+
* are rebased to absolute before splicing; nested imports recurse the same
|
|
44
|
+
* way. A conditional import (layer/supports/media) has no faithful inline
|
|
45
|
+
* form — it is kept verbatim (still network-dependent) with a warning, as is
|
|
46
|
+
* a relative href with no base to resolve against. An import cycle fails the
|
|
47
|
+
* build loudly.
|
|
48
|
+
*/
|
|
49
|
+
export declare function inlineCssImports(css: string, fetchText: (url: string) => string | Promise<string>, base?: string, stack?: readonly string[]): Promise<InlinedCss>;
|
|
50
|
+
/** `…/name.ext?q` → `…/name<suffix>.ext?q` (query untouched). */
|
|
51
|
+
export declare function withVariantUrl(url: string, suffix: string): string;
|
|
52
|
+
//# sourceMappingURL=resource-urls.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal URL/text helpers for the schema-tagged resource kinds. Nothing here
|
|
3
|
+
* is public API — consumers only ever see the effects: `resolve(ref, {
|
|
4
|
+
* schema })` settles documents with absolute resource URLs, and
|
|
5
|
+
* `visitResources` enumerates/rewrites them.
|
|
6
|
+
*/
|
|
7
|
+
/** Prepend `baseUrl` to a relative path so it becomes a full URL; absolute paths pass through. */
|
|
8
|
+
export function toAbsoluteUrl(path, baseUrl) {
|
|
9
|
+
if (path === '' || isAbsoluteUrl(path)) {
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
return `${baseUrl.replace(/\/+$/, '')}/${path}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Scheme-qualified (`https:`, `data:`, `blob:`…), protocol-relative
|
|
16
|
+
* (`//host/…`), or rooted (`/…` — already resolved, just host-less). Note a
|
|
17
|
+
* page-relative offline path (`host-slug/data/…`, no leading slash) is NOT
|
|
18
|
+
* absolute: it is resolved against the page at runtime, so document loads must
|
|
19
|
+
* not rebase it (see `documentRef`).
|
|
20
|
+
*/
|
|
21
|
+
export function isAbsoluteUrl(path) {
|
|
22
|
+
return path.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(path);
|
|
23
|
+
}
|
|
24
|
+
/** The ref's own directory — the base its document's relative paths resolve against. */
|
|
25
|
+
export function refDirectory(ref) {
|
|
26
|
+
const path = ref.split(/[?#]/, 1)[0];
|
|
27
|
+
return path.slice(0, path.lastIndexOf('/') + 1);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `[...text.matchAll(regex)]` without `String.prototype.matchAll` — the
|
|
31
|
+
* collect functions run on the boot path and the SDK still targets Chrome
|
|
32
|
+
* 66–72 WebViews (matchAll is Chrome 73+). Resets `lastIndex` up front so the
|
|
33
|
+
* shared module-const `/g` regexes cannot carry scan state between calls.
|
|
34
|
+
*/
|
|
35
|
+
function execAll(regex, text) {
|
|
36
|
+
const matches = [];
|
|
37
|
+
regex.lastIndex = 0;
|
|
38
|
+
for (let match = regex.exec(text); match; match = regex.exec(text)) {
|
|
39
|
+
matches.push(match);
|
|
40
|
+
if (match[0] === '')
|
|
41
|
+
regex.lastIndex++; // zero-length match must not stall the scan
|
|
42
|
+
}
|
|
43
|
+
return matches;
|
|
44
|
+
}
|
|
45
|
+
/* ── SVG documents (`svgAsset`) — `<image>` hrefs are asset paths ─────────── */
|
|
46
|
+
const IMAGE_TAG = /<image\b[^>]*>/g;
|
|
47
|
+
const HREF_ATTR = /(\b(?:xlink:)?href\s*=\s*)(?:"([^"]*)"|'([^']*)')/g;
|
|
48
|
+
/** In-document (`#id`) and empty hrefs are not asset URLs. */
|
|
49
|
+
function isAssetHref(href) {
|
|
50
|
+
return !!href && !href.startsWith('#');
|
|
51
|
+
}
|
|
52
|
+
/** Every distinct `<image>` asset href in one SVG document string. */
|
|
53
|
+
export function collectSvgImageHrefs(svg) {
|
|
54
|
+
const hrefs = new Set();
|
|
55
|
+
if (!svg.includes('<image'))
|
|
56
|
+
return [];
|
|
57
|
+
for (const tag of svg.match(IMAGE_TAG) ?? []) {
|
|
58
|
+
for (const match of execAll(HREF_ATTR, tag)) {
|
|
59
|
+
const href = match[2] ?? match[3];
|
|
60
|
+
if (isAssetHref(href))
|
|
61
|
+
hrefs.add(href);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...hrefs];
|
|
65
|
+
}
|
|
66
|
+
/** Rewrite every `<image>` asset href in one SVG document string. */
|
|
67
|
+
export function rewriteSvgImageHrefs(svg, rewrite) {
|
|
68
|
+
if (!svg.includes('<image'))
|
|
69
|
+
return svg;
|
|
70
|
+
return svg.replace(IMAGE_TAG, (tag) => tag.replace(HREF_ATTR, (match, attr, doubleQuoted, singleQuoted) => {
|
|
71
|
+
const href = doubleQuoted ?? singleQuoted ?? '';
|
|
72
|
+
if (!isAssetHref(href))
|
|
73
|
+
return match;
|
|
74
|
+
const quote = doubleQuoted !== undefined ? '"' : "'";
|
|
75
|
+
return `${attr}${quote}${rewrite(href)}${quote}`;
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
/* ── CSS text (`cssAsset`) — `url(…)`s are asset paths ────────────────────── */
|
|
79
|
+
const CSS_URL = /url\(\s*(['"]?)(.*?)\1\s*\)/g;
|
|
80
|
+
/** Every distinct non-`data:` `url(…)` in one CSS text. */
|
|
81
|
+
export function collectCssUrls(css) {
|
|
82
|
+
const urls = new Set();
|
|
83
|
+
for (const match of execAll(CSS_URL, css)) {
|
|
84
|
+
const url = match[2];
|
|
85
|
+
if (url && !url.startsWith('data:'))
|
|
86
|
+
urls.add(url);
|
|
87
|
+
}
|
|
88
|
+
return [...urls];
|
|
89
|
+
}
|
|
90
|
+
/** Rewrite every non-`data:` `url(…)` in one CSS text. */
|
|
91
|
+
export function rewriteCssUrls(css, rewrite) {
|
|
92
|
+
return css.replace(CSS_URL, (match, quote, url) => {
|
|
93
|
+
if (!url || url.startsWith('data:'))
|
|
94
|
+
return match;
|
|
95
|
+
return `url(${quote}${rewrite(url)}${quote})`;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/* ── CSS `@import` inlining (offline serialization) ───────────────────────── */
|
|
99
|
+
// One `@import` rule: `url(…)` or bare-string href, then everything up to the
|
|
100
|
+
// terminating `;` (an optional layer/supports/media condition).
|
|
101
|
+
const CSS_IMPORT = /@import\s+(?:url\(\s*(['"]?)(.*?)\1\s*\)|(["'])(.*?)\3)\s*([^;]*);/gi;
|
|
102
|
+
/**
|
|
103
|
+
* Replace every unconditional `@import` with the imported stylesheet's text.
|
|
104
|
+
*
|
|
105
|
+
* An offline copy cannot keep stylesheets as fetched files: a URL like
|
|
106
|
+
* `https://fonts.googleapis.com/css2?family=…` saves to an extension-less
|
|
107
|
+
* path that static servers refuse to serve as `text/css` (stylesheet loads
|
|
108
|
+
* enforce MIME), and the file's inner `url(…)`s would stay remote. Splicing
|
|
109
|
+
* the text into the importing CSS instead lets the ordinary `url(…)`
|
|
110
|
+
* localization pass handle every asset the import pulled in.
|
|
111
|
+
*
|
|
112
|
+
* Inner URLs of an imported sheet resolve against the *sheet's* URL, so they
|
|
113
|
+
* are rebased to absolute before splicing; nested imports recurse the same
|
|
114
|
+
* way. A conditional import (layer/supports/media) has no faithful inline
|
|
115
|
+
* form — it is kept verbatim (still network-dependent) with a warning, as is
|
|
116
|
+
* a relative href with no base to resolve against. An import cycle fails the
|
|
117
|
+
* build loudly.
|
|
118
|
+
*/
|
|
119
|
+
export async function inlineCssImports(css, fetchText, base, stack = []) {
|
|
120
|
+
const external = new Set();
|
|
121
|
+
const imports = [...css.matchAll(CSS_IMPORT)];
|
|
122
|
+
const replacements = await Promise.all(imports.map(async (match) => {
|
|
123
|
+
const href = match[2] ?? match[4] ?? '';
|
|
124
|
+
const condition = match[5].trim();
|
|
125
|
+
const url = resolveImportHref(href, base);
|
|
126
|
+
if (!url || condition) {
|
|
127
|
+
if (condition) {
|
|
128
|
+
console.warn(`Keeping conditional CSS import remote: @import ${href} ${condition}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.warn(`Keeping unresolvable relative CSS import: @import ${href}`);
|
|
132
|
+
}
|
|
133
|
+
external.add(href);
|
|
134
|
+
return match[0];
|
|
135
|
+
}
|
|
136
|
+
if (stack.includes(url)) {
|
|
137
|
+
throw new Error(`CSS @import cycle: ${[...stack, url].join(' -> ')}`);
|
|
138
|
+
}
|
|
139
|
+
const imported = await fetchText(url);
|
|
140
|
+
// A common authoring slip is `@import url(font.ttf)` sitting beside a real
|
|
141
|
+
// `@font-face` — the browser fetches the font, fails to parse it as CSS,
|
|
142
|
+
// and the import contributes nothing (a no-op). Splicing its bytes in here
|
|
143
|
+
// would instead inject binary garbage that corrupts the whole stylesheet.
|
|
144
|
+
// Replicate the browser: drop the import. The font still loads offline via
|
|
145
|
+
// the sibling `@font-face`'s own `url(…)`, which the asset pass localizes.
|
|
146
|
+
if (!isStylesheetText(imported)) {
|
|
147
|
+
console.warn(`Dropping non-stylesheet CSS @import (not parseable as CSS): @import ${href}`);
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
// nested imports and relative url(…)s resolve against the imported sheet
|
|
151
|
+
const nested = await inlineCssImports(imported, fetchText, url, [...stack, url]);
|
|
152
|
+
for (const kept of nested.external) {
|
|
153
|
+
// the rebase below turns a kept rule's relative href absolute — track both forms
|
|
154
|
+
external.add(kept);
|
|
155
|
+
external.add(resolveImportHref(kept, url) ?? kept);
|
|
156
|
+
}
|
|
157
|
+
return rewriteCssUrls(nested.css, (inner) => resolveImportHref(inner, url) ?? inner);
|
|
158
|
+
}));
|
|
159
|
+
let index = 0;
|
|
160
|
+
return { css: css.replace(CSS_IMPORT, () => replacements[index++]), external };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Whether fetched `@import` content is a stylesheet rather than a binary asset
|
|
164
|
+
* (font/image) mistakenly targeted by `@import`. A NUL byte — abundant in every
|
|
165
|
+
* font format (TTF/OTF/WOFF/WOFF2/EOT) once decoded as text — never appears in a
|
|
166
|
+
* CSS stylesheet, so it is a precise, false-positive-free signal for binary.
|
|
167
|
+
*/
|
|
168
|
+
function isStylesheetText(text) {
|
|
169
|
+
return !text.includes('\u0000');
|
|
170
|
+
}
|
|
171
|
+
/** The href's absolute URL, or undefined when it is relative with no base to resolve against. */
|
|
172
|
+
function resolveImportHref(href, base) {
|
|
173
|
+
if (!href)
|
|
174
|
+
return undefined;
|
|
175
|
+
try {
|
|
176
|
+
return base ? new URL(href, base).href : new URL(href).href;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/* ── Logo variants — pre-scaled sibling files by filename suffix ──────────── */
|
|
183
|
+
/** `…/name.ext?q` → `…/name<suffix>.ext?q` (query untouched). */
|
|
184
|
+
export function withVariantUrl(url, suffix) {
|
|
185
|
+
const [path, query] = splitOnce(url, /[?#]/);
|
|
186
|
+
const lastSlash = path.lastIndexOf('/');
|
|
187
|
+
const dir = path.slice(0, lastSlash + 1);
|
|
188
|
+
const filename = path.slice(lastSlash + 1);
|
|
189
|
+
const dot = filename.lastIndexOf('.');
|
|
190
|
+
const suffixed = dot > 0 ? `${filename.slice(0, dot)}${suffix}${filename.slice(dot)}` : `${filename}${suffix}`;
|
|
191
|
+
return `${dir}${suffixed}${query}`;
|
|
192
|
+
}
|
|
193
|
+
function splitOnce(value, at) {
|
|
194
|
+
const index = value.search(at);
|
|
195
|
+
return index === -1 ? [value, ''] : [value.slice(0, index), value.slice(index)];
|
|
196
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Config, type Manifest } from '@expofp/schema';
|
|
2
|
+
import { type ResourceHandler } from './visit-resources.js';
|
|
3
|
+
/**
|
|
4
|
+
* Where serialized resources go: the caller decides storage (paths, prefixes,
|
|
5
|
+
* downloads); the config decides *what* the resources are.
|
|
6
|
+
*/
|
|
7
|
+
export type ConfigResourceStore = ResourceHandler;
|
|
8
|
+
/**
|
|
9
|
+
* Serialize a loaded config into a portable manifest: every schema-tagged
|
|
10
|
+
* asset (plain URLs, `<image>` hrefs inside the drawings, CSS `url(…)`s,
|
|
11
|
+
* pre-scaled exhibitor image variants) and every referenced payload document
|
|
12
|
+
* (drawing, layers, wayfinding graph — resolved on the fly) is handed to
|
|
13
|
+
* `store`, and the returned copy points at whatever URLs the store assigned.
|
|
14
|
+
*
|
|
15
|
+
* The legacy data directory fields are dropped — a serialized config IS the
|
|
16
|
+
* complete event data document, so a runtime consuming it derives and fetches
|
|
17
|
+
* nothing beyond the refs the store wrote.
|
|
18
|
+
*/
|
|
19
|
+
export declare function serializeConfigResources(config: Config, store: ConfigResourceStore, options?: {
|
|
20
|
+
signal?: AbortSignal | null;
|
|
21
|
+
}): Promise<Manifest>;
|
|
22
|
+
//# sourceMappingURL=serialize-config-resources.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ConfigSchema } from '@expofp/schema';
|
|
2
|
+
import { visitResources } from './visit-resources.js';
|
|
3
|
+
/**
|
|
4
|
+
* Serialize a loaded config into a portable manifest: every schema-tagged
|
|
5
|
+
* asset (plain URLs, `<image>` hrefs inside the drawings, CSS `url(…)`s,
|
|
6
|
+
* pre-scaled exhibitor image variants) and every referenced payload document
|
|
7
|
+
* (drawing, layers, wayfinding graph — resolved on the fly) is handed to
|
|
8
|
+
* `store`, and the returned copy points at whatever URLs the store assigned.
|
|
9
|
+
*
|
|
10
|
+
* The legacy data directory fields are dropped — a serialized config IS the
|
|
11
|
+
* complete event data document, so a runtime consuming it derives and fetches
|
|
12
|
+
* nothing beyond the refs the store wrote.
|
|
13
|
+
*/
|
|
14
|
+
export async function serializeConfigResources(config, store, options) {
|
|
15
|
+
// a JSON round-trip IS the serialization: it thaws the frozen config and
|
|
16
|
+
// sheds the refs' in-code `process` decorations in one move
|
|
17
|
+
const manifest = JSON.parse(JSON.stringify(config));
|
|
18
|
+
await visitResources(ConfigSchema, manifest, store, { signal: options?.signal });
|
|
19
|
+
return manifest;
|
|
20
|
+
}
|