@px-lsp/server 0.3.0 → 0.3.1
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 +1 -1
- package/data/ck3/skeletons.json +1 -0
- package/data/eu5/skeletons.json +1 -0
- package/data/vic3/skeletons.json +1 -0
- package/dist/browser-data/ck3/docs.json +1 -1
- package/dist/browser-data/ck3/tokens.json +1 -1
- package/dist/browser-data/vic3/tokens.json +1 -1
- package/dist/browser.js +50 -37
- package/dist/server.js +2000 -352
- package/dist/types/packages/server/src/features/blockSnippets.d.ts +20 -4
- package/dist/types/packages/server/src/features/completion.d.ts +11 -0
- package/dist/types/packages/server/src/features/definitionSkeletons.d.ts +24 -0
- package/dist/types/packages/server/src/games/ck3/schema.d.ts +1 -1
- package/dist/types/packages/server/src/games/eu5/index.d.ts +1 -1
- package/dist/types/packages/server/src/games/profile.d.ts +85 -1
- package/dist/types/packages/server/src/schema/skeletons.d.ts +102 -0
- package/dist/types/packages/server/src/schema/types.d.ts +30 -0
- package/package.json +3 -3
- package/src/coa/coa.ts +31 -0
- package/src/coa/coaDesigner.ts +131 -0
- package/src/coa/coaParse.ts +3 -0
- package/src/creators/definitionEdit.ts +198 -0
- package/src/creators/definitionForm.ts +479 -0
- package/src/creators/modifierFormats.ts +0 -0
- package/src/data/docsParser.ts +21 -4
- package/src/features/blockSnippets.ts +151 -20
- package/src/features/calendarDates.ts +8 -5
- package/src/features/completion.ts +38 -4
- package/src/features/definitionSkeletons.ts +109 -0
- package/src/features/inlayHints.ts +5 -2
- package/src/features/locText.ts +223 -0
- package/src/features/snippetList.ts +83 -0
- package/src/games/ck3/index.ts +16 -0
- package/src/games/ck3/meta.ts +81 -1
- package/src/games/ck3/schema.ts +83 -7
- package/src/games/ck3/structures.ts +37 -0
- package/src/games/eu5/index.ts +7 -1
- package/src/games/eu5/meta.ts +5 -1
- package/src/games/profile.ts +77 -1
- package/src/games/vic3/index.ts +4 -0
- package/src/games/vic3/meta.ts +5 -1
- package/src/gui/sourceModel.ts +51 -8
- package/src/gui/textResolve.ts +3 -3
- package/src/overview/dynastyTree.ts +493 -0
- package/src/overview/eventGraph.ts +21 -1
- package/src/overview/eventVocabulary.ts +33 -33
- package/src/overview/exampleWiki.ts +4 -1
- package/src/schema/loader.ts +2 -1
- package/src/schema/skeletons.ts +182 -0
- package/src/schema/types.ts +30 -0
- package/src/server.ts +147 -6
package/src/games/profile.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { CalendarLocSpec } from "@px-lsp/protocol/calendarLoc";
|
|
12
12
|
import type { DefRootKey, RefField, SchemaEntry, StructureSpec } from "../schema/types";
|
|
13
|
+
import type { KindSkeleton } from "../schema/skeletons";
|
|
13
14
|
import type { PlaceholderSpec } from "../data/modifierTemplates";
|
|
14
15
|
import type { GuiLayoutQuirks, GuiTextMetrics } from "../gui/layoutEngine";
|
|
15
16
|
import type { SaveSchema } from "../gui/saveSchema";
|
|
@@ -70,8 +71,10 @@ export interface GameMeta {
|
|
|
70
71
|
engine: "jomini" | "clausewitz-classic";
|
|
71
72
|
/** Mod descriptor convention: Paradox-launcher `.mod` file vs `.metadata/metadata.json`. */
|
|
72
73
|
descriptor: "mod" | "metadata";
|
|
73
|
-
/** Per-
|
|
74
|
+
/** Per-mod config dir holding schema.json / playset.json overlays, workshop.json, the listing folder. */
|
|
74
75
|
configDirName: string;
|
|
76
|
+
/** The pre-0.4.0 per-game name of that dir, still read as a fallback and renamed on first write. */
|
|
77
|
+
legacyConfigDirName?: string;
|
|
75
78
|
/** Game folder under `Documents/Paradox Interactive/` (script_docs logs live in its logs/). */
|
|
76
79
|
docsFolderName: string;
|
|
77
80
|
/**
|
|
@@ -87,6 +90,12 @@ export interface GameMeta {
|
|
|
87
90
|
*/
|
|
88
91
|
dataTypesCommand?: string;
|
|
89
92
|
steamAppId: number;
|
|
93
|
+
/**
|
|
94
|
+
* Folder under the game data dir holding one icon per DLC, named after the
|
|
95
|
+
* DLC folder's number (`dlc_003.dds` or `dlc003.dds`). Absent = the install
|
|
96
|
+
* ships no such folder and the DLC's own `thumbnail.png` is used instead.
|
|
97
|
+
*/
|
|
98
|
+
dlcIconDir?: string;
|
|
90
99
|
/** Whether event files declare `namespace = x` and use `ns.N` event ids. */
|
|
91
100
|
eventNamespaces: boolean;
|
|
92
101
|
/**
|
|
@@ -128,6 +137,39 @@ export interface GameMeta {
|
|
|
128
137
|
* not open for this game.
|
|
129
138
|
*/
|
|
130
139
|
flagBuilder?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* The game ships its own in-game Coat of Arms designer, and the files that
|
|
142
|
+
* drive it (`gfx/coat_of_arms/{patterns,colored_emblems,color_palettes,
|
|
143
|
+
* emblem_layouts}/50_coa_designer_*.txt` and the `99_coa_designer_templates`
|
|
144
|
+
* coat-of-arms templates). Present = the toolkit offers the designer
|
|
145
|
+
* instead of the raw Flag Builder, when those files are actually on disk.
|
|
146
|
+
* Absent = the Flag Builder, as before.
|
|
147
|
+
*/
|
|
148
|
+
coaDesigner?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Visual content creators offered for this game, in Create-group order. A
|
|
151
|
+
* row is a definition kind the schema knows (the panel's command opens that
|
|
152
|
+
* creator), except `dynasty_tree`, which is a view over history files rather
|
|
153
|
+
* than one definition. Absent = no creator has been built against this
|
|
154
|
+
* game's own files, and the Create group shows only the scaffolds.
|
|
155
|
+
*/
|
|
156
|
+
creators?: {
|
|
157
|
+
/** Schema kind, or the id of a bespoke creator that owns no single kind. */
|
|
158
|
+
kind: string;
|
|
159
|
+
label: string;
|
|
160
|
+
/** A Lucide icon name present in the client's webviews/shared/icons.ts. */
|
|
161
|
+
icon: string;
|
|
162
|
+
/** One-line hint for the panel row. */
|
|
163
|
+
tip?: string;
|
|
164
|
+
}[];
|
|
165
|
+
/**
|
|
166
|
+
* Where the game states how a modifier is PRINTED: the folder of format
|
|
167
|
+
* definitions (one block per modifier), and the `.gui` file whose `texticon`
|
|
168
|
+
* blocks map an icon name to its sprite. Both paths are relative to the game
|
|
169
|
+
* data dir. Absent = this game's print rules have not been read out of its
|
|
170
|
+
* own files, and `paradox/modifierFormats` answers null rather than guessing.
|
|
171
|
+
*/
|
|
172
|
+
modifierFormats?: { folder: string; textIcons: string };
|
|
131
173
|
/**
|
|
132
174
|
* Database entry-mode prefixes legal on top-level definition keys
|
|
133
175
|
* (EU5's `REPLACE:key`). The indexer strips a leading `<MODE>:` before
|
|
@@ -166,6 +208,25 @@ export interface GameMeta {
|
|
|
166
208
|
cacheSuffix: string;
|
|
167
209
|
}
|
|
168
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Where a creator's condition builder gets the values of one trigger
|
|
213
|
+
* (`DefinitionForm.conditions`). Each source is something the server already
|
|
214
|
+
* holds, so a game patch changes the list without a release:
|
|
215
|
+
*
|
|
216
|
+
* docList the trigger's own script_docs entry enumerates them on its
|
|
217
|
+
* metadata line ("has_dlc_feature … Traits: Valid Features: a,
|
|
218
|
+
* b, and c", CK3 triggers.log 1.19)
|
|
219
|
+
* kind every indexed definition of a definition kind
|
|
220
|
+
* innerKeys the inner block keys of every definition of a kind, minus
|
|
221
|
+
* `except` (a CK3 game rule's settings ARE its inner blocks;
|
|
222
|
+
* the other two keys are `categories` and `default`, per
|
|
223
|
+
* common/game_rules/_game_rules.info)
|
|
224
|
+
*/
|
|
225
|
+
export type ConditionValueSource =
|
|
226
|
+
| { from: "docList" }
|
|
227
|
+
| { from: "kind"; kind: string }
|
|
228
|
+
| { from: "innerKeys"; kind: string; except?: string[] };
|
|
229
|
+
|
|
169
230
|
/** A game's full knowledge bundle: meta plus the tables the engine consumes. */
|
|
170
231
|
export interface GameProfile extends GameMeta {
|
|
171
232
|
/** Folder→definition-kind table (see schema/types.ts). */
|
|
@@ -185,6 +246,14 @@ export interface GameProfile extends GameMeta {
|
|
|
185
246
|
* those entries always win.
|
|
186
247
|
*/
|
|
187
248
|
structures?: Record<string, StructureSpec>;
|
|
249
|
+
/**
|
|
250
|
+
* Definition skeletons per kind (data/<id>/skeletons.json, harvested by
|
|
251
|
+
* scripts/build-skeletons.ts): the shape a new definition of the kind takes,
|
|
252
|
+
* measured over the game's own files. Absent or empty = nobody has measured
|
|
253
|
+
* this game's vanilla tree, and completion offers no skeleton for it rather
|
|
254
|
+
* than a shape written from memory.
|
|
255
|
+
*/
|
|
256
|
+
skeletons?: Record<string, KindSkeleton>;
|
|
188
257
|
/**
|
|
189
258
|
* Definition kinds that declare their own root scope in their body, keyed by
|
|
190
259
|
* kind (scopes/inference.ts). Absent = every kind's root scope comes from the
|
|
@@ -193,6 +262,13 @@ export interface GameProfile extends GameMeta {
|
|
|
193
262
|
defRootKeys?: Record<string, DefRootKey>;
|
|
194
263
|
/** Templated-modifier placeholder table (data/modifierTemplates.ts). */
|
|
195
264
|
modifierPlaceholders: Record<string, PlaceholderSpec>;
|
|
265
|
+
/**
|
|
266
|
+
* Triggers a creator's condition builder may offer a value list for, keyed
|
|
267
|
+
* by trigger name (`paradox/definitionForm`'s `conditions`). Absent = the
|
|
268
|
+
* creators offer no picker for this game and fall back to free input, which
|
|
269
|
+
* is the honest answer for a game whose sources have not been measured.
|
|
270
|
+
*/
|
|
271
|
+
conditionValues?: Record<string, ConditionValueSource>;
|
|
196
272
|
/**
|
|
197
273
|
* Bundled `[ ... ]` data-function tables (data/<id>/dataTypes.json), when the
|
|
198
274
|
* game ships one. Shape is data/dataTypes.ts's bundled-JSON shape; typed
|
package/src/games/vic3/index.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* (markdown format). No wiki fallback and no structures layer, see below.
|
|
6
6
|
*/
|
|
7
7
|
import type { GameProfile } from "../profile";
|
|
8
|
+
import type { SkeletonData } from "../../schema/skeletons";
|
|
8
9
|
import { vic3Meta } from "./meta";
|
|
9
10
|
import { VIC3_BLOCK_REF_FIELDS, VIC3_PREFIX_REFS, VIC3_REF_FIELDS, VIC3_SCHEMA } from "./schema";
|
|
10
11
|
import { VIC3_STRUCTURES, VIC3_STRUCTURE_SOURCES } from "./structures";
|
|
11
12
|
import { VIC3_SAVE_SCHEMA } from "./saveSchema";
|
|
12
13
|
import GUI_SCHEMA from "../../../data/vic3/guiSchema.json";
|
|
14
|
+
// Definition skeletons measured over the vanilla tree (scripts/build-skeletons.ts).
|
|
15
|
+
import SKELETONS from "../../../data/vic3/skeletons.json";
|
|
13
16
|
|
|
14
17
|
export const vic3Profile: GameProfile = {
|
|
15
18
|
...vic3Meta,
|
|
@@ -18,6 +21,7 @@ export const vic3Profile: GameProfile = {
|
|
|
18
21
|
prefixRefs: VIC3_PREFIX_REFS,
|
|
19
22
|
blockRefFields: VIC3_BLOCK_REF_FIELDS,
|
|
20
23
|
guiSchema: GUI_SCHEMA,
|
|
24
|
+
skeletons: (SKELETONS as unknown as SkeletonData).kinds,
|
|
21
25
|
// guiTextMetrics live on the meta (the client reads them too, for the GUI
|
|
22
26
|
// editor gate and its canvas line height).
|
|
23
27
|
// Vic3 keeps an authored size on an EMPTY container (px_probe_c C5: the
|
package/src/games/vic3/meta.ts
CHANGED
|
@@ -12,13 +12,17 @@ export const vic3Meta: GameMeta = {
|
|
|
12
12
|
engine: "jomini",
|
|
13
13
|
// Vic3 mods carry .metadata/metadata.json instead of a launcher .mod file.
|
|
14
14
|
descriptor: "metadata",
|
|
15
|
-
configDirName: ".
|
|
15
|
+
configDirName: ".px-toolkit",
|
|
16
|
+
legacyConfigDirName: ".vic3modding",
|
|
16
17
|
docsFolderName: "Victoria 3",
|
|
17
18
|
// Vic3's `script_docs` writes to Documents/.../docs (like EU5), while
|
|
18
19
|
// `dump_data_types` writes to logs/data_types — verified on a live install.
|
|
19
20
|
scriptDocsSubdir: "docs",
|
|
20
21
|
dataTypesCommand: "dump_data_types",
|
|
21
22
|
steamAppId: 529340,
|
|
23
|
+
// Verified on the live install: a different folder name from CK3's, and only
|
|
24
|
+
// some DLC have a file there (the rest fall back to their thumbnail.png).
|
|
25
|
+
dlcIconDir: "gfx/interface/icons/dlc_icons",
|
|
22
26
|
eventNamespaces: true,
|
|
23
27
|
scaffolds: VIC3_SCAFFOLDS,
|
|
24
28
|
// uiFont deliberately absent: this game ships no Gitan (its fonts/ holds
|
package/src/gui/sourceModel.ts
CHANGED
|
@@ -57,6 +57,40 @@ export type GuiEntryKind = "widget" | "decl" | "property";
|
|
|
57
57
|
* block alone.
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The three .gui-specific tables the model reads, lifted into a parameter so
|
|
62
|
+
* the same span model can serve plain jomini script (`common/traits/*.txt`),
|
|
63
|
+
* which has none of them.
|
|
64
|
+
*
|
|
65
|
+
* `propertyBlocks` is NOT a set for script: the gui rule is the INVERSE one (a
|
|
66
|
+
* block child is a child widget unless its key is a known attribute block), so
|
|
67
|
+
* an empty set would classify every block in a trait as a widget and the
|
|
68
|
+
* writer would refuse to rewrite it. `null` says "this dialect has no child
|
|
69
|
+
* declarations at all", which is what script is.
|
|
70
|
+
*/
|
|
71
|
+
export interface SourceDialect {
|
|
72
|
+
/** Bare words that label the next assignment as a declaration. */
|
|
73
|
+
declMarkers: ReadonlySet<string>;
|
|
74
|
+
/** Keys taking the `key = "name" { ... }` slot spelling. */
|
|
75
|
+
slotKeys: ReadonlySet<string>;
|
|
76
|
+
/** Block-valued keys that are attributes; `null` = every block is an attribute. */
|
|
77
|
+
propertyBlocks: ReadonlySet<string> | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The default: the .gui tables, so every existing caller is unchanged. */
|
|
81
|
+
export const GUI_DIALECT: SourceDialect = {
|
|
82
|
+
declMarkers: DECL_MARKERS,
|
|
83
|
+
slotKeys: SLOT_KEYS,
|
|
84
|
+
propertyBlocks: PROPERTY_BLOCKS,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Plain jomini script: no markers, no slot form, no child declarations. */
|
|
88
|
+
export const SCRIPT_DIALECT: SourceDialect = {
|
|
89
|
+
declMarkers: new Set(),
|
|
90
|
+
slotKeys: new Set(),
|
|
91
|
+
propertyBlocks: null,
|
|
92
|
+
};
|
|
93
|
+
|
|
60
94
|
/** One `key [op] value` statement, with every raw span the writer addresses it by. */
|
|
61
95
|
export interface GuiEntry {
|
|
62
96
|
kind: GuiEntryKind;
|
|
@@ -177,13 +211,14 @@ interface BuildCtx {
|
|
|
177
211
|
lineKind: Uint8Array;
|
|
178
212
|
commentsByLine: Map<number, CommentNode[]>;
|
|
179
213
|
entries: GuiEntry[];
|
|
214
|
+
dialect: SourceDialect;
|
|
180
215
|
}
|
|
181
216
|
|
|
182
217
|
const LINE_BLANK = 0;
|
|
183
218
|
const LINE_COMMENT = 1;
|
|
184
219
|
const LINE_CODE = 2;
|
|
185
220
|
|
|
186
|
-
export function parseGuiSource(text: string): GuiSourceFile {
|
|
221
|
+
export function parseGuiSource(text: string, dialect: SourceDialect = GUI_DIALECT): GuiSourceFile {
|
|
187
222
|
const parsed = parseScript(text);
|
|
188
223
|
const lines = new LineIndex(text);
|
|
189
224
|
const commentsByLine = new Map<number, CommentNode[]>();
|
|
@@ -198,6 +233,7 @@ export function parseGuiSource(text: string): GuiSourceFile {
|
|
|
198
233
|
lineKind: classifyLines(text, lines, commentsByLine),
|
|
199
234
|
commentsByLine,
|
|
200
235
|
entries: [],
|
|
236
|
+
dialect,
|
|
201
237
|
};
|
|
202
238
|
const root = buildBody(parsed.root.statements, -1, null, { start: 0, end: text.length }, null, ctx);
|
|
203
239
|
return {
|
|
@@ -269,7 +305,7 @@ function buildBody(
|
|
|
269
305
|
// A bare `template` / `type` / `blockoverride` word labels the next
|
|
270
306
|
// assignment; anything else clears the label.
|
|
271
307
|
marker =
|
|
272
|
-
stmt.value.kind === "scalar" &&
|
|
308
|
+
stmt.value.kind === "scalar" && ctx.dialect.declMarkers.has(stmt.value.text.toLowerCase())
|
|
273
309
|
? { text: stmt.value.text.toLowerCase(), span: stmt.value.range }
|
|
274
310
|
: null;
|
|
275
311
|
continue;
|
|
@@ -353,7 +389,7 @@ function buildEntry(
|
|
|
353
389
|
): GuiEntry {
|
|
354
390
|
const { text, lines } = ctx;
|
|
355
391
|
const opSpan = operatorSpan(text, stmt);
|
|
356
|
-
const slot = slotForm(stmt, declMarker);
|
|
392
|
+
const slot = slotForm(stmt, declMarker, ctx.dialect);
|
|
357
393
|
const marker = slot?.marker ?? declMarker;
|
|
358
394
|
const key = slot?.key ?? stmt.key;
|
|
359
395
|
const value = slot?.value ?? stmt.value;
|
|
@@ -384,7 +420,7 @@ function buildEntry(
|
|
|
384
420
|
const ownLine = startsLine && endsLine;
|
|
385
421
|
|
|
386
422
|
const entry: GuiEntry = {
|
|
387
|
-
kind: classify(key, marker, block),
|
|
423
|
+
kind: classify(key, marker, block, ctx.dialect),
|
|
388
424
|
key: key.text,
|
|
389
425
|
keyLower: key.text.toLowerCase(),
|
|
390
426
|
keySpan: key.range,
|
|
@@ -444,22 +480,29 @@ function buildEntry(
|
|
|
444
480
|
* the writer and the preview can never disagree about what is a widget: a
|
|
445
481
|
* block child is a widget unless its key is a known attribute block.
|
|
446
482
|
*/
|
|
447
|
-
function classify(
|
|
483
|
+
function classify(
|
|
484
|
+
key: ScalarNode,
|
|
485
|
+
marker: { text: string } | null,
|
|
486
|
+
block: BlockNode | null,
|
|
487
|
+
dialect: SourceDialect
|
|
488
|
+
): GuiEntryKind {
|
|
448
489
|
if (marker) return "decl";
|
|
449
490
|
if (!block) return "property";
|
|
491
|
+
if (!dialect.propertyBlocks) return "property";
|
|
450
492
|
const lower = key.text.toLowerCase();
|
|
451
|
-
if (
|
|
493
|
+
if (dialect.propertyBlocks.has(lower) || lower.startsWith("@")) return "property";
|
|
452
494
|
return "widget";
|
|
453
495
|
}
|
|
454
496
|
|
|
455
497
|
/** The `blockoverride = "name" { ... }` spelling, re-read as marker + name + body. */
|
|
456
498
|
function slotForm(
|
|
457
499
|
stmt: AssignmentNode,
|
|
458
|
-
marker: { text: string; span: GuiSpan } | null
|
|
500
|
+
marker: { text: string; span: GuiSpan } | null,
|
|
501
|
+
dialect: SourceDialect
|
|
459
502
|
): { marker: { text: string; span: GuiSpan }; key: ScalarNode; value: BlockNode } | null {
|
|
460
503
|
if (marker || stmt.value?.kind !== "tagged-block") return null;
|
|
461
504
|
const lower = stmt.key.text.toLowerCase();
|
|
462
|
-
if (!
|
|
505
|
+
if (!dialect.slotKeys.has(lower)) return null;
|
|
463
506
|
return {
|
|
464
507
|
marker: { text: lower, span: stmt.key.range },
|
|
465
508
|
key: stmt.value.tag,
|
package/src/gui/textResolve.ts
CHANGED
|
@@ -34,7 +34,7 @@ const LOC_KEY = /^[A-Za-z0-9_][A-Za-z0-9_.\-']*$/;
|
|
|
34
34
|
* Loc formatting the game does not draw: `#bold text#!`, `#R text#!`, `§Ytext§!`,
|
|
35
35
|
* `@icon!` icon references. Removed for measurement; the preview has no glyphs for them.
|
|
36
36
|
*/
|
|
37
|
-
function stripFormatting(s: string): string {
|
|
37
|
+
export function stripFormatting(s: string): string {
|
|
38
38
|
return s
|
|
39
39
|
.replace(/#!/g, "")
|
|
40
40
|
.replace(/#[A-Za-z_][A-Za-z0-9_;:]*\s?/g, "")
|
|
@@ -45,7 +45,7 @@ function stripFormatting(s: string): string {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** Split `a [Fn] b [[literal]` into literal and datafunction pieces. */
|
|
48
|
-
function tokenize(s: string): { literal?: string; fn?: string }[] {
|
|
48
|
+
export function tokenize(s: string): { literal?: string; fn?: string }[] {
|
|
49
49
|
const out: { literal?: string; fn?: string }[] = [];
|
|
50
50
|
let lit = "";
|
|
51
51
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -87,7 +87,7 @@ function locOfCall(fn: string, loc: TextResolvers["loc"]): string | undefined {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** `GetPlayer.GetName` -> `GetName`; `Concept('x','y')` -> `Concept`; strips a `Get` prefix and arguments. */
|
|
90
|
-
function chipFor(fn: string): string {
|
|
90
|
+
export function chipFor(fn: string): string {
|
|
91
91
|
// `|0`, `|%`: format specifiers after the chain, not part of the name.
|
|
92
92
|
const chain = fn.split("|")[0];
|
|
93
93
|
const last = chain.split(".").pop() ?? chain;
|