@alpaca-software/40kdc-data 1.0.18 → 1.0.20
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.
|
@@ -25,13 +25,24 @@
|
|
|
25
25
|
* • 9x Khorne Berzerker ← model group (has ◦ children) …
|
|
26
26
|
* ◦ 8x Bolt pistol ← … children are squad-wide wargear
|
|
27
27
|
* • 4x Intercessor: Bolt rifle ← model group (colon wargear, no children)
|
|
28
|
+
*
|
|
29
|
+
* Fire Dragons (120 points) ← GW app v2.0.5 "Attached Units" nesting
|
|
30
|
+
* • Attached as: Bodyguard ← attachment annotation (skipped)
|
|
31
|
+
* • 4x Fire Dragon ← model group (deeper • child) …
|
|
32
|
+
* • 4x Close combat weapon ← … first weapon is bulleted …
|
|
33
|
+
* 4x Dragon fusion gun ← … the rest are unbulleted continuations
|
|
28
34
|
* ```
|
|
29
35
|
*
|
|
30
|
-
* **Model vs wargear** (the crux), unified across dialects: a
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* count.
|
|
34
|
-
* (
|
|
36
|
+
* **Model vs wargear** (the crux), unified across dialects: a model group is a
|
|
37
|
+
* bulleted entry, at the shallowest model indent, that is followed by a *deeper
|
|
38
|
+
* bulleted* line (its squad-wide wargear); its `Nx` count (default 1) adds to
|
|
39
|
+
* the model count. Keying on the child being *bulleted* keeps a lone bulleted
|
|
40
|
+
* weapon trailed by unbulleted continuation lines (a Fire Prism's `Prism
|
|
41
|
+
* cannon`) as wargear, not a model. A bullet with a `: wargear` colon is also a
|
|
42
|
+
* model group. Everything else is wargear — a `Nx`/bare item, or the GW app's
|
|
43
|
+
* unbulleted continuation lines (v2.0.5 bullets only the *first* weapon under a
|
|
44
|
+
* model and emits the rest unbulleted, one indent deeper) — or an annotation
|
|
45
|
+
* (`Warlord`, `… Character`, `Enhancements: …`, `Attached as: …`).
|
|
35
46
|
*
|
|
36
47
|
* **Faction / detachment**: the GW export lists them as bare lines between the
|
|
37
48
|
* title and the first section (`World Eaters` / `Berzerker Warband`). The first
|
|
@@ -55,12 +66,23 @@ interface Bullet {
|
|
|
55
66
|
name: string;
|
|
56
67
|
/** Comma-separated wargear listed after a `:` on a model bullet. */
|
|
57
68
|
colon_wargear: string | null;
|
|
58
|
-
/** True for `Warlord` / `… Character` / `Enhancements:`
|
|
69
|
+
/** True for `Warlord` / `… Character` / `Enhancements:` / `Attached as:`
|
|
70
|
+
* annotations. */
|
|
59
71
|
is_annotation: boolean;
|
|
60
72
|
/** `[name, points]` when this bullet declares an enhancement. */
|
|
61
73
|
enhancement: [string, number | null] | null;
|
|
74
|
+
/** True when the source line carried a `•`/`◦` marker; false for the GW app's
|
|
75
|
+
* unbulleted continuation wargear lines. Model detection keys on this: a model
|
|
76
|
+
* is an entry followed by a *deeper bulleted* line, so a lone bulleted weapon
|
|
77
|
+
* with plain continuations (Fire Prism) is not mistaken for a model. */
|
|
78
|
+
bulleted: boolean;
|
|
79
|
+
/** True for an `Attached as: …` v2.0.5 annotation — never a model or wargear,
|
|
80
|
+
* even though it sits (bulleted) shallower than the models. */
|
|
81
|
+
is_attachment: boolean;
|
|
82
|
+
/** An `Attached as: … (Character)` annotation flags the unit as a character. */
|
|
83
|
+
sets_character: boolean;
|
|
62
84
|
}
|
|
63
|
-
declare function parseBullet(indent: number, body: string): Bullet;
|
|
85
|
+
declare function parseBullet(indent: number, body: string, bulleted: boolean): Bullet;
|
|
64
86
|
export declare const gwHeaderlessAdapter: FormatAdapter;
|
|
65
87
|
export declare const _internals: {
|
|
66
88
|
headerlessText: typeof headerlessText;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gw-headerless.d.ts","sourceRoot":"","sources":["../../src/import/gw-headerless.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"gw-headerless.d.ts","sourceRoot":"","sources":["../../src/import/gw-headerless.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAkFlD,sEAAsE;AACtE,iBAAS,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwBvD;AAED,UAAU,MAAM;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;sBACkB;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,iEAAiE;IACjE,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAC5C;;;4EAGwE;IACxE,QAAQ,EAAE,OAAO,CAAC;IAClB;mEAC+D;IAC/D,aAAa,EAAE,OAAO,CAAC;IACvB,gFAAgF;IAChF,cAAc,EAAE,OAAO,CAAC;CACzB;AASD,iBAAS,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CA2E5E;AAiID,eAAO,MAAM,mBAAmB,EAAE,aAmJjC,CAAC;AAGF,eAAO,MAAM,UAAU;;;CAGtB,CAAC"}
|
|
@@ -20,6 +20,9 @@ const RE_NX_PREFIX = /^(\d+)x\s+(.+)$/i;
|
|
|
20
20
|
const RE_ENHANCEMENT_ANNOT = /^(.+?)\s*\(\+\s*(\d+)\s*pts?\s*\)\s*$/i;
|
|
21
21
|
/** `Enhancements: X` / `E: X` enhancement bullet. */
|
|
22
22
|
const RE_ENHANCEMENT_LABEL = /^(?:e|enh|enhancement|enhancements)\s*:\s*(.+)$/i;
|
|
23
|
+
/** `Attached as: <role>` — GW app v2.0.5 attachment annotation (captures the
|
|
24
|
+
* role so `(Character)` can flag the unit). */
|
|
25
|
+
const RE_ATTACHED_AS = /^attached\s+as\s*:\s*(.+)$/i;
|
|
23
26
|
const RE_WITH_LINE = /^[\t ]*\d+\s+with\b/m;
|
|
24
27
|
const RE_BULLET_ANYWHERE = /^[\t ]*[•◦]/mu;
|
|
25
28
|
/** ListForge-text first line: `<name> - <faction> - <detachment> (N Points)`.
|
|
@@ -38,17 +41,52 @@ function parsePts(raw) {
|
|
|
38
41
|
const n = Number.parseInt(raw.replace(/,/g, ""), 10);
|
|
39
42
|
return Number.isNaN(n) ? null : n;
|
|
40
43
|
}
|
|
44
|
+
/** A line of only `+` characters — the BCP summary block's fence. */
|
|
45
|
+
const RE_PLUS_FENCE = /^\++$/;
|
|
46
|
+
/** A line inside that block identifying it as BCP's (not GW's own `+ …` fence). */
|
|
47
|
+
const RE_BCP_SUMMARY_MARKER = /^\s*(?:Player Name|Team Name|Factions Used|Army Points)\s*:/im;
|
|
48
|
+
/**
|
|
49
|
+
* BCP prepends a `++…++`-fenced summary block (`Player Name:` / `Factions Used:`
|
|
50
|
+
* / `Army Points: N` / …) to text-type lists. It is BCP metadata, not part of the
|
|
51
|
+
* pasted roster, and it derails the body grammar: the fence line gets consumed as
|
|
52
|
+
* the roster title, so the *real* title line (`House Rosecairn (1995 points)`)
|
|
53
|
+
* becomes a phantom unit and its points double the computed total. Strip the
|
|
54
|
+
* leading block when present. Only a block whose fence pair wraps a BCP marker is
|
|
55
|
+
* removed, so a framed GW export's own `+ FACTION KEYWORD:` fence is left intact.
|
|
56
|
+
*/
|
|
57
|
+
function stripBcpSummary(text) {
|
|
58
|
+
const lines = text.split(/\r?\n/);
|
|
59
|
+
let open = 0;
|
|
60
|
+
while (open < lines.length && lines[open].trim() === "")
|
|
61
|
+
open += 1;
|
|
62
|
+
if (open >= lines.length || !RE_PLUS_FENCE.test(lines[open].trim()))
|
|
63
|
+
return text;
|
|
64
|
+
let close = -1;
|
|
65
|
+
for (let j = open + 1; j < lines.length; j += 1) {
|
|
66
|
+
if (RE_PLUS_FENCE.test(lines[j].trim())) {
|
|
67
|
+
close = j;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (close === -1)
|
|
72
|
+
return text;
|
|
73
|
+
const block = lines.slice(open + 1, close).join("\n");
|
|
74
|
+
if (!RE_BCP_SUMMARY_MARKER.test(block))
|
|
75
|
+
return text;
|
|
76
|
+
return lines.slice(close + 1).join("\n");
|
|
77
|
+
}
|
|
41
78
|
/** Accept bullet-bearing plain text that no framed adapter claims. */
|
|
42
79
|
function headerlessText(decoded) {
|
|
43
80
|
if (typeof decoded !== "string")
|
|
44
81
|
return null;
|
|
45
|
-
|
|
82
|
+
const text = stripBcpSummary(decoded);
|
|
83
|
+
if (!RE_BULLET_ANYWHERE.test(text))
|
|
46
84
|
return null; // need a bullet
|
|
47
|
-
if (
|
|
85
|
+
if (text.includes("+ FACTION KEYWORD:"))
|
|
48
86
|
return null; // framed GW → gwAdapter
|
|
49
|
-
if (RE_WITH_LINE.test(
|
|
87
|
+
if (RE_WITH_LINE.test(text))
|
|
50
88
|
return null; // WTC-full
|
|
51
|
-
const lines =
|
|
89
|
+
const lines = text.split(/\r?\n/);
|
|
52
90
|
// ListForge-text's `name - faction - detachment (N Points)` header → defer to
|
|
53
91
|
// listForgeTextAdapter (registered ahead of us). Mirrors its own matcher so
|
|
54
92
|
// the two stay disjoint, per the importer's single-match invariant.
|
|
@@ -64,14 +102,32 @@ function headerlessText(decoded) {
|
|
|
64
102
|
return null;
|
|
65
103
|
}
|
|
66
104
|
// Require a `Name (N pts|Points)` line somewhere — the unit/title signature.
|
|
67
|
-
return lines.some((l) => RE_PTS_LINE.test(l.trim())) ?
|
|
105
|
+
return lines.some((l) => RE_PTS_LINE.test(l.trim())) ? text : null;
|
|
68
106
|
}
|
|
69
|
-
function parseBullet(indent, body) {
|
|
107
|
+
function parseBullet(indent, body, bulleted) {
|
|
108
|
+
const base = { indent, bulleted, is_attachment: false, sets_character: false };
|
|
109
|
+
// `Attached as: <role>` — the v2.0.5 multi-detachment export tags each unit
|
|
110
|
+
// with how it attaches. Pure annotation: never a model or wargear. Its `:`
|
|
111
|
+
// must be caught before the generic colon split below, else the role is read
|
|
112
|
+
// as inline wargear. A `(Character)` role flags the unit as a character.
|
|
113
|
+
const attached = RE_ATTACHED_AS.exec(body);
|
|
114
|
+
if (attached) {
|
|
115
|
+
return {
|
|
116
|
+
...base,
|
|
117
|
+
count: null,
|
|
118
|
+
name: "",
|
|
119
|
+
colon_wargear: null,
|
|
120
|
+
is_annotation: true,
|
|
121
|
+
enhancement: null,
|
|
122
|
+
is_attachment: true,
|
|
123
|
+
sets_character: /\(\s*Character\s*\)/i.test(attached[1]),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
70
126
|
// Enhancement label first — `Enhancements: X` must not read as a model.
|
|
71
127
|
const label = RE_ENHANCEMENT_LABEL.exec(body);
|
|
72
128
|
if (label) {
|
|
73
129
|
return {
|
|
74
|
-
|
|
130
|
+
...base,
|
|
75
131
|
count: null,
|
|
76
132
|
name: "",
|
|
77
133
|
colon_wargear: null,
|
|
@@ -87,7 +143,7 @@ function parseBullet(indent, body) {
|
|
|
87
143
|
if (annot) {
|
|
88
144
|
const pts = Number.parseInt(annot[2], 10);
|
|
89
145
|
return {
|
|
90
|
-
|
|
146
|
+
...base,
|
|
91
147
|
count,
|
|
92
148
|
name: rest,
|
|
93
149
|
colon_wargear: null,
|
|
@@ -100,7 +156,7 @@ function parseBullet(indent, body) {
|
|
|
100
156
|
if (idx >= 0) {
|
|
101
157
|
const wargear = rest.slice(idx + 1).trim();
|
|
102
158
|
return {
|
|
103
|
-
|
|
159
|
+
...base,
|
|
104
160
|
count,
|
|
105
161
|
name: rest.slice(0, idx).trim(),
|
|
106
162
|
colon_wargear: wargear.length > 0 ? wargear : null,
|
|
@@ -110,7 +166,7 @@ function parseBullet(indent, body) {
|
|
|
110
166
|
}
|
|
111
167
|
// Bare token: annotation iff it has no count (Warlord / Character / wargear).
|
|
112
168
|
return {
|
|
113
|
-
|
|
169
|
+
...base,
|
|
114
170
|
count,
|
|
115
171
|
name: rest,
|
|
116
172
|
colon_wargear: null,
|
|
@@ -119,9 +175,30 @@ function parseBullet(indent, body) {
|
|
|
119
175
|
};
|
|
120
176
|
}
|
|
121
177
|
function finishUnit(acc) {
|
|
122
|
-
|
|
123
|
-
|
|
178
|
+
// Models live at the shallowest *bulleted* indent that isn't an annotation,
|
|
179
|
+
// enhancement, or colon-wargear line. The GW v2.0.5 export prefixes each unit
|
|
180
|
+
// with an `Attached as:` bullet shallower than the models, so the old
|
|
181
|
+
// "min of all indents" would misplace the model level — filter those out.
|
|
182
|
+
const modelEligible = acc.bullets.filter((b) => b.bulleted &&
|
|
183
|
+
!b.is_attachment &&
|
|
184
|
+
!b.enhancement &&
|
|
185
|
+
b.colon_wargear === null);
|
|
186
|
+
const modelIndent = modelEligible.length
|
|
187
|
+
? Math.min(...modelEligible.map((b) => b.indent))
|
|
124
188
|
: 0;
|
|
189
|
+
// A model group: a bulleted entry at the model indent that is followed by a
|
|
190
|
+
// *deeper bulleted* line (its squad-wide wargear). Keying on the child being
|
|
191
|
+
// bulleted keeps a lone bulleted weapon trailed by plain continuation lines
|
|
192
|
+
// (Fire Prism's Prism cannon) as wargear, not a model. A count-less model
|
|
193
|
+
// name (`• Bloodreaper` with children) still counts as one model (`?? 1`).
|
|
194
|
+
const isModelGroup = (b, next) => b.bulleted &&
|
|
195
|
+
b.colon_wargear === null &&
|
|
196
|
+
!b.enhancement &&
|
|
197
|
+
!b.is_attachment &&
|
|
198
|
+
b.indent === modelIndent &&
|
|
199
|
+
next !== undefined &&
|
|
200
|
+
next.bulleted &&
|
|
201
|
+
next.indent > b.indent;
|
|
125
202
|
const wargear = new Map();
|
|
126
203
|
const addWargear = (raw_name, count) => {
|
|
127
204
|
const name = raw_name.trim();
|
|
@@ -136,9 +213,12 @@ function finishUnit(acc) {
|
|
|
136
213
|
let enhancement_points = null;
|
|
137
214
|
for (let i = 0; i < acc.bullets.length; i += 1) {
|
|
138
215
|
const b = acc.bullets[i];
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
216
|
+
// `Attached as: …` carries no model or gear; a `(Character)` role flags the
|
|
217
|
+
// unit as a character. Skip before model detection — it sits shallower than
|
|
218
|
+
// the models and would otherwise read as a model group.
|
|
219
|
+
if (b.is_attachment) {
|
|
220
|
+
if (b.sets_character)
|
|
221
|
+
is_character = true;
|
|
142
222
|
continue;
|
|
143
223
|
}
|
|
144
224
|
// Enhancement annotation (`Enhancements: X` or `X (+N pts)`).
|
|
@@ -159,9 +239,8 @@ function finishUnit(acc) {
|
|
|
159
239
|
}
|
|
160
240
|
continue;
|
|
161
241
|
}
|
|
162
|
-
// Model group:
|
|
163
|
-
|
|
164
|
-
if (next && next.indent > topIndent) {
|
|
242
|
+
// Model group: counted bullet at the model indent with a deeper bulleted child.
|
|
243
|
+
if (isModelGroup(b, acc.bullets[i + 1])) {
|
|
165
244
|
model_count += b.count ?? 1;
|
|
166
245
|
continue;
|
|
167
246
|
}
|
|
@@ -185,7 +264,8 @@ function finishUnit(acc) {
|
|
|
185
264
|
addWargear(token, 1);
|
|
186
265
|
continue;
|
|
187
266
|
}
|
|
188
|
-
//
|
|
267
|
+
// Everything else is wargear — a bulleted weapon under a model or an
|
|
268
|
+
// unbulleted continuation line, at any depth.
|
|
189
269
|
addWargear(b.name, b.count ?? 1);
|
|
190
270
|
}
|
|
191
271
|
if (model_count === 0)
|
|
@@ -247,13 +327,26 @@ export const gwHeaderlessAdapter = {
|
|
|
247
327
|
// Bullets attach to the open unit.
|
|
248
328
|
const bullet = RE_BULLET.exec(raw);
|
|
249
329
|
if (bullet) {
|
|
250
|
-
if (current)
|
|
251
|
-
current.bullets.push(parseBullet(bullet[1].length, bullet[2]));
|
|
330
|
+
if (current) {
|
|
331
|
+
current.bullets.push(parseBullet(bullet[1].length, bullet[2], true));
|
|
332
|
+
}
|
|
252
333
|
continue;
|
|
253
334
|
}
|
|
254
335
|
// GW export footer.
|
|
255
336
|
if (line.startsWith("Exported with"))
|
|
256
337
|
continue;
|
|
338
|
+
// The GW app bullets only the first wargear line under a model and emits
|
|
339
|
+
// the rest unbulleted, one indent deeper (` 4x Shuriken pistol`).
|
|
340
|
+
// Capture those `Nx …` continuation lines as the open unit's wargear at
|
|
341
|
+
// their real indent so `finishUnit` can place them. A unit header also
|
|
342
|
+
// lacks a bullet but carries a `(N pts)` parenthetical, so it is excluded
|
|
343
|
+
// here and handled just below.
|
|
344
|
+
const nx = RE_NX_PREFIX.exec(line);
|
|
345
|
+
if (current && nx && !RE_PTS_LINE.test(line)) {
|
|
346
|
+
const indent = raw.length - raw.trimStart().length;
|
|
347
|
+
current.bullets.push(parseBullet(indent, line, false));
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
257
350
|
// `## Section` markdown header (strip an optional `(N pts)` tail).
|
|
258
351
|
const md = RE_MD_SECTION.exec(line);
|
|
259
352
|
if (md) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gw-headerless.js","sourceRoot":"","sources":["../../src/import/gw-headerless.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Headerless plain-text adapter: the GW 40K app's *exported* list (no\n * `++…++` / `+ FACTION KEYWORD:` summary fence), the NewRecruit \"copy as text\"\n * dialect, and the markdown-ish `## Section (N pts)` shape hand-authored lists\n * use. All three share one body grammar; they differ only in cosmetic framing,\n * so a single lenient parser covers them.\n *\n * Shape (any of):\n *\n * ```\n * <list name> (1995 Points) ← title line (consumed, not a unit)\n * World Eaters ← faction (bare preamble line)\n * Berzerker Warband ← detachment (bare preamble line)\n * Strike Force (2,000 Points) ← battle-size metadata\n *\n * CHARACTERS ← ALL-CAPS role section …\n * ## Battleline (200 pts) ← … or `##` markdown section …\n * Epic Hero: ← … or `Title:` colon section\n *\n * Khârn the Betrayer (100 Points) ← unit header: Name (N pts|Points)\n * • Warlord ← annotation\n * • 1x Gorechild ← Nx wargear (single-model unit)\n * • Enhancements: Berzerker Glaive ← enhancement\n * Khorne Berzerkers (180 Points)\n * • 9x Khorne Berzerker ← model group (has ◦ children) …\n * ◦ 8x Bolt pistol ← … children are squad-wide wargear\n * • 4x Intercessor: Bolt rifle ← model group (colon wargear, no children)\n * ```\n *\n * **Model vs wargear** (the crux), unified across dialects: a top-level bullet\n * is a *model group* when it carries a `: wargear` colon **or** is followed by\n * deeper-indented child bullets; its `Nx` count (default 1) adds to the model\n * count. Otherwise it is plain wargear (an `Nx`/bare item) or an annotation\n * (`Warlord`, `… Character`, `Enhancements: …`).\n *\n * **Faction / detachment**: the GW export lists them as bare lines between the\n * title and the first section (`World Eaters` / `Berzerker Warband`). The first\n * two such preamble lines are captured so `resolve` can scope to them.\n *\n * **Disjointness**: this adapter is the fallback for bullet-bearing text that\n * the framed adapters reject — it declines input carrying the GW\n * `+ FACTION KEYWORD:` fence (→ {@link gwAdapter}), the NewRecruit\n * `# ++ Army Roster ++` header (→ newrecruit-simple), or WTC `N with` body\n * lines, and requires at least one `•`/`◦` bullet.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport { inferBattleSizeRaw } from \"./newrecruit-text.js\";\n\nconst CHARACTERS_SECTION = \"CHARACTERS\";\nconst ALLIED_SECTION = \"ALLIED UNITS\";\nconst CHARACTER_SUFFIX = \" Character\";\nconst WARLORD_MARKER = \"Warlord\";\n\n/** Title / unit header: `Name (N pts|Points)` with an optional trailing comment\n * (the GW export sometimes appends TO notes). Points may carry thousands\n * commas. Case-insensitive `pts`/`points`. */\nconst RE_PTS_LINE = /^(.+?)\\s*\\(\\s*([\\d,]+)\\s*(?:pts?|points?)\\s*\\).*$/i;\n/** `## Section [ (N pts) ]` markdown header. */\nconst RE_MD_SECTION = /^#{1,6}\\s*(.+?)\\s*$/;\n/** ALL-CAPS role section (`CHARACTERS`, `OTHER DATASHEETS`, …). */\nconst RE_CAPS_SECTION = /^[A-Z][A-Z0-9 \\-/&]+$/;\n/** `Title:` colon section (`Epic Hero:`, `Battleline:`). */\nconst RE_COLON_SECTION = /^([A-Za-z][\\w /&-]*):\\s*$/;\n/** Bullet line: leading indent, a `•` or `◦` marker, then the body. */\nconst RE_BULLET = /^([\\t ]*)[•◦]\\s*(.+?)\\s*$/u;\nconst RE_NX_PREFIX = /^(\\d+)x\\s+(.+)$/i;\n/** Inline enhancement annotation: `Name (+N pts)`. */\nconst RE_ENHANCEMENT_ANNOT = /^(.+?)\\s*\\(\\+\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\n/** `Enhancements: X` / `E: X` enhancement bullet. */\nconst RE_ENHANCEMENT_LABEL = /^(?:e|enh|enhancement|enhancements)\\s*:\\s*(.+)$/i;\nconst RE_WITH_LINE = /^[\\t ]*\\d+\\s+with\\b/m;\nconst RE_BULLET_ANYWHERE = /^[\\t ]*[•◦]/mu;\n/** ListForge-text first line: `<name> - <faction> - <detachment> (N Points)`.\n * Used only to *decline* — that framed header belongs to `listForgeTextAdapter`,\n * which runs ahead of us; declining keeps the matchers mutually exclusive. */\nconst RE_LISTFORGE_FIRST_LINE = /^(.+)\\s\\(\\s*\\d+\\s*Points?\\s*\\)\\s*$/i;\n\n/** Battle-size labels that look like unit headers (`Strike Force (2,000 Points)`)\n * but are army metadata, not datasheets. */\nconst BATTLE_SIZE_NAMES = new Set([\n \"combat patrol\",\n \"incursion\",\n \"strike force\",\n \"onslaught\",\n]);\n\nfunction parsePts(raw: string): number | null {\n const n = Number.parseInt(raw.replace(/,/g, \"\"), 10);\n return Number.isNaN(n) ? null : n;\n}\n\n/** Accept bullet-bearing plain text that no framed adapter claims. */\nfunction headerlessText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n if (!RE_BULLET_ANYWHERE.test(decoded)) return null; // need a bullet\n if (decoded.includes(\"+ FACTION KEYWORD:\")) return null; // framed GW → gwAdapter\n if (RE_WITH_LINE.test(decoded)) return null; // WTC-full\n const lines = decoded.split(/\\r?\\n/);\n // ListForge-text's `name - faction - detachment (N Points)` header → defer to\n // listForgeTextAdapter (registered ahead of us). Mirrors its own matcher so\n // the two stay disjoint, per the importer's single-match invariant.\n const firstNonBlank = lines.find((l) => l.trim().length > 0);\n const lf = firstNonBlank && RE_LISTFORGE_FIRST_LINE.exec(firstNonBlank.trim());\n if (lf && lf[1].split(\" - \").length >= 3) return null;\n // NewRecruit `# ++ Army Roster ++` → newrecruit-simple.\n if (\n lines.some((l) => {\n const t = l.trim();\n return t.startsWith(\"# ++\") && t.includes(\"Army Roster\");\n })\n ) {\n return null;\n }\n // Require a `Name (N pts|Points)` line somewhere — the unit/title signature.\n return lines.some((l) => RE_PTS_LINE.test(l.trim())) ? decoded : null;\n}\n\ninterface Bullet {\n indent: number;\n count: number | null;\n /** Model/wargear name (after any `Nx` and before any `: wargear`). */\n name: string;\n /** Comma-separated wargear listed after a `:` on a model bullet. */\n colon_wargear: string | null;\n /** True for `Warlord` / `… Character` / `Enhancements:` annotations. */\n is_annotation: boolean;\n /** `[name, points]` when this bullet declares an enhancement. */\n enhancement: [string, number | null] | null;\n}\n\ninterface UnitAcc {\n raw_name: string;\n displayed_pts: number | null;\n is_character_section: boolean;\n bullets: Bullet[];\n}\n\nfunction parseBullet(indent: number, body: string): Bullet {\n // Enhancement label first — `Enhancements: X` must not read as a model.\n const label = RE_ENHANCEMENT_LABEL.exec(body);\n if (label) {\n return {\n indent,\n count: null,\n name: \"\",\n colon_wargear: null,\n is_annotation: true,\n enhancement: [label[1].trim(), null],\n };\n }\n\n const nx = RE_NX_PREFIX.exec(body);\n const count = nx ? Number.parseInt(nx[1], 10) : null;\n const rest = (nx ? nx[2] : body).trim();\n\n // `Name (+N pts)` enhancement annotation.\n const annot = RE_ENHANCEMENT_ANNOT.exec(rest);\n if (annot) {\n const pts = Number.parseInt(annot[2], 10);\n return {\n indent,\n count,\n name: rest,\n colon_wargear: null,\n is_annotation: true,\n enhancement: [annot[1].trim(), Number.isNaN(pts) ? null : pts],\n };\n }\n\n // `ModelType: w1, w2` — a model bullet with inline wargear.\n const idx = rest.indexOf(\":\");\n if (idx >= 0) {\n const wargear = rest.slice(idx + 1).trim();\n return {\n indent,\n count,\n name: rest.slice(0, idx).trim(),\n colon_wargear: wargear.length > 0 ? wargear : null,\n is_annotation: false,\n enhancement: null,\n };\n }\n\n // Bare token: annotation iff it has no count (Warlord / Character / wargear).\n return {\n indent,\n count,\n name: rest,\n colon_wargear: null,\n is_annotation: count === null,\n enhancement: null,\n };\n}\n\nfunction finishUnit(acc: UnitAcc): ParsedUnit {\n const topIndent = acc.bullets.length\n ? Math.min(...acc.bullets.map((b) => b.indent))\n : 0;\n\n const wargear = new Map<string, number>();\n const addWargear = (raw_name: string, count: number): void => {\n const name = raw_name.trim();\n if (!name) return;\n wargear.set(name, (wargear.get(name) ?? 0) + count);\n };\n\n let model_count = 0;\n let is_warlord = false;\n let is_character = acc.is_character_section;\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n\n for (let i = 0; i < acc.bullets.length; i += 1) {\n const b = acc.bullets[i];\n\n // Child bullet: a model group's squad-wide wargear (count already total).\n if (b.indent > topIndent) {\n addWargear(b.name, b.count ?? 1);\n continue;\n }\n\n // Enhancement annotation (`Enhancements: X` or `X (+N pts)`).\n if (b.enhancement) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = b.enhancement[0];\n enhancement_points = b.enhancement[1];\n }\n continue;\n }\n\n // Model with inline `: wargear` (the `##`/fixture dialect).\n if (b.colon_wargear !== null) {\n const n = b.count ?? 1;\n model_count += n;\n for (const item of b.colon_wargear.split(\",\").map((s) => s.trim())) {\n if (item) addWargear(item, n);\n }\n continue;\n }\n\n // Model group: top-level bullet followed by deeper child bullets.\n const next = acc.bullets[i + 1];\n if (next && next.indent > topIndent) {\n model_count += b.count ?? 1;\n continue;\n }\n\n // Annotation (no count): Warlord / Character flags, else bare wargear.\n if (b.is_annotation) {\n const leftover: string[] = [];\n for (const token of b.name.split(\",\").map((t) => t.trim())) {\n if (!token) continue;\n if (token === WARLORD_MARKER) {\n is_warlord = true;\n } else if (token.endsWith(CHARACTER_SUFFIX)) {\n is_character = true;\n } else {\n leftover.push(token);\n }\n }\n for (const token of leftover) addWargear(token, 1);\n continue;\n }\n\n // Plain `Nx` wargear on a single-model unit.\n addWargear(b.name, b.count ?? 1);\n }\n\n if (model_count === 0) model_count = 1;\n\n let points = acc.displayed_pts;\n if (acc.displayed_pts !== null && enhancement_points !== null) {\n points = Math.max(0, acc.displayed_pts - enhancement_points);\n }\n\n return {\n raw_name: acc.raw_name,\n is_character,\n model_count,\n points,\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear: [...wargear].map(\n ([raw_name, count]): ParsedWargear => ({ raw_name, count }),\n ),\n };\n}\n\nfunction isBattleSize(name: string): boolean {\n return BATTLE_SIZE_NAMES.has(name.trim().toLowerCase());\n}\n\nexport const gwHeaderlessAdapter: FormatAdapter = {\n // Provenance: a GW-family plain-text export. Reuses the `gw` id so no schema\n // churn is needed for a new label (mirrors the Rust adapter).\n id: \"gw\",\n\n matches(decoded: unknown): boolean {\n return headerlessText(decoded) !== null;\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = headerlessText(decoded);\n if (text === null) {\n throw new Error(\"gw-headerless: not a headerless plain-text list\");\n }\n\n let name = \"Imported roster\";\n let declared_limit: number | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n let current: UnitAcc | null = null;\n let section: string | null = null;\n let allied = 0;\n let consumed_title = false;\n // The GW app export lists faction then detachment as bare lines between the\n // title and the first section. Capture the first two so `resolve` can scope\n // to them; later bare lines (stray notes) are ignored.\n let faction_raw_name: string | null = null;\n const detachment_raw_names: string[] = [];\n\n const flush = (): void => {\n if (current) {\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (const rawLine of text.split(\"\\n\")) {\n const raw = rawLine.replace(/\\r+$/, \"\");\n const line = raw.trim();\n if (!line) continue;\n\n // Bullets attach to the open unit.\n const bullet = RE_BULLET.exec(raw);\n if (bullet) {\n if (current) current.bullets.push(parseBullet(bullet[1].length, bullet[2]));\n continue;\n }\n\n // GW export footer.\n if (line.startsWith(\"Exported with\")) continue;\n\n // `## Section` markdown header (strip an optional `(N pts)` tail).\n const md = RE_MD_SECTION.exec(line);\n if (md) {\n flush();\n const pts = RE_PTS_LINE.exec(md[1]);\n section = pts ? pts[1].trim() : md[1].trim();\n continue;\n }\n\n // First `Name (N pts|Points)` line is the roster title, not a unit.\n const pts = RE_PTS_LINE.exec(line);\n if (pts) {\n const header_name = pts[1].trim();\n const points = parsePts(pts[2]);\n if (!consumed_title && current === null && units.length === 0) {\n consumed_title = true;\n name = header_name;\n declared_limit = points;\n continue;\n }\n // Battle-size metadata (`Strike Force (2,000 Points)`).\n if (isBattleSize(header_name)) {\n battle_size_raw = line;\n if (declared_limit === null) declared_limit = points;\n continue;\n }\n // A real unit header.\n flush();\n const inChars = section?.toLowerCase() === CHARACTERS_SECTION.toLowerCase();\n if (section === ALLIED_SECTION) allied += 1;\n current = {\n raw_name: header_name,\n displayed_pts: points,\n is_character_section: inChars,\n bullets: [],\n };\n continue;\n }\n\n // Section headers without points (ALL-CAPS role, `Title:` colon).\n if (RE_CAPS_SECTION.test(line) || RE_COLON_SECTION.test(line)) {\n flush();\n section = line.replace(/:\\s*$/, \"\").trim();\n continue;\n }\n\n // Anything else (faction/detachment preamble, stray notes).\n if (!consumed_title && current === null && units.length === 0) {\n // Very first content line with no `(N pts)` title → use as name.\n consumed_title = true;\n name = line;\n } else if (current === null && units.length === 0) {\n // Preamble after the title, before the first unit: faction then\n // detachment. Names are resolved (and warned on miss) downstream.\n if (faction_raw_name === null) {\n faction_raw_name = line;\n } else if (detachment_raw_names.length === 0) {\n detachment_raw_names.push(line);\n }\n }\n }\n flush();\n\n let total_computed = 0;\n for (const u of units) {\n total_computed += (u.points ?? 0) + (u.enhancement_points ?? 0);\n }\n\n return {\n name,\n generated_by: null,\n faction_raw_name,\n detachment_raw_names,\n battle_size_raw: battle_size_raw ?? inferBattleSizeRaw(declared_limit),\n declared_limit,\n total_reported: null,\n total_computed,\n units,\n multi_force: allied > 0,\n };\n },\n};\n\n// Internals re-exported for unit tests.\nexport const _internals = {\n headerlessText,\n parseBullet,\n};\n"]}
|
|
1
|
+
{"version":3,"file":"gw-headerless.js","sourceRoot":"","sources":["../../src/import/gw-headerless.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Headerless plain-text adapter: the GW 40K app's *exported* list (no\n * `++…++` / `+ FACTION KEYWORD:` summary fence), the NewRecruit \"copy as text\"\n * dialect, and the markdown-ish `## Section (N pts)` shape hand-authored lists\n * use. All three share one body grammar; they differ only in cosmetic framing,\n * so a single lenient parser covers them.\n *\n * Shape (any of):\n *\n * ```\n * <list name> (1995 Points) ← title line (consumed, not a unit)\n * World Eaters ← faction (bare preamble line)\n * Berzerker Warband ← detachment (bare preamble line)\n * Strike Force (2,000 Points) ← battle-size metadata\n *\n * CHARACTERS ← ALL-CAPS role section …\n * ## Battleline (200 pts) ← … or `##` markdown section …\n * Epic Hero: ← … or `Title:` colon section\n *\n * Khârn the Betrayer (100 Points) ← unit header: Name (N pts|Points)\n * • Warlord ← annotation\n * • 1x Gorechild ← Nx wargear (single-model unit)\n * • Enhancements: Berzerker Glaive ← enhancement\n * Khorne Berzerkers (180 Points)\n * • 9x Khorne Berzerker ← model group (has ◦ children) …\n * ◦ 8x Bolt pistol ← … children are squad-wide wargear\n * • 4x Intercessor: Bolt rifle ← model group (colon wargear, no children)\n *\n * Fire Dragons (120 points) ← GW app v2.0.5 \"Attached Units\" nesting\n * • Attached as: Bodyguard ← attachment annotation (skipped)\n * • 4x Fire Dragon ← model group (deeper • child) …\n * • 4x Close combat weapon ← … first weapon is bulleted …\n * 4x Dragon fusion gun ← … the rest are unbulleted continuations\n * ```\n *\n * **Model vs wargear** (the crux), unified across dialects: a model group is a\n * bulleted entry, at the shallowest model indent, that is followed by a *deeper\n * bulleted* line (its squad-wide wargear); its `Nx` count (default 1) adds to\n * the model count. Keying on the child being *bulleted* keeps a lone bulleted\n * weapon trailed by unbulleted continuation lines (a Fire Prism's `Prism\n * cannon`) as wargear, not a model. A bullet with a `: wargear` colon is also a\n * model group. Everything else is wargear — a `Nx`/bare item, or the GW app's\n * unbulleted continuation lines (v2.0.5 bullets only the *first* weapon under a\n * model and emits the rest unbulleted, one indent deeper) — or an annotation\n * (`Warlord`, `… Character`, `Enhancements: …`, `Attached as: …`).\n *\n * **Faction / detachment**: the GW export lists them as bare lines between the\n * title and the first section (`World Eaters` / `Berzerker Warband`). The first\n * two such preamble lines are captured so `resolve` can scope to them.\n *\n * **Disjointness**: this adapter is the fallback for bullet-bearing text that\n * the framed adapters reject — it declines input carrying the GW\n * `+ FACTION KEYWORD:` fence (→ {@link gwAdapter}), the NewRecruit\n * `# ++ Army Roster ++` header (→ newrecruit-simple), or WTC `N with` body\n * lines, and requires at least one `•`/`◦` bullet.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport { inferBattleSizeRaw } from \"./newrecruit-text.js\";\n\nconst CHARACTERS_SECTION = \"CHARACTERS\";\nconst ALLIED_SECTION = \"ALLIED UNITS\";\nconst CHARACTER_SUFFIX = \" Character\";\nconst WARLORD_MARKER = \"Warlord\";\n\n/** Title / unit header: `Name (N pts|Points)` with an optional trailing comment\n * (the GW export sometimes appends TO notes). Points may carry thousands\n * commas. Case-insensitive `pts`/`points`. */\nconst RE_PTS_LINE = /^(.+?)\\s*\\(\\s*([\\d,]+)\\s*(?:pts?|points?)\\s*\\).*$/i;\n/** `## Section [ (N pts) ]` markdown header. */\nconst RE_MD_SECTION = /^#{1,6}\\s*(.+?)\\s*$/;\n/** ALL-CAPS role section (`CHARACTERS`, `OTHER DATASHEETS`, …). */\nconst RE_CAPS_SECTION = /^[A-Z][A-Z0-9 \\-/&]+$/;\n/** `Title:` colon section (`Epic Hero:`, `Battleline:`). */\nconst RE_COLON_SECTION = /^([A-Za-z][\\w /&-]*):\\s*$/;\n/** Bullet line: leading indent, a `•` or `◦` marker, then the body. */\nconst RE_BULLET = /^([\\t ]*)[•◦]\\s*(.+?)\\s*$/u;\nconst RE_NX_PREFIX = /^(\\d+)x\\s+(.+)$/i;\n/** Inline enhancement annotation: `Name (+N pts)`. */\nconst RE_ENHANCEMENT_ANNOT = /^(.+?)\\s*\\(\\+\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\n/** `Enhancements: X` / `E: X` enhancement bullet. */\nconst RE_ENHANCEMENT_LABEL = /^(?:e|enh|enhancement|enhancements)\\s*:\\s*(.+)$/i;\n/** `Attached as: <role>` — GW app v2.0.5 attachment annotation (captures the\n * role so `(Character)` can flag the unit). */\nconst RE_ATTACHED_AS = /^attached\\s+as\\s*:\\s*(.+)$/i;\nconst RE_WITH_LINE = /^[\\t ]*\\d+\\s+with\\b/m;\nconst RE_BULLET_ANYWHERE = /^[\\t ]*[•◦]/mu;\n/** ListForge-text first line: `<name> - <faction> - <detachment> (N Points)`.\n * Used only to *decline* — that framed header belongs to `listForgeTextAdapter`,\n * which runs ahead of us; declining keeps the matchers mutually exclusive. */\nconst RE_LISTFORGE_FIRST_LINE = /^(.+)\\s\\(\\s*\\d+\\s*Points?\\s*\\)\\s*$/i;\n\n/** Battle-size labels that look like unit headers (`Strike Force (2,000 Points)`)\n * but are army metadata, not datasheets. */\nconst BATTLE_SIZE_NAMES = new Set([\n \"combat patrol\",\n \"incursion\",\n \"strike force\",\n \"onslaught\",\n]);\n\nfunction parsePts(raw: string): number | null {\n const n = Number.parseInt(raw.replace(/,/g, \"\"), 10);\n return Number.isNaN(n) ? null : n;\n}\n\n/** A line of only `+` characters — the BCP summary block's fence. */\nconst RE_PLUS_FENCE = /^\\++$/;\n/** A line inside that block identifying it as BCP's (not GW's own `+ …` fence). */\nconst RE_BCP_SUMMARY_MARKER = /^\\s*(?:Player Name|Team Name|Factions Used|Army Points)\\s*:/im;\n\n/**\n * BCP prepends a `++…++`-fenced summary block (`Player Name:` / `Factions Used:`\n * / `Army Points: N` / …) to text-type lists. It is BCP metadata, not part of the\n * pasted roster, and it derails the body grammar: the fence line gets consumed as\n * the roster title, so the *real* title line (`House Rosecairn (1995 points)`)\n * becomes a phantom unit and its points double the computed total. Strip the\n * leading block when present. Only a block whose fence pair wraps a BCP marker is\n * removed, so a framed GW export's own `+ FACTION KEYWORD:` fence is left intact.\n */\nfunction stripBcpSummary(text: string): string {\n const lines = text.split(/\\r?\\n/);\n let open = 0;\n while (open < lines.length && lines[open].trim() === \"\") open += 1;\n if (open >= lines.length || !RE_PLUS_FENCE.test(lines[open].trim())) return text;\n let close = -1;\n for (let j = open + 1; j < lines.length; j += 1) {\n if (RE_PLUS_FENCE.test(lines[j].trim())) {\n close = j;\n break;\n }\n }\n if (close === -1) return text;\n const block = lines.slice(open + 1, close).join(\"\\n\");\n if (!RE_BCP_SUMMARY_MARKER.test(block)) return text;\n return lines.slice(close + 1).join(\"\\n\");\n}\n\n/** Accept bullet-bearing plain text that no framed adapter claims. */\nfunction headerlessText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n const text = stripBcpSummary(decoded);\n if (!RE_BULLET_ANYWHERE.test(text)) return null; // need a bullet\n if (text.includes(\"+ FACTION KEYWORD:\")) return null; // framed GW → gwAdapter\n if (RE_WITH_LINE.test(text)) return null; // WTC-full\n const lines = text.split(/\\r?\\n/);\n // ListForge-text's `name - faction - detachment (N Points)` header → defer to\n // listForgeTextAdapter (registered ahead of us). Mirrors its own matcher so\n // the two stay disjoint, per the importer's single-match invariant.\n const firstNonBlank = lines.find((l) => l.trim().length > 0);\n const lf = firstNonBlank && RE_LISTFORGE_FIRST_LINE.exec(firstNonBlank.trim());\n if (lf && lf[1].split(\" - \").length >= 3) return null;\n // NewRecruit `# ++ Army Roster ++` → newrecruit-simple.\n if (\n lines.some((l) => {\n const t = l.trim();\n return t.startsWith(\"# ++\") && t.includes(\"Army Roster\");\n })\n ) {\n return null;\n }\n // Require a `Name (N pts|Points)` line somewhere — the unit/title signature.\n return lines.some((l) => RE_PTS_LINE.test(l.trim())) ? text : null;\n}\n\ninterface Bullet {\n indent: number;\n count: number | null;\n /** Model/wargear name (after any `Nx` and before any `: wargear`). */\n name: string;\n /** Comma-separated wargear listed after a `:` on a model bullet. */\n colon_wargear: string | null;\n /** True for `Warlord` / `… Character` / `Enhancements:` / `Attached as:`\n * annotations. */\n is_annotation: boolean;\n /** `[name, points]` when this bullet declares an enhancement. */\n enhancement: [string, number | null] | null;\n /** True when the source line carried a `•`/`◦` marker; false for the GW app's\n * unbulleted continuation wargear lines. Model detection keys on this: a model\n * is an entry followed by a *deeper bulleted* line, so a lone bulleted weapon\n * with plain continuations (Fire Prism) is not mistaken for a model. */\n bulleted: boolean;\n /** True for an `Attached as: …` v2.0.5 annotation — never a model or wargear,\n * even though it sits (bulleted) shallower than the models. */\n is_attachment: boolean;\n /** An `Attached as: … (Character)` annotation flags the unit as a character. */\n sets_character: boolean;\n}\n\ninterface UnitAcc {\n raw_name: string;\n displayed_pts: number | null;\n is_character_section: boolean;\n bullets: Bullet[];\n}\n\nfunction parseBullet(indent: number, body: string, bulleted: boolean): Bullet {\n const base = { indent, bulleted, is_attachment: false, sets_character: false } as const;\n\n // `Attached as: <role>` — the v2.0.5 multi-detachment export tags each unit\n // with how it attaches. Pure annotation: never a model or wargear. Its `:`\n // must be caught before the generic colon split below, else the role is read\n // as inline wargear. A `(Character)` role flags the unit as a character.\n const attached = RE_ATTACHED_AS.exec(body);\n if (attached) {\n return {\n ...base,\n count: null,\n name: \"\",\n colon_wargear: null,\n is_annotation: true,\n enhancement: null,\n is_attachment: true,\n sets_character: /\\(\\s*Character\\s*\\)/i.test(attached[1]),\n };\n }\n\n // Enhancement label first — `Enhancements: X` must not read as a model.\n const label = RE_ENHANCEMENT_LABEL.exec(body);\n if (label) {\n return {\n ...base,\n count: null,\n name: \"\",\n colon_wargear: null,\n is_annotation: true,\n enhancement: [label[1].trim(), null],\n };\n }\n\n const nx = RE_NX_PREFIX.exec(body);\n const count = nx ? Number.parseInt(nx[1], 10) : null;\n const rest = (nx ? nx[2] : body).trim();\n\n // `Name (+N pts)` enhancement annotation.\n const annot = RE_ENHANCEMENT_ANNOT.exec(rest);\n if (annot) {\n const pts = Number.parseInt(annot[2], 10);\n return {\n ...base,\n count,\n name: rest,\n colon_wargear: null,\n is_annotation: true,\n enhancement: [annot[1].trim(), Number.isNaN(pts) ? null : pts],\n };\n }\n\n // `ModelType: w1, w2` — a model bullet with inline wargear.\n const idx = rest.indexOf(\":\");\n if (idx >= 0) {\n const wargear = rest.slice(idx + 1).trim();\n return {\n ...base,\n count,\n name: rest.slice(0, idx).trim(),\n colon_wargear: wargear.length > 0 ? wargear : null,\n is_annotation: false,\n enhancement: null,\n };\n }\n\n // Bare token: annotation iff it has no count (Warlord / Character / wargear).\n return {\n ...base,\n count,\n name: rest,\n colon_wargear: null,\n is_annotation: count === null,\n enhancement: null,\n };\n}\n\nfunction finishUnit(acc: UnitAcc): ParsedUnit {\n // Models live at the shallowest *bulleted* indent that isn't an annotation,\n // enhancement, or colon-wargear line. The GW v2.0.5 export prefixes each unit\n // with an `Attached as:` bullet shallower than the models, so the old\n // \"min of all indents\" would misplace the model level — filter those out.\n const modelEligible = acc.bullets.filter(\n (b) =>\n b.bulleted &&\n !b.is_attachment &&\n !b.enhancement &&\n b.colon_wargear === null,\n );\n const modelIndent = modelEligible.length\n ? Math.min(...modelEligible.map((b) => b.indent))\n : 0;\n\n // A model group: a bulleted entry at the model indent that is followed by a\n // *deeper bulleted* line (its squad-wide wargear). Keying on the child being\n // bulleted keeps a lone bulleted weapon trailed by plain continuation lines\n // (Fire Prism's Prism cannon) as wargear, not a model. A count-less model\n // name (`• Bloodreaper` with children) still counts as one model (`?? 1`).\n const isModelGroup = (b: Bullet, next: Bullet | undefined): boolean =>\n b.bulleted &&\n b.colon_wargear === null &&\n !b.enhancement &&\n !b.is_attachment &&\n b.indent === modelIndent &&\n next !== undefined &&\n next.bulleted &&\n next.indent > b.indent;\n\n const wargear = new Map<string, number>();\n const addWargear = (raw_name: string, count: number): void => {\n const name = raw_name.trim();\n if (!name) return;\n wargear.set(name, (wargear.get(name) ?? 0) + count);\n };\n\n let model_count = 0;\n let is_warlord = false;\n let is_character = acc.is_character_section;\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n\n for (let i = 0; i < acc.bullets.length; i += 1) {\n const b = acc.bullets[i];\n\n // `Attached as: …` carries no model or gear; a `(Character)` role flags the\n // unit as a character. Skip before model detection — it sits shallower than\n // the models and would otherwise read as a model group.\n if (b.is_attachment) {\n if (b.sets_character) is_character = true;\n continue;\n }\n\n // Enhancement annotation (`Enhancements: X` or `X (+N pts)`).\n if (b.enhancement) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = b.enhancement[0];\n enhancement_points = b.enhancement[1];\n }\n continue;\n }\n\n // Model with inline `: wargear` (the `##`/fixture dialect).\n if (b.colon_wargear !== null) {\n const n = b.count ?? 1;\n model_count += n;\n for (const item of b.colon_wargear.split(\",\").map((s) => s.trim())) {\n if (item) addWargear(item, n);\n }\n continue;\n }\n\n // Model group: counted bullet at the model indent with a deeper bulleted child.\n if (isModelGroup(b, acc.bullets[i + 1])) {\n model_count += b.count ?? 1;\n continue;\n }\n\n // Annotation (no count): Warlord / Character flags, else bare wargear.\n if (b.is_annotation) {\n const leftover: string[] = [];\n for (const token of b.name.split(\",\").map((t) => t.trim())) {\n if (!token) continue;\n if (token === WARLORD_MARKER) {\n is_warlord = true;\n } else if (token.endsWith(CHARACTER_SUFFIX)) {\n is_character = true;\n } else {\n leftover.push(token);\n }\n }\n for (const token of leftover) addWargear(token, 1);\n continue;\n }\n\n // Everything else is wargear — a bulleted weapon under a model or an\n // unbulleted continuation line, at any depth.\n addWargear(b.name, b.count ?? 1);\n }\n\n if (model_count === 0) model_count = 1;\n\n let points = acc.displayed_pts;\n if (acc.displayed_pts !== null && enhancement_points !== null) {\n points = Math.max(0, acc.displayed_pts - enhancement_points);\n }\n\n return {\n raw_name: acc.raw_name,\n is_character,\n model_count,\n points,\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear: [...wargear].map(\n ([raw_name, count]): ParsedWargear => ({ raw_name, count }),\n ),\n };\n}\n\nfunction isBattleSize(name: string): boolean {\n return BATTLE_SIZE_NAMES.has(name.trim().toLowerCase());\n}\n\nexport const gwHeaderlessAdapter: FormatAdapter = {\n // Provenance: a GW-family plain-text export. Reuses the `gw` id so no schema\n // churn is needed for a new label (mirrors the Rust adapter).\n id: \"gw\",\n\n matches(decoded: unknown): boolean {\n return headerlessText(decoded) !== null;\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = headerlessText(decoded);\n if (text === null) {\n throw new Error(\"gw-headerless: not a headerless plain-text list\");\n }\n\n let name = \"Imported roster\";\n let declared_limit: number | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n let current: UnitAcc | null = null;\n let section: string | null = null;\n let allied = 0;\n let consumed_title = false;\n // The GW app export lists faction then detachment as bare lines between the\n // title and the first section. Capture the first two so `resolve` can scope\n // to them; later bare lines (stray notes) are ignored.\n let faction_raw_name: string | null = null;\n const detachment_raw_names: string[] = [];\n\n const flush = (): void => {\n if (current) {\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (const rawLine of text.split(\"\\n\")) {\n const raw = rawLine.replace(/\\r+$/, \"\");\n const line = raw.trim();\n if (!line) continue;\n\n // Bullets attach to the open unit.\n const bullet = RE_BULLET.exec(raw);\n if (bullet) {\n if (current) {\n current.bullets.push(parseBullet(bullet[1].length, bullet[2], true));\n }\n continue;\n }\n\n // GW export footer.\n if (line.startsWith(\"Exported with\")) continue;\n\n // The GW app bullets only the first wargear line under a model and emits\n // the rest unbulleted, one indent deeper (` 4x Shuriken pistol`).\n // Capture those `Nx …` continuation lines as the open unit's wargear at\n // their real indent so `finishUnit` can place them. A unit header also\n // lacks a bullet but carries a `(N pts)` parenthetical, so it is excluded\n // here and handled just below.\n const nx = RE_NX_PREFIX.exec(line);\n if (current && nx && !RE_PTS_LINE.test(line)) {\n const indent = raw.length - raw.trimStart().length;\n current.bullets.push(parseBullet(indent, line, false));\n continue;\n }\n\n // `## Section` markdown header (strip an optional `(N pts)` tail).\n const md = RE_MD_SECTION.exec(line);\n if (md) {\n flush();\n const pts = RE_PTS_LINE.exec(md[1]);\n section = pts ? pts[1].trim() : md[1].trim();\n continue;\n }\n\n // First `Name (N pts|Points)` line is the roster title, not a unit.\n const pts = RE_PTS_LINE.exec(line);\n if (pts) {\n const header_name = pts[1].trim();\n const points = parsePts(pts[2]);\n if (!consumed_title && current === null && units.length === 0) {\n consumed_title = true;\n name = header_name;\n declared_limit = points;\n continue;\n }\n // Battle-size metadata (`Strike Force (2,000 Points)`).\n if (isBattleSize(header_name)) {\n battle_size_raw = line;\n if (declared_limit === null) declared_limit = points;\n continue;\n }\n // A real unit header.\n flush();\n const inChars = section?.toLowerCase() === CHARACTERS_SECTION.toLowerCase();\n if (section === ALLIED_SECTION) allied += 1;\n current = {\n raw_name: header_name,\n displayed_pts: points,\n is_character_section: inChars,\n bullets: [],\n };\n continue;\n }\n\n // Section headers without points (ALL-CAPS role, `Title:` colon).\n if (RE_CAPS_SECTION.test(line) || RE_COLON_SECTION.test(line)) {\n flush();\n section = line.replace(/:\\s*$/, \"\").trim();\n continue;\n }\n\n // Anything else (faction/detachment preamble, stray notes).\n if (!consumed_title && current === null && units.length === 0) {\n // Very first content line with no `(N pts)` title → use as name.\n consumed_title = true;\n name = line;\n } else if (current === null && units.length === 0) {\n // Preamble after the title, before the first unit: faction then\n // detachment. Names are resolved (and warned on miss) downstream.\n if (faction_raw_name === null) {\n faction_raw_name = line;\n } else if (detachment_raw_names.length === 0) {\n detachment_raw_names.push(line);\n }\n }\n }\n flush();\n\n let total_computed = 0;\n for (const u of units) {\n total_computed += (u.points ?? 0) + (u.enhancement_points ?? 0);\n }\n\n return {\n name,\n generated_by: null,\n faction_raw_name,\n detachment_raw_names,\n battle_size_raw: battle_size_raw ?? inferBattleSizeRaw(declared_limit),\n declared_limit,\n total_reported: null,\n total_computed,\n units,\n multi_force: allied > 0,\n };\n },\n};\n\n// Internals re-exported for unit tests.\nexport const _internals = {\n headerlessText,\n parseBullet,\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpaca-software/40kdc-data",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The 40kdc Warhammer 40K dataset behind a linked, typed API — find units, follow them to their weapons, abilities, phases, and factions. Also validates data against the canonical JSON Schemas.",
|
|
6
6
|
"keywords": [
|