@patterkit/runtime 0.20.0 → 0.22.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.
- package/CHANGELOG.md +27 -0
- package/dist/index.cjs +54 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +55 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -14,6 +14,8 @@ type StepResult = {
|
|
|
14
14
|
character?: string;
|
|
15
15
|
characterName?: string;
|
|
16
16
|
direction?: string;
|
|
17
|
+
qualifier?: string;
|
|
18
|
+
qualifierName?: string;
|
|
17
19
|
gameData?: GameData;
|
|
18
20
|
tags?: string[];
|
|
19
21
|
} | {
|
|
@@ -44,6 +46,10 @@ interface BeatInfo {
|
|
|
44
46
|
characterName?: string;
|
|
45
47
|
/** Performance direction (line only). */
|
|
46
48
|
direction?: string;
|
|
49
|
+
/** Speaker qualifier `gameId` (line only), e.g. `vo`. */
|
|
50
|
+
qualifier?: string;
|
|
51
|
+
/** The qualifier's shown name (source locale), e.g. `V.O.`. */
|
|
52
|
+
qualifierName?: string;
|
|
47
53
|
/** Source text, un-interpolated (line / text). Omitted for gameEvent and IDs-only bundles. */
|
|
48
54
|
text?: string;
|
|
49
55
|
/** Author gameData overrides on this beat (raw, as the step carries them). Omitted when empty. */
|
|
@@ -111,6 +117,9 @@ interface ChoicePrompt {
|
|
|
111
117
|
/** The speaker's resolved player-facing name (locale-aware; absent when the character has none). */
|
|
112
118
|
characterName?: string;
|
|
113
119
|
direction?: string;
|
|
120
|
+
/** The speaker qualifier's `gameId` (`vo`), and its resolved shown name (locale-aware). */
|
|
121
|
+
qualifier?: string;
|
|
122
|
+
qualifierName?: string;
|
|
114
123
|
}
|
|
115
124
|
/** A single option of a pending `choice` group. */
|
|
116
125
|
interface ChoiceOption {
|
|
@@ -352,6 +361,8 @@ interface FlowHost {
|
|
|
352
361
|
defaultStrings: Record<string, string>;
|
|
353
362
|
/** Cast canonical name -> authoring `displayName` (the unlocalised fallback when no loc string exists). */
|
|
354
363
|
castDisplay: Map<string, string>;
|
|
364
|
+
/** Speaker qualifier gameId -> authored name (the unlocalised fallback when no loc string exists). */
|
|
365
|
+
qualifierDisplay: Map<string, string>;
|
|
355
366
|
nodeIndex: Map<string, SelectableNode>;
|
|
356
367
|
blockIndex: Map<string, {
|
|
357
368
|
sceneId: string;
|
|
@@ -680,8 +691,6 @@ declare class Engine {
|
|
|
680
691
|
* across the family 2026-09-05, after a Storylets venue clock and this project's coverage driver
|
|
681
692
|
* were each refused the one property they existed to move.) */
|
|
682
693
|
setProperty(ref: string, value: ScalarValue): void;
|
|
683
|
-
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
684
|
-
* value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
|
|
685
694
|
/** The run's decisions, in order, each naming the flow it happened in. Empty unless the
|
|
686
695
|
* run was opened with `log: true`. A flow's own log stays flow-local; this is the only
|
|
687
696
|
* place a story spanning several flows reads as one sequence. */
|
|
@@ -696,6 +705,8 @@ declare class Engine {
|
|
|
696
705
|
/** How many times each node has been entered across every flow, by node id: the shared count. With a
|
|
697
706
|
* flow's own {@link Flow.getVisitCounts}, every visit count the run keeps, read without a save. */
|
|
698
707
|
getVisitCounts(): Record<string, number>;
|
|
708
|
+
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
709
|
+
* value, declared default (for reset), and enum options. Every runtime has the same call. */
|
|
699
710
|
listProperties(): PropertyRow[];
|
|
700
711
|
private splitShared;
|
|
701
712
|
/** The shared `@patter` values alone, as `{ patter: {...} }`.
|
|
@@ -994,6 +1005,10 @@ declare class Flow {
|
|
|
994
1005
|
* locale, else the authoring `displayName`. Undefined when the character has no display name at all
|
|
995
1006
|
* (the host falls back to the `character` token itself). */
|
|
996
1007
|
private resolveCharacterName;
|
|
1008
|
+
/** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
|
|
1009
|
+
* active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
|
|
1010
|
+
* maps the `qualifier` gameId itself). */
|
|
1011
|
+
private resolveQualifierName;
|
|
997
1012
|
/** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
|
|
998
1013
|
private splitRef;
|
|
999
1014
|
/** A fresh, unregistered bag for the NOT-shared `@patter` globals (the shared ones live on the host). */
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ type StepResult = {
|
|
|
14
14
|
character?: string;
|
|
15
15
|
characterName?: string;
|
|
16
16
|
direction?: string;
|
|
17
|
+
qualifier?: string;
|
|
18
|
+
qualifierName?: string;
|
|
17
19
|
gameData?: GameData;
|
|
18
20
|
tags?: string[];
|
|
19
21
|
} | {
|
|
@@ -44,6 +46,10 @@ interface BeatInfo {
|
|
|
44
46
|
characterName?: string;
|
|
45
47
|
/** Performance direction (line only). */
|
|
46
48
|
direction?: string;
|
|
49
|
+
/** Speaker qualifier `gameId` (line only), e.g. `vo`. */
|
|
50
|
+
qualifier?: string;
|
|
51
|
+
/** The qualifier's shown name (source locale), e.g. `V.O.`. */
|
|
52
|
+
qualifierName?: string;
|
|
47
53
|
/** Source text, un-interpolated (line / text). Omitted for gameEvent and IDs-only bundles. */
|
|
48
54
|
text?: string;
|
|
49
55
|
/** Author gameData overrides on this beat (raw, as the step carries them). Omitted when empty. */
|
|
@@ -111,6 +117,9 @@ interface ChoicePrompt {
|
|
|
111
117
|
/** The speaker's resolved player-facing name (locale-aware; absent when the character has none). */
|
|
112
118
|
characterName?: string;
|
|
113
119
|
direction?: string;
|
|
120
|
+
/** The speaker qualifier's `gameId` (`vo`), and its resolved shown name (locale-aware). */
|
|
121
|
+
qualifier?: string;
|
|
122
|
+
qualifierName?: string;
|
|
114
123
|
}
|
|
115
124
|
/** A single option of a pending `choice` group. */
|
|
116
125
|
interface ChoiceOption {
|
|
@@ -352,6 +361,8 @@ interface FlowHost {
|
|
|
352
361
|
defaultStrings: Record<string, string>;
|
|
353
362
|
/** Cast canonical name -> authoring `displayName` (the unlocalised fallback when no loc string exists). */
|
|
354
363
|
castDisplay: Map<string, string>;
|
|
364
|
+
/** Speaker qualifier gameId -> authored name (the unlocalised fallback when no loc string exists). */
|
|
365
|
+
qualifierDisplay: Map<string, string>;
|
|
355
366
|
nodeIndex: Map<string, SelectableNode>;
|
|
356
367
|
blockIndex: Map<string, {
|
|
357
368
|
sceneId: string;
|
|
@@ -680,8 +691,6 @@ declare class Engine {
|
|
|
680
691
|
* across the family 2026-09-05, after a Storylets venue clock and this project's coverage driver
|
|
681
692
|
* were each refused the one property they existed to move.) */
|
|
682
693
|
setProperty(ref: string, value: ScalarValue): void;
|
|
683
|
-
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
684
|
-
* value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
|
|
685
694
|
/** The run's decisions, in order, each naming the flow it happened in. Empty unless the
|
|
686
695
|
* run was opened with `log: true`. A flow's own log stays flow-local; this is the only
|
|
687
696
|
* place a story spanning several flows reads as one sequence. */
|
|
@@ -696,6 +705,8 @@ declare class Engine {
|
|
|
696
705
|
/** How many times each node has been entered across every flow, by node id: the shared count. With a
|
|
697
706
|
* flow's own {@link Flow.getVisitCounts}, every visit count the run keeps, read without a save. */
|
|
698
707
|
getVisitCounts(): Record<string, number>;
|
|
708
|
+
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
709
|
+
* value, declared default (for reset), and enum options. Every runtime has the same call. */
|
|
699
710
|
listProperties(): PropertyRow[];
|
|
700
711
|
private splitShared;
|
|
701
712
|
/** The shared `@patter` values alone, as `{ patter: {...} }`.
|
|
@@ -994,6 +1005,10 @@ declare class Flow {
|
|
|
994
1005
|
* locale, else the authoring `displayName`. Undefined when the character has no display name at all
|
|
995
1006
|
* (the host falls back to the `character` token itself). */
|
|
996
1007
|
private resolveCharacterName;
|
|
1008
|
+
/** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
|
|
1009
|
+
* active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
|
|
1010
|
+
* maps the `qualifier` gameId itself). */
|
|
1011
|
+
private resolveQualifierName;
|
|
997
1012
|
/** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
|
|
998
1013
|
private splitRef;
|
|
999
1014
|
/** A fresh, unregistered bag for the NOT-shared `@patter` globals (the shared ones live on the host). */
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { matchedSpecificity as scoreSpecificity } from "@wildwinter/expr-specifi
|
|
|
4
4
|
import { ScopeRegistry } from "@wildwinter/scoperegistry";
|
|
5
5
|
import { defaultFor, PropertyBag } from "@wildwinter/scoperegistry";
|
|
6
6
|
import { patterDialect, interpolate, splitRef, stripCaptions } from "@patterkit/dialect";
|
|
7
|
-
import { walkNodes, effectiveGameId, castStringKey, DEFAULT_CAPTION_DELIMITERS, DEFAULT_CAPTION_CHARACTER } from "@patterkit/model";
|
|
7
|
+
import { walkNodes, effectiveGameId, castStringKey, qualifierStringKey, DEFAULT_CAPTION_DELIMITERS, DEFAULT_CAPTION_CHARACTER } from "@patterkit/model";
|
|
8
8
|
|
|
9
9
|
// src/tags.ts
|
|
10
10
|
function dedupe(tags) {
|
|
@@ -43,6 +43,18 @@ function buildTagIndex(bundle) {
|
|
|
43
43
|
// src/engine.ts
|
|
44
44
|
import { SAVE_VERSION } from "@patterkit/model";
|
|
45
45
|
var astCache = /* @__PURE__ */ new WeakMap();
|
|
46
|
+
var tagIndexCache = /* @__PURE__ */ new WeakMap();
|
|
47
|
+
function tagIndexFor(bundle) {
|
|
48
|
+
let index = tagIndexCache.get(bundle);
|
|
49
|
+
if (!index) {
|
|
50
|
+
index = buildTagIndex(bundle);
|
|
51
|
+
tagIndexCache.set(bundle, index);
|
|
52
|
+
}
|
|
53
|
+
return index;
|
|
54
|
+
}
|
|
55
|
+
function tagsOf(host, id) {
|
|
56
|
+
return host.tagIndex.get(id)?.slice();
|
|
57
|
+
}
|
|
46
58
|
var OWNER = "Patter";
|
|
47
59
|
var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
|
|
48
60
|
var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
|
|
@@ -105,6 +117,8 @@ var Engine = class _Engine {
|
|
|
105
117
|
}
|
|
106
118
|
const castDisplay = /* @__PURE__ */ new Map();
|
|
107
119
|
for (const c of bundle.cast ?? []) if (c.displayName) castDisplay.set(c.name, c.displayName);
|
|
120
|
+
const qualifierDisplay = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const q of bundle.qualifiers ?? []) if (q?.gameId) qualifierDisplay.set(q.gameId, q.name);
|
|
108
122
|
this.defaultSeed = (options.seed ?? 2654435769) >>> 0;
|
|
109
123
|
const nodeIndex = /* @__PURE__ */ new Map();
|
|
110
124
|
const blockIndex = /* @__PURE__ */ new Map();
|
|
@@ -165,13 +179,14 @@ var Engine = class _Engine {
|
|
|
165
179
|
strings,
|
|
166
180
|
defaultStrings,
|
|
167
181
|
castDisplay,
|
|
182
|
+
qualifierDisplay,
|
|
168
183
|
nodeIndex,
|
|
169
184
|
blockIndex,
|
|
170
185
|
blockById,
|
|
171
186
|
sceneGameIdToId: this.sceneGameIdToId,
|
|
172
187
|
blockGameIdToId: this.blockGameIdToId,
|
|
173
188
|
// same instances the engine resolves with
|
|
174
|
-
tagIndex:
|
|
189
|
+
tagIndex: tagIndexFor(bundle),
|
|
175
190
|
registry,
|
|
176
191
|
ownsRegistry,
|
|
177
192
|
patterBag,
|
|
@@ -430,18 +445,18 @@ var Engine = class _Engine {
|
|
|
430
445
|
* delivered step carries. Empty array for an unknown id or a beat with no tags anywhere up the chain.
|
|
431
446
|
*/
|
|
432
447
|
tagsForBeat(beatId) {
|
|
433
|
-
return this.host
|
|
448
|
+
return tagsOf(this.host, beatId) ?? [];
|
|
434
449
|
}
|
|
435
450
|
/** A scene's own tags (by internal id or gameId address). Empty when none / unknown. */
|
|
436
451
|
tagsForScene(sceneRef) {
|
|
437
452
|
const id = this.resolveSceneRef(sceneRef);
|
|
438
|
-
return (id != null ? this.host
|
|
453
|
+
return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
|
|
439
454
|
}
|
|
440
455
|
/** A block's accumulated tags (scene + block), by scene + block ref (id or gameId). Empty when none / unknown. */
|
|
441
456
|
tagsForBlock(sceneRef, blockRef) {
|
|
442
457
|
const sceneId = this.resolveSceneRef(sceneRef);
|
|
443
458
|
const id = this.resolveBlockRef(sceneId, blockRef);
|
|
444
|
-
return (id != null ? this.host
|
|
459
|
+
return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
|
|
445
460
|
}
|
|
446
461
|
/**
|
|
447
462
|
* A scene's own author gameData (by internal id or gameId address): the RAW sparse overrides, exactly
|
|
@@ -560,7 +575,7 @@ var Engine = class _Engine {
|
|
|
560
575
|
}
|
|
561
576
|
/** One beat's static data (source locale), the same shape a delivered step carries. */
|
|
562
577
|
beatInfo(beat) {
|
|
563
|
-
const tags = this.host
|
|
578
|
+
const tags = tagsOf(this.host, beat.id);
|
|
564
579
|
const info = { id: beat.id, kind: beat.kind };
|
|
565
580
|
if (beat.kind === "line") {
|
|
566
581
|
if (beat.character !== void 0) {
|
|
@@ -569,6 +584,11 @@ var Engine = class _Engine {
|
|
|
569
584
|
if (name !== void 0) info.characterName = name;
|
|
570
585
|
}
|
|
571
586
|
if (beat.direction !== void 0) info.direction = beat.direction;
|
|
587
|
+
if (beat.qualifier !== void 0) {
|
|
588
|
+
info.qualifier = beat.qualifier;
|
|
589
|
+
const name = this.host.defaultStrings[qualifierStringKey(beat.qualifier)] ?? this.host.qualifierDisplay.get(beat.qualifier);
|
|
590
|
+
if (name !== void 0) info.qualifierName = name;
|
|
591
|
+
}
|
|
572
592
|
}
|
|
573
593
|
if (beat.kind === "line" || beat.kind === "text") {
|
|
574
594
|
const source = this.host.defaultStrings[beat.id];
|
|
@@ -580,7 +600,7 @@ var Engine = class _Engine {
|
|
|
580
600
|
}
|
|
581
601
|
/** A `{ tags }` fragment for an id, present only when the id has accumulated tags (keeps output tidy). */
|
|
582
602
|
tagsField(id) {
|
|
583
|
-
const tags = this.host
|
|
603
|
+
const tags = tagsOf(this.host, id);
|
|
584
604
|
return tags && tags.length ? { tags } : {};
|
|
585
605
|
}
|
|
586
606
|
/** Retrieve an open flow by id (undefined if none / closed). */
|
|
@@ -717,8 +737,6 @@ var Engine = class _Engine {
|
|
|
717
737
|
this.host.registry.set(scope, name, value, { host: true });
|
|
718
738
|
if (this.host.journal && prev !== void 0) this.host.journal.undo.push(() => this.host.registry.set(scope, name, prev, { host: true }));
|
|
719
739
|
}
|
|
720
|
-
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
721
|
-
* value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
|
|
722
740
|
/** The run's decisions, in order, each naming the flow it happened in. Empty unless the
|
|
723
741
|
* run was opened with `log: true`. A flow's own log stays flow-local; this is the only
|
|
724
742
|
* place a story spanning several flows reads as one sequence. */
|
|
@@ -750,6 +768,8 @@ var Engine = class _Engine {
|
|
|
750
768
|
getVisitCounts() {
|
|
751
769
|
return Object.fromEntries(this.host.sharedVisits);
|
|
752
770
|
}
|
|
771
|
+
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
772
|
+
* value, declared default (for reset), and enum options. Every runtime has the same call. */
|
|
753
773
|
listProperties() {
|
|
754
774
|
return this.host.patterSharedDecls.map((d) => ({
|
|
755
775
|
name: d.name,
|
|
@@ -1737,7 +1757,7 @@ var Flow = class {
|
|
|
1737
1757
|
};
|
|
1738
1758
|
// -- Strings / beats ------------------------------------------------------
|
|
1739
1759
|
beatResult(beat) {
|
|
1740
|
-
const tags = this.host
|
|
1760
|
+
const tags = tagsOf(this.host, beat.id);
|
|
1741
1761
|
const withTags = tags && tags.length ? { tags } : {};
|
|
1742
1762
|
switch (beat.kind) {
|
|
1743
1763
|
case "gameEvent":
|
|
@@ -1757,6 +1777,8 @@ var Flow = class {
|
|
|
1757
1777
|
character: silent ? void 0 : beat.character,
|
|
1758
1778
|
characterName: silent ? void 0 : this.resolveCharacterName(beat.character),
|
|
1759
1779
|
direction: silent ? void 0 : beat.direction,
|
|
1780
|
+
qualifier: silent ? void 0 : beat.qualifier,
|
|
1781
|
+
qualifierName: silent ? void 0 : this.resolveQualifierName(beat.qualifier),
|
|
1760
1782
|
gameData: beat.gameData,
|
|
1761
1783
|
...withTags
|
|
1762
1784
|
};
|
|
@@ -1795,7 +1817,15 @@ var Flow = class {
|
|
|
1795
1817
|
const beat = this.promptBeatOf(node);
|
|
1796
1818
|
if (!beat) return void 0;
|
|
1797
1819
|
const text = this.interpolate(this.resolveString(beat.id));
|
|
1798
|
-
return beat.kind === "line" ? {
|
|
1820
|
+
return beat.kind === "line" ? {
|
|
1821
|
+
kind: "line",
|
|
1822
|
+
text: this.captionLine(text),
|
|
1823
|
+
character: beat.character,
|
|
1824
|
+
characterName: this.resolveCharacterName(beat.character),
|
|
1825
|
+
direction: beat.direction,
|
|
1826
|
+
qualifier: beat.qualifier,
|
|
1827
|
+
qualifierName: this.resolveQualifierName(beat.qualifier)
|
|
1828
|
+
} : { kind: "text", text };
|
|
1799
1829
|
}
|
|
1800
1830
|
/** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
|
|
1801
1831
|
authoredPromptOf(node) {
|
|
@@ -1804,7 +1834,7 @@ var Flow = class {
|
|
|
1804
1834
|
/** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
|
|
1805
1835
|
* choice showed. */
|
|
1806
1836
|
promptResult(beat, shown) {
|
|
1807
|
-
const tags = this.host
|
|
1837
|
+
const tags = tagsOf(this.host, beat.id);
|
|
1808
1838
|
const withTags = tags && tags.length ? { tags } : {};
|
|
1809
1839
|
if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
|
|
1810
1840
|
return {
|
|
@@ -1814,6 +1844,8 @@ var Flow = class {
|
|
|
1814
1844
|
character: shown.character,
|
|
1815
1845
|
characterName: shown.characterName,
|
|
1816
1846
|
direction: shown.direction,
|
|
1847
|
+
qualifier: shown.qualifier,
|
|
1848
|
+
qualifierName: shown.qualifierName,
|
|
1817
1849
|
gameData: beat.gameData,
|
|
1818
1850
|
...withTags
|
|
1819
1851
|
};
|
|
@@ -1850,6 +1882,15 @@ var Flow = class {
|
|
|
1850
1882
|
const key = castStringKey(character);
|
|
1851
1883
|
return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.castDisplay.get(character);
|
|
1852
1884
|
}
|
|
1885
|
+
/** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
|
|
1886
|
+
* active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
|
|
1887
|
+
* maps the `qualifier` gameId itself). */
|
|
1888
|
+
resolveQualifierName(qualifier) {
|
|
1889
|
+
if (qualifier === void 0) return void 0;
|
|
1890
|
+
if (this.host.emitIds) return void 0;
|
|
1891
|
+
const key = qualifierStringKey(qualifier);
|
|
1892
|
+
return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.qualifierDisplay.get(qualifier);
|
|
1893
|
+
}
|
|
1853
1894
|
/** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
|
|
1854
1895
|
splitRef(ref) {
|
|
1855
1896
|
return splitHostRef(this.host, ref);
|
|
@@ -1946,6 +1987,8 @@ function savedPrompt(p) {
|
|
|
1946
1987
|
if (p.character !== void 0) out.character = p.character;
|
|
1947
1988
|
if (p.characterName !== void 0) out.characterName = p.characterName;
|
|
1948
1989
|
if (p.direction !== void 0) out.direction = p.direction;
|
|
1990
|
+
if (p.qualifier !== void 0) out.qualifier = p.qualifier;
|
|
1991
|
+
if (p.qualifierName !== void 0) out.qualifierName = p.qualifierName;
|
|
1949
1992
|
return out;
|
|
1950
1993
|
}
|
|
1951
1994
|
function serialiseSelectors(map) {
|