@alpaca-software/40kdc-data 0.5.11 → 0.5.14
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 +10 -0
- package/dist/author-input-pack.d.ts +27 -0
- package/dist/author-input-pack.d.ts.map +1 -0
- package/dist/author-input-pack.js +198 -0
- package/dist/author-input-pack.js.map +1 -0
- package/dist/codegen-data.js +1 -0
- package/dist/codegen-data.js.map +1 -1
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +3 -1
- package/dist/commands/import.js.map +1 -1
- package/dist/convert-faction.d.ts.map +1 -1
- package/dist/convert-faction.js +22 -2
- package/dist/convert-faction.js.map +1 -1
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/dataset.d.ts +2 -1
- package/dist/data/dataset.d.ts.map +1 -1
- package/dist/data/dataset.js +2 -0
- package/dist/data/dataset.js.map +1 -1
- package/dist/data/entities.d.ts +22 -0
- package/dist/data/entities.d.ts.map +1 -1
- package/dist/data/entities.js +28 -0
- package/dist/data/entities.js.map +1 -1
- package/dist/data/index.d.ts +3 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +3 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/roster-resolve.d.ts +16 -1
- package/dist/data/roster-resolve.d.ts.map +1 -1
- package/dist/data/roster-resolve.js +19 -0
- package/dist/data/roster-resolve.js.map +1 -1
- package/dist/data/types.d.ts +3 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/data/types.js +1 -0
- package/dist/data/types.js.map +1 -1
- package/dist/export/newrecruit-json.d.ts.map +1 -1
- package/dist/export/newrecruit-json.js +4 -3
- package/dist/export/newrecruit-json.js.map +1 -1
- package/dist/export/newrecruit-simple.js +2 -2
- package/dist/export/newrecruit-simple.js.map +1 -1
- package/dist/export/newrecruit-wtc.d.ts.map +1 -1
- package/dist/export/newrecruit-wtc.js +3 -1
- package/dist/export/newrecruit-wtc.js.map +1 -1
- package/dist/export/rosterizer.d.ts.map +1 -1
- package/dist/export/rosterizer.js +6 -7
- package/dist/export/rosterizer.js.map +1 -1
- package/dist/extract-faction-pack.d.ts +61 -0
- package/dist/extract-faction-pack.d.ts.map +1 -0
- package/dist/extract-faction-pack.js +318 -0
- package/dist/extract-faction-pack.js.map +1 -0
- package/dist/gen-conformance.js +37 -4
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +46 -2
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/import/gw-headerless.d.ts +70 -0
- package/dist/import/gw-headerless.d.ts.map +1 -0
- package/dist/import/gw-headerless.js +343 -0
- package/dist/import/gw-headerless.js.map +1 -0
- package/dist/import/gw.js +1 -1
- package/dist/import/gw.js.map +1 -1
- package/dist/import/import-roster.d.ts.map +1 -1
- package/dist/import/import-roster.js +8 -0
- package/dist/import/import-roster.js.map +1 -1
- package/dist/import/listforge-text.js +1 -1
- package/dist/import/listforge-text.js.map +1 -1
- package/dist/import/listforge.d.ts.map +1 -1
- package/dist/import/listforge.js +18 -3
- package/dist/import/listforge.js.map +1 -1
- package/dist/import/newrecruit-json.d.ts.map +1 -1
- package/dist/import/newrecruit-json.js +21 -3
- package/dist/import/newrecruit-json.js.map +1 -1
- package/dist/import/newrecruit-simple.js +1 -1
- package/dist/import/newrecruit-simple.js.map +1 -1
- package/dist/import/newrecruit-wtc.js +1 -1
- package/dist/import/newrecruit-wtc.js.map +1 -1
- package/dist/import/resolve.d.ts.map +1 -1
- package/dist/import/resolve.js +46 -18
- package/dist/import/resolve.js.map +1 -1
- package/dist/import/roster-json.d.ts +5 -3
- package/dist/import/roster-json.d.ts.map +1 -1
- package/dist/import/roster-json.js +8 -4
- package/dist/import/roster-json.js.map +1 -1
- package/dist/import/rosterizer.js +3 -3
- package/dist/import/rosterizer.js.map +1 -1
- package/dist/import/types.d.ts +15 -4
- package/dist/import/types.d.ts.map +1 -1
- package/dist/import/types.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/merge-faction-pack.d.ts +2 -0
- package/dist/merge-faction-pack.d.ts.map +1 -0
- package/dist/merge-faction-pack.js +157 -0
- package/dist/merge-faction-pack.js.map +1 -0
- package/dist/pack-blocks.d.ts +32 -0
- package/dist/pack-blocks.d.ts.map +1 -0
- package/dist/pack-blocks.js +99 -0
- package/dist/pack-blocks.js.map +1 -0
- package/dist/rube-goldberg.js +1 -1
- package/dist/rube-goldberg.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +33 -1
- package/dist/runner.js.map +1 -1
- package/dist/scope.d.ts +31 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +34 -0
- package/dist/scope.js.map +1 -0
- package/dist/seed-detachment-abilities.d.ts +2 -0
- package/dist/seed-detachment-abilities.d.ts.map +1 -0
- package/dist/seed-detachment-abilities.js +137 -0
- package/dist/seed-detachment-abilities.js.map +1 -0
- package/dist/translate/effect.d.ts +17 -3
- package/dist/translate/effect.d.ts.map +1 -1
- package/dist/translate/effect.js +23 -4
- package/dist/translate/effect.js.map +1 -1
- package/dist/translate/index.d.ts +1 -1
- package/dist/translate/index.d.ts.map +1 -1
- package/dist/translate/index.js +1 -1
- package/dist/translate/index.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +1 -0
- package/dist/validate.js.map +1 -1
- package/package.json +3 -1
- package/schemas/core/detachment.schema.json +6 -0
- package/schemas/core/hull-shape.schema.json +30 -0
- package/schemas/core/roster.schema.json +24 -8
- package/schemas/core/stratagem.schema.json +2 -2
- package/schemas/core/unit-composition.schema.json +7 -0
- package/schemas/enrichment/ability-dsl/ability.schema.json +14 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listforge-text.js","sourceRoot":"","sources":["../../src/import/listforge-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ListForge plain-text adapter: lower ListForge's copy-paste text export to a\n * {@link ParsedRoster}.\n *\n * This is the bullet-list text users copy out of the ListForge app (distinct\n * from the base64+gzip share-JSON the `listforge` adapter handles). Shape:\n *\n * ```\n * all gas no breaks - Chaos Daemons - Daemonic Incursion (1995 Points)\n *\n * Epic Hero:\n * Rotigus (250 pts)\n * • Gnarlrod\n * • Streams of brackish filth\n *\n * Battleline:\n * Bloodletters (110 pts)\n * • Bloodreaper\n * • Hellblade\n * • Daemonic Icon\n * • 9x Bloodletter\n * • 9x Hellblade\n * ```\n *\n * - The first non-blank line is `<list name> - <faction> - <detachment>\n * (<N> Points)`. A list name containing ` - ` breaks the split — a documented\n * ListForge limitation, not ours.\n * - Sections are mixed-case battlefield-role lines ending with `:`\n * (`Epic Hero:`, `Character:`, `Battleline:`, …). Units under `Epic Hero:` or\n * `Character:` are characters.\n * - Bullet classification mirrors the GW adapter: a top-level bullet with\n * deeper children is a **model group** (its `Nx` count — implicitly 1 —\n * adds to the model count); without children it's **wargear**. Child-bullet\n * `Nx` counts are already squad-wide totals; a child without a count is one\n * item (`• Hellblade` under a lone Bloodreaper).\n * - `E: <name>` is the enhancement annotation (ListForge reports no points for\n * it, so `enhancement_points` stays null and unit points stay as displayed).\n * A bare `Warlord` bullet flags the warlord.\n *\n * **Disjointness**: the `(N Points)` first-line suffix is unique to this\n * format — newrecruit-simple's first line ends `- [N pts]`, the GW export\n * opens with a `++++` fence, and the WTC formats carry `N with` lines or no\n * bullets at all.\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 FIRST_LINE = /^(.+)\\s\\(\\s*(\\d+)\\s*Points?\\s*\\)\\s*$/i;\nconst SECTION_HEADER = /^[A-Za-z][A-Za-z0-9 /&'-]*:$/;\nconst UNIT_HEADER = /^(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst BULLET_LINE = /^(\\s*)•\\s*(.+?)\\s*$/u;\nconst NX_PREFIX = /^(\\d+)x\\s+(.+)$/;\nconst BULLET = /^[\\t ]*•/mu;\nconst WITH_LINE = /^[\\t ]*\\d+\\s+with\\b/m;\n\nconst ENHANCEMENT_PREFIX = \"E: \";\nconst WARLORD_MARKER = \"Warlord\";\nconst CHARACTER_SECTIONS = new Set([\"epic hero\", \"character\"]);\n\n/** Accept plain text whose first non-blank line is the ListForge\n * `name - faction - detachment (N Points)` header, with `•` bullets and no\n * WTC `N with` lines. */\nfunction isListForgeText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n const firstNonBlank = decoded\n .split(/\\r?\\n/)\n .find((l) => l.trim().length > 0);\n if (!firstNonBlank) return null;\n const first = FIRST_LINE.exec(firstNonBlank.trim());\n if (!first || first[1].split(\" - \").length < 3) return null;\n if (!BULLET.test(decoded)) return null;\n if (WITH_LINE.test(decoded)) return null;\n return decoded;\n}\n\ninterface Header {\n name: string;\n faction_raw_name: string | null;\n detachment_raw_name: string | null;\n total_reported: number | null;\n}\n\nfunction parseFirstLine(line: string): Header | null {\n const m = FIRST_LINE.exec(line.trim());\n if (!m) return null;\n const parts = m[1].split(\" - \").map((s) => s.trim()).filter(Boolean);\n if (parts.length < 3) return null;\n // `<list name> - <faction> - <detachment>`; the name is everything before\n // the trailing two segments so faction names with hyphens stay intact only\n // when ListForge itself doesn't insert ` - ` (it doesn't).\n return {\n name: parts.slice(0, parts.length - 2).join(\" - \"),\n faction_raw_name: parts[parts.length - 2],\n detachment_raw_name: parts[parts.length - 1],\n total_reported: Number.parseInt(m[2], 10),\n };\n}\n\ninterface Bullet {\n indent: number;\n count: number | null;\n text: string;\n}\n\ninterface UnitAcc {\n raw_name: string;\n displayed_pts: number | null;\n is_character: boolean;\n bullets: Bullet[];\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 let model_count = 0;\n let is_warlord = false;\n let enhancement_raw_name: string | null = null;\n\n const addWargear = (raw_name: string, count: number): void => {\n wargear.set(raw_name, (wargear.get(raw_name) ?? 0) + count);\n };\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 weapon. ListForge child counts are\n // squad-wide totals; a count-less child is a single item.\n if (b.indent > topIndent) {\n addWargear(b.text, b.count ?? 1);\n continue;\n }\n\n // Top-level annotations.\n if (b.count === null) {\n if (b.text === WARLORD_MARKER) {\n is_warlord = true;\n continue;\n }\n if (b.text.startsWith(ENHANCEMENT_PREFIX)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = b.text.slice(ENHANCEMENT_PREFIX.length).trim();\n }\n continue;\n }\n }\n\n // Top-level entry: a model group when it has child bullets beneath it,\n // otherwise plain wargear. Either way a missing `Nx` count means 1.\n const next = acc.bullets[i + 1];\n if (next && next.indent > b.indent) {\n model_count += b.count ?? 1;\n } else {\n addWargear(b.text, b.count ?? 1);\n }\n }\n\n if (model_count === 0) model_count = 1;\n\n return {\n raw_name: acc.raw_name,\n is_character: acc.is_character,\n model_count,\n points: acc.displayed_pts,\n is_warlord,\n enhancement_raw_name,\n // ListForge's text export reports no enhancement cost, so the unit's\n // displayed points stay as-is and no enhancement points are claimed.\n enhancement_points: null,\n wargear: [...wargear].map(\n ([raw_name, count]): ParsedWargear => ({ raw_name, count }),\n ),\n };\n}\n\nexport const listForgeTextAdapter: FormatAdapter = {\n id: \"listforge-text\",\n\n matches(decoded: unknown): boolean {\n return isListForgeText(decoded) !== null;\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isListForgeText(decoded);\n if (text === null) {\n throw new Error(\"listforge-text: input is not a ListForge text export\");\n }\n\n const lines = text.split(/\\r?\\n/);\n let header: Header | null = null;\n const units: ParsedUnit[] = [];\n let current: UnitAcc | null = null;\n let sectionIsCharacter = false;\n\n const finalize = (): void => {\n if (current) {\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line) continue;\n\n if (!header) {\n header = parseFirstLine(line);\n if (header) continue;\n }\n\n const bulletMatch = BULLET_LINE.exec(raw);\n if (bulletMatch) {\n if (current) {\n const rest = bulletMatch[2];\n const nx = NX_PREFIX.exec(rest);\n current.bullets.push({\n indent: bulletMatch[1].length,\n count: nx ? Number.parseInt(nx[1], 10) : null,\n text: (nx ? nx[2] : rest).trim(),\n });\n }\n continue;\n }\n\n if (SECTION_HEADER.test(line)) {\n finalize();\n sectionIsCharacter = CHARACTER_SECTIONS.has(\n line.slice(0, -1).trim().toLowerCase(),\n );\n continue;\n }\n\n const unitMatch = UNIT_HEADER.exec(line);\n if (unitMatch) {\n finalize();\n current = {\n raw_name: unitMatch[1].trim(),\n displayed_pts: Number.parseInt(unitMatch[2], 10),\n is_character: sectionIsCharacter,\n bullets: [],\n };\n }\n }\n finalize();\n\n if (!header) {\n throw new Error(\"listforge-text: missing ListForge header line\");\n }\n\n let total_computed = 0;\n for (const u of units) total_computed += u.points ?? 0;\n\n // Like the GW export, ListForge text reports only the army total — use it\n // as the declared limit so battle-size inference stays round-trippable.\n const declared_limit = header.total_reported;\n\n return {\n name: header.name,\n generated_by: \"List Forge\",\n faction_raw_name: header.faction_raw_name,\n detachment_raw_name: header.detachment_raw_name,\n battle_size_raw: inferBattleSizeRaw(declared_limit),\n declared_limit,\n total_reported: header.total_reported,\n total_computed,\n units,\n multi_force: false,\n };\n },\n};\n\n// Internals re-exported for unit tests.\nexport const _internals = {\n isListForgeText,\n parseFirstLine,\n};\n"]}
|
|
1
|
+
{"version":3,"file":"listforge-text.js","sourceRoot":"","sources":["../../src/import/listforge-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ListForge plain-text adapter: lower ListForge's copy-paste text export to a\n * {@link ParsedRoster}.\n *\n * This is the bullet-list text users copy out of the ListForge app (distinct\n * from the base64+gzip share-JSON the `listforge` adapter handles). Shape:\n *\n * ```\n * all gas no breaks - Chaos Daemons - Daemonic Incursion (1995 Points)\n *\n * Epic Hero:\n * Rotigus (250 pts)\n * • Gnarlrod\n * • Streams of brackish filth\n *\n * Battleline:\n * Bloodletters (110 pts)\n * • Bloodreaper\n * • Hellblade\n * • Daemonic Icon\n * • 9x Bloodletter\n * • 9x Hellblade\n * ```\n *\n * - The first non-blank line is `<list name> - <faction> - <detachment>\n * (<N> Points)`. A list name containing ` - ` breaks the split — a documented\n * ListForge limitation, not ours.\n * - Sections are mixed-case battlefield-role lines ending with `:`\n * (`Epic Hero:`, `Character:`, `Battleline:`, …). Units under `Epic Hero:` or\n * `Character:` are characters.\n * - Bullet classification mirrors the GW adapter: a top-level bullet with\n * deeper children is a **model group** (its `Nx` count — implicitly 1 —\n * adds to the model count); without children it's **wargear**. Child-bullet\n * `Nx` counts are already squad-wide totals; a child without a count is one\n * item (`• Hellblade` under a lone Bloodreaper).\n * - `E: <name>` is the enhancement annotation (ListForge reports no points for\n * it, so `enhancement_points` stays null and unit points stay as displayed).\n * A bare `Warlord` bullet flags the warlord.\n *\n * **Disjointness**: the `(N Points)` first-line suffix is unique to this\n * format — newrecruit-simple's first line ends `- [N pts]`, the GW export\n * opens with a `++++` fence, and the WTC formats carry `N with` lines or no\n * bullets at all.\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 FIRST_LINE = /^(.+)\\s\\(\\s*(\\d+)\\s*Points?\\s*\\)\\s*$/i;\nconst SECTION_HEADER = /^[A-Za-z][A-Za-z0-9 /&'-]*:$/;\nconst UNIT_HEADER = /^(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst BULLET_LINE = /^(\\s*)•\\s*(.+?)\\s*$/u;\nconst NX_PREFIX = /^(\\d+)x\\s+(.+)$/;\nconst BULLET = /^[\\t ]*•/mu;\nconst WITH_LINE = /^[\\t ]*\\d+\\s+with\\b/m;\n\nconst ENHANCEMENT_PREFIX = \"E: \";\nconst WARLORD_MARKER = \"Warlord\";\nconst CHARACTER_SECTIONS = new Set([\"epic hero\", \"character\"]);\n\n/** Accept plain text whose first non-blank line is the ListForge\n * `name - faction - detachment (N Points)` header, with `•` bullets and no\n * WTC `N with` lines. */\nfunction isListForgeText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n const firstNonBlank = decoded\n .split(/\\r?\\n/)\n .find((l) => l.trim().length > 0);\n if (!firstNonBlank) return null;\n const first = FIRST_LINE.exec(firstNonBlank.trim());\n if (!first || first[1].split(\" - \").length < 3) return null;\n if (!BULLET.test(decoded)) return null;\n if (WITH_LINE.test(decoded)) return null;\n return decoded;\n}\n\ninterface Header {\n name: string;\n faction_raw_name: string | null;\n detachment_raw_name: string | null;\n total_reported: number | null;\n}\n\nfunction parseFirstLine(line: string): Header | null {\n const m = FIRST_LINE.exec(line.trim());\n if (!m) return null;\n const parts = m[1].split(\" - \").map((s) => s.trim()).filter(Boolean);\n if (parts.length < 3) return null;\n // `<list name> - <faction> - <detachment>`; the name is everything before\n // the trailing two segments so faction names with hyphens stay intact only\n // when ListForge itself doesn't insert ` - ` (it doesn't).\n return {\n name: parts.slice(0, parts.length - 2).join(\" - \"),\n faction_raw_name: parts[parts.length - 2],\n detachment_raw_name: parts[parts.length - 1],\n total_reported: Number.parseInt(m[2], 10),\n };\n}\n\ninterface Bullet {\n indent: number;\n count: number | null;\n text: string;\n}\n\ninterface UnitAcc {\n raw_name: string;\n displayed_pts: number | null;\n is_character: boolean;\n bullets: Bullet[];\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 let model_count = 0;\n let is_warlord = false;\n let enhancement_raw_name: string | null = null;\n\n const addWargear = (raw_name: string, count: number): void => {\n wargear.set(raw_name, (wargear.get(raw_name) ?? 0) + count);\n };\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 weapon. ListForge child counts are\n // squad-wide totals; a count-less child is a single item.\n if (b.indent > topIndent) {\n addWargear(b.text, b.count ?? 1);\n continue;\n }\n\n // Top-level annotations.\n if (b.count === null) {\n if (b.text === WARLORD_MARKER) {\n is_warlord = true;\n continue;\n }\n if (b.text.startsWith(ENHANCEMENT_PREFIX)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = b.text.slice(ENHANCEMENT_PREFIX.length).trim();\n }\n continue;\n }\n }\n\n // Top-level entry: a model group when it has child bullets beneath it,\n // otherwise plain wargear. Either way a missing `Nx` count means 1.\n const next = acc.bullets[i + 1];\n if (next && next.indent > b.indent) {\n model_count += b.count ?? 1;\n } else {\n addWargear(b.text, b.count ?? 1);\n }\n }\n\n if (model_count === 0) model_count = 1;\n\n return {\n raw_name: acc.raw_name,\n is_character: acc.is_character,\n model_count,\n points: acc.displayed_pts,\n is_warlord,\n enhancement_raw_name,\n // ListForge's text export reports no enhancement cost, so the unit's\n // displayed points stay as-is and no enhancement points are claimed.\n enhancement_points: null,\n wargear: [...wargear].map(\n ([raw_name, count]): ParsedWargear => ({ raw_name, count }),\n ),\n };\n}\n\nexport const listForgeTextAdapter: FormatAdapter = {\n id: \"listforge-text\",\n\n matches(decoded: unknown): boolean {\n return isListForgeText(decoded) !== null;\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isListForgeText(decoded);\n if (text === null) {\n throw new Error(\"listforge-text: input is not a ListForge text export\");\n }\n\n const lines = text.split(/\\r?\\n/);\n let header: Header | null = null;\n const units: ParsedUnit[] = [];\n let current: UnitAcc | null = null;\n let sectionIsCharacter = false;\n\n const finalize = (): void => {\n if (current) {\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line) continue;\n\n if (!header) {\n header = parseFirstLine(line);\n if (header) continue;\n }\n\n const bulletMatch = BULLET_LINE.exec(raw);\n if (bulletMatch) {\n if (current) {\n const rest = bulletMatch[2];\n const nx = NX_PREFIX.exec(rest);\n current.bullets.push({\n indent: bulletMatch[1].length,\n count: nx ? Number.parseInt(nx[1], 10) : null,\n text: (nx ? nx[2] : rest).trim(),\n });\n }\n continue;\n }\n\n if (SECTION_HEADER.test(line)) {\n finalize();\n sectionIsCharacter = CHARACTER_SECTIONS.has(\n line.slice(0, -1).trim().toLowerCase(),\n );\n continue;\n }\n\n const unitMatch = UNIT_HEADER.exec(line);\n if (unitMatch) {\n finalize();\n current = {\n raw_name: unitMatch[1].trim(),\n displayed_pts: Number.parseInt(unitMatch[2], 10),\n is_character: sectionIsCharacter,\n bullets: [],\n };\n }\n }\n finalize();\n\n if (!header) {\n throw new Error(\"listforge-text: missing ListForge header line\");\n }\n\n let total_computed = 0;\n for (const u of units) total_computed += u.points ?? 0;\n\n // Like the GW export, ListForge text reports only the army total — use it\n // as the declared limit so battle-size inference stays round-trippable.\n const declared_limit = header.total_reported;\n\n return {\n name: header.name,\n generated_by: \"List Forge\",\n faction_raw_name: header.faction_raw_name,\n detachment_raw_names: header.detachment_raw_name ? [header.detachment_raw_name] : [],\n battle_size_raw: inferBattleSizeRaw(declared_limit),\n declared_limit,\n total_reported: header.total_reported,\n total_computed,\n units,\n multi_force: false,\n };\n },\n};\n\n// Internals re-exported for unit tests.\nexport const _internals = {\n isListForgeText,\n parseFirstLine,\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listforge.d.ts","sourceRoot":"","sources":["../../src/import/listforge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"listforge.d.ts","sourceRoot":"","sources":["../../src/import/listforge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA+NlD,eAAO,MAAM,gBAAgB,EAAE,aA4D9B,CAAC"}
|
package/dist/import/listforge.js
CHANGED
|
@@ -112,6 +112,21 @@ function configValue(selections, configName) {
|
|
|
112
112
|
const child = childSelections(node)[0];
|
|
113
113
|
return child ? selectionName(child) : null;
|
|
114
114
|
}
|
|
115
|
+
/** Every value under a named config, across repeated blocks and multiple
|
|
116
|
+
* children, in source order. Used for multi-detachment 11e lists. */
|
|
117
|
+
function configValues(selections, configName) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const node of selections) {
|
|
120
|
+
if (selectionName(node) !== configName)
|
|
121
|
+
continue;
|
|
122
|
+
for (const child of childSelections(node)) {
|
|
123
|
+
const name = selectionName(child);
|
|
124
|
+
if (name)
|
|
125
|
+
out.push(name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
115
130
|
function parseLimit(label) {
|
|
116
131
|
if (!label)
|
|
117
132
|
return null;
|
|
@@ -171,12 +186,12 @@ export const listForgeAdapter = {
|
|
|
171
186
|
}
|
|
172
187
|
const forces = asArray(roster.forces);
|
|
173
188
|
// Configuration lives among each force's top-level selections.
|
|
174
|
-
|
|
189
|
+
const detachment_raw_names = [];
|
|
175
190
|
let battle_size_raw = null;
|
|
176
191
|
const units = [];
|
|
177
192
|
for (const force of forces) {
|
|
178
193
|
const top = childSelections(force);
|
|
179
|
-
|
|
194
|
+
detachment_raw_names.push(...configValues(top, "Detachment"));
|
|
180
195
|
battle_size_raw ??= configValue(top, "Battle Size");
|
|
181
196
|
for (const sel of top) {
|
|
182
197
|
if (isUnitSelection(sel))
|
|
@@ -202,7 +217,7 @@ export const listForgeAdapter = {
|
|
|
202
217
|
name: asString(payload.name) ?? asString(roster.name) ?? "Imported roster",
|
|
203
218
|
generated_by: asString(payload.generatedBy),
|
|
204
219
|
faction_raw_name: factions[0] ?? null,
|
|
205
|
-
|
|
220
|
+
detachment_raw_names,
|
|
206
221
|
battle_size_raw,
|
|
207
222
|
declared_limit: parseLimit(battle_size_raw),
|
|
208
223
|
total_reported,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listforge.js","sourceRoot":"","sources":["../../src/import/listforge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ListForge adapter: lower a decoded ListForge \"share JSON\" payload (a\n * BattleScribe-derived roster tree) to a {@link ParsedRoster}.\n *\n * The walk reads an ALLOWLIST of fields only — `name`, `number`, `type`,\n * `categories[].name`, `group`, and `costs` point values — and never touches\n * `rules[].description` or ability `profiles[].characteristics[].$text`, which\n * carry reproduced rules text. This keeps the importer's output free of\n * copyrighted prose by construction.\n *\n * Selection-tree shape (recursive `selections`):\n * - Configuration nodes (`type: \"upgrade\"`) named \"Detachment\" / \"Battle Size\"\n * carry the chosen value as their first child selection.\n * - Unit nodes (`type: \"model\" | \"unit\"`) carry role categories, a points cost,\n * and — nested anywhere beneath them — their wargear (weapon-category\n * selections), enhancement (a selection whose `group` starts \"Enhancements\"),\n * the \"Warlord\" marker, and model sub-selections.\n * - Every unit carries a `\"Faction: <Name>\"` category.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\n\nconst PTS_COST_NAME = \"pts\";\nconst FACTION_CATEGORY = /^Faction:\\s*(.+)$/;\nconst POINTS_LIMIT = /(\\d[\\d,]*)\\s*Point/i;\nconst ENHANCEMENT_GROUP_PREFIX = \"Enhancements\";\nconst CHARACTER_CATEGORIES = new Set([\"Character\", \"Epic Hero\"]);\nconst WEAPON_CATEGORY_SUFFIX = \" Weapon\"; // \"Ranged Weapon\", \"Melee Weapon\", \"Psychic Weapon\"\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_HOST_PREFIX = \"https://newrecruit\";\n\n// --- Minimal structural views of the parts of the payload we read. ----------\n\ninterface RawCategory {\n name?: unknown;\n}\ninterface RawCost {\n name?: unknown;\n value?: unknown;\n}\ninterface RawSelection {\n name?: unknown;\n type?: unknown;\n number?: unknown;\n group?: unknown;\n categories?: unknown;\n costs?: unknown;\n selections?: unknown;\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction selectionName(sel: RawSelection): string {\n return asString(sel.name) ?? \"\";\n}\n\nfunction selectionType(sel: RawSelection): string {\n return asString(sel.type) ?? \"\";\n}\n\n/** A selection's multiplicity (`number`), defaulting to 1. */\nfunction selectionCount(sel: RawSelection): number {\n return typeof sel.number === \"number\" && sel.number > 0 ? sel.number : 1;\n}\n\n/** Point value from a selection's cost block, or null when absent. */\nfunction pointsOf(sel: RawSelection): number | null {\n for (const raw of asArray(sel.costs)) {\n const cost = raw as RawCost;\n if (asString(cost.name) === PTS_COST_NAME && typeof cost.value === \"number\") {\n return cost.value;\n }\n }\n return null;\n}\n\nfunction categoryNames(sel: RawSelection): string[] {\n return asArray(sel.categories)\n .map((c) => asString((c as RawCategory).name))\n .filter((n): n is string => n !== null);\n}\n\nfunction childSelections(sel: RawSelection): RawSelection[] {\n return asArray(sel.selections) as RawSelection[];\n}\n\n/** Depth-first visit of a selection and everything beneath it. */\nfunction walk(sel: RawSelection, visit: (s: RawSelection) => void): void {\n visit(sel);\n for (const child of childSelections(sel)) walk(child, visit);\n}\n\nfunction isUnitSelection(sel: RawSelection): boolean {\n const type = selectionType(sel);\n return type === \"model\" || type === \"unit\";\n}\n\nfunction isCharacter(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => CHARACTER_CATEGORIES.has(n));\n}\n\nfunction isWeaponSelection(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => n.endsWith(WEAPON_CATEGORY_SUFFIX));\n}\n\nfunction isEnhancementSelection(sel: RawSelection): boolean {\n const group = asString(sel.group);\n return group !== null && group.startsWith(ENHANCEMENT_GROUP_PREFIX);\n}\n\n/** Sum the model count of a unit from its nested model selections. */\nfunction modelCount(unit: RawSelection): number {\n let total = 0;\n walk(unit, (s) => {\n if (selectionType(s) === \"model\") total += selectionCount(s);\n });\n return total > 0 ? total : selectionCount(unit);\n}\n\n/** Build a parsed unit from a top-level unit selection. */\nfunction parseUnit(unit: RawSelection): ParsedUnit {\n const wargear: ParsedWargear[] = [];\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n let is_warlord = false;\n\n for (const node of childSelections(unit)) {\n walk(node, (s) => {\n if (isEnhancementSelection(s)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = selectionName(s);\n enhancement_points = pointsOf(s);\n }\n return;\n }\n if (selectionName(s) === \"Warlord\") {\n is_warlord = true;\n return;\n }\n if (isWeaponSelection(s)) {\n wargear.push({ raw_name: selectionName(s), count: selectionCount(s) });\n }\n });\n }\n\n return {\n raw_name: selectionName(unit),\n is_character: isCharacter(unit),\n model_count: modelCount(unit),\n points: pointsOf(unit),\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear,\n };\n}\n\n/** Value carried as the first child of a named configuration selection. */\nfunction configValue(\n selections: RawSelection[],\n configName: string,\n): string | null {\n const node = selections.find((s) => selectionName(s) === configName);\n if (!node) return null;\n const child = childSelections(node)[0];\n return child ? selectionName(child) : null;\n}\n\nfunction parseLimit(label: string | null): number | null {\n if (!label) return null;\n const match = POINTS_LIMIT.exec(label);\n if (!match) return null;\n return Number.parseInt(match[1].replace(/,/g, \"\"), 10);\n}\n\n/** First `\"Faction: X\"` category found anywhere; reports all distinct names. */\nfunction collectFactions(forces: RawSelection[]): string[] {\n const seen = new Set<string>();\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n for (const name of categoryNames(s)) {\n const match = FACTION_CATEGORY.exec(name);\n if (match) seen.add(match[1].trim());\n }\n });\n }\n }\n return [...seen];\n}\n\ninterface RawRoster {\n name?: unknown;\n costs?: unknown;\n forces?: unknown;\n xmlns?: unknown;\n generatedBy?: unknown;\n}\ninterface RawPayload {\n name?: unknown;\n generatedBy?: unknown;\n roster?: unknown;\n}\n\nfunction rosterOf(decoded: unknown): RawRoster | null {\n if (!decoded || typeof decoded !== \"object\") return null;\n const roster = (decoded as RawPayload).roster;\n if (!roster || typeof roster !== \"object\") return null;\n if (!Array.isArray((roster as RawRoster).forces)) return null;\n return roster as RawRoster;\n}\n\n/** Detect a NewRecruit-flavoured BattleScribe payload. ListForge's matcher\n * excludes these so the greedy first-match dispatcher routes them to the\n * NewRecruit adapter without falling through to here. */\nfunction hasNewRecruitSignature(decoded: unknown, roster: RawRoster): boolean {\n if (asString(roster.xmlns) === NEWRECRUIT_XMLNS) return true;\n const genBy =\n asString((decoded as RawPayload).generatedBy) ?? asString(roster.generatedBy);\n return genBy !== null && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX);\n}\n\nexport const listForgeAdapter: FormatAdapter = {\n id: \"listforge\",\n\n matches(decoded: unknown): boolean {\n const roster = rosterOf(decoded);\n if (!roster) return false;\n return !hasNewRecruitSignature(decoded, roster);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const payload = decoded as RawPayload;\n const roster = rosterOf(decoded);\n if (!roster) {\n throw new Error(\"listforge: payload has no roster.forces array\");\n }\n\n const forces = asArray(roster.forces) as RawSelection[];\n\n // Configuration lives among each force's top-level selections.\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n for (const force of forces) {\n const top = childSelections(force);\n detachment_raw_name ??= configValue(top, \"Detachment\");\n battle_size_raw ??= configValue(top, \"Battle Size\");\n for (const sel of top) {\n if (isUnitSelection(sel)) units.push(parseUnit(sel));\n }\n }\n\n const factions = collectFactions(forces);\n const total_reported = pointsOf(roster as RawSelection);\n\n // Honest computed total: sum every cost line in the tree. A unit's own cost\n // and its nested enhancement's cost are distinct lines that together make up\n // the unit's army contribution, so a full walk reproduces the army total.\n let total_computed = 0;\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n const pts = pointsOf(s);\n if (pts) total_computed += pts;\n });\n }\n }\n\n return {\n name: asString(payload.name) ?? asString(roster.name) ?? \"Imported roster\",\n generated_by: asString(payload.generatedBy),\n faction_raw_name: factions[0] ?? null,\n detachment_raw_name,\n battle_size_raw,\n declared_limit: parseLimit(battle_size_raw),\n total_reported,\n total_computed,\n units,\n multi_force: factions.length > 1,\n };\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"listforge.js","sourceRoot":"","sources":["../../src/import/listforge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ListForge adapter: lower a decoded ListForge \"share JSON\" payload (a\n * BattleScribe-derived roster tree) to a {@link ParsedRoster}.\n *\n * The walk reads an ALLOWLIST of fields only — `name`, `number`, `type`,\n * `categories[].name`, `group`, and `costs` point values — and never touches\n * `rules[].description` or ability `profiles[].characteristics[].$text`, which\n * carry reproduced rules text. This keeps the importer's output free of\n * copyrighted prose by construction.\n *\n * Selection-tree shape (recursive `selections`):\n * - Configuration nodes (`type: \"upgrade\"`) named \"Detachment\" / \"Battle Size\"\n * carry the chosen value as their first child selection.\n * - Unit nodes (`type: \"model\" | \"unit\"`) carry role categories, a points cost,\n * and — nested anywhere beneath them — their wargear (weapon-category\n * selections), enhancement (a selection whose `group` starts \"Enhancements\"),\n * the \"Warlord\" marker, and model sub-selections.\n * - Every unit carries a `\"Faction: <Name>\"` category.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\n\nconst PTS_COST_NAME = \"pts\";\nconst FACTION_CATEGORY = /^Faction:\\s*(.+)$/;\nconst POINTS_LIMIT = /(\\d[\\d,]*)\\s*Point/i;\nconst ENHANCEMENT_GROUP_PREFIX = \"Enhancements\";\nconst CHARACTER_CATEGORIES = new Set([\"Character\", \"Epic Hero\"]);\nconst WEAPON_CATEGORY_SUFFIX = \" Weapon\"; // \"Ranged Weapon\", \"Melee Weapon\", \"Psychic Weapon\"\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_HOST_PREFIX = \"https://newrecruit\";\n\n// --- Minimal structural views of the parts of the payload we read. ----------\n\ninterface RawCategory {\n name?: unknown;\n}\ninterface RawCost {\n name?: unknown;\n value?: unknown;\n}\ninterface RawSelection {\n name?: unknown;\n type?: unknown;\n number?: unknown;\n group?: unknown;\n categories?: unknown;\n costs?: unknown;\n selections?: unknown;\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction selectionName(sel: RawSelection): string {\n return asString(sel.name) ?? \"\";\n}\n\nfunction selectionType(sel: RawSelection): string {\n return asString(sel.type) ?? \"\";\n}\n\n/** A selection's multiplicity (`number`), defaulting to 1. */\nfunction selectionCount(sel: RawSelection): number {\n return typeof sel.number === \"number\" && sel.number > 0 ? sel.number : 1;\n}\n\n/** Point value from a selection's cost block, or null when absent. */\nfunction pointsOf(sel: RawSelection): number | null {\n for (const raw of asArray(sel.costs)) {\n const cost = raw as RawCost;\n if (asString(cost.name) === PTS_COST_NAME && typeof cost.value === \"number\") {\n return cost.value;\n }\n }\n return null;\n}\n\nfunction categoryNames(sel: RawSelection): string[] {\n return asArray(sel.categories)\n .map((c) => asString((c as RawCategory).name))\n .filter((n): n is string => n !== null);\n}\n\nfunction childSelections(sel: RawSelection): RawSelection[] {\n return asArray(sel.selections) as RawSelection[];\n}\n\n/** Depth-first visit of a selection and everything beneath it. */\nfunction walk(sel: RawSelection, visit: (s: RawSelection) => void): void {\n visit(sel);\n for (const child of childSelections(sel)) walk(child, visit);\n}\n\nfunction isUnitSelection(sel: RawSelection): boolean {\n const type = selectionType(sel);\n return type === \"model\" || type === \"unit\";\n}\n\nfunction isCharacter(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => CHARACTER_CATEGORIES.has(n));\n}\n\nfunction isWeaponSelection(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => n.endsWith(WEAPON_CATEGORY_SUFFIX));\n}\n\nfunction isEnhancementSelection(sel: RawSelection): boolean {\n const group = asString(sel.group);\n return group !== null && group.startsWith(ENHANCEMENT_GROUP_PREFIX);\n}\n\n/** Sum the model count of a unit from its nested model selections. */\nfunction modelCount(unit: RawSelection): number {\n let total = 0;\n walk(unit, (s) => {\n if (selectionType(s) === \"model\") total += selectionCount(s);\n });\n return total > 0 ? total : selectionCount(unit);\n}\n\n/** Build a parsed unit from a top-level unit selection. */\nfunction parseUnit(unit: RawSelection): ParsedUnit {\n const wargear: ParsedWargear[] = [];\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n let is_warlord = false;\n\n for (const node of childSelections(unit)) {\n walk(node, (s) => {\n if (isEnhancementSelection(s)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = selectionName(s);\n enhancement_points = pointsOf(s);\n }\n return;\n }\n if (selectionName(s) === \"Warlord\") {\n is_warlord = true;\n return;\n }\n if (isWeaponSelection(s)) {\n wargear.push({ raw_name: selectionName(s), count: selectionCount(s) });\n }\n });\n }\n\n return {\n raw_name: selectionName(unit),\n is_character: isCharacter(unit),\n model_count: modelCount(unit),\n points: pointsOf(unit),\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear,\n };\n}\n\n/** Value carried as the first child of a named configuration selection. */\nfunction configValue(\n selections: RawSelection[],\n configName: string,\n): string | null {\n const node = selections.find((s) => selectionName(s) === configName);\n if (!node) return null;\n const child = childSelections(node)[0];\n return child ? selectionName(child) : null;\n}\n\n/** Every value under a named config, across repeated blocks and multiple\n * children, in source order. Used for multi-detachment 11e lists. */\nfunction configValues(selections: RawSelection[], configName: string): string[] {\n const out: string[] = [];\n for (const node of selections) {\n if (selectionName(node) !== configName) continue;\n for (const child of childSelections(node)) {\n const name = selectionName(child);\n if (name) out.push(name);\n }\n }\n return out;\n}\n\nfunction parseLimit(label: string | null): number | null {\n if (!label) return null;\n const match = POINTS_LIMIT.exec(label);\n if (!match) return null;\n return Number.parseInt(match[1].replace(/,/g, \"\"), 10);\n}\n\n/** First `\"Faction: X\"` category found anywhere; reports all distinct names. */\nfunction collectFactions(forces: RawSelection[]): string[] {\n const seen = new Set<string>();\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n for (const name of categoryNames(s)) {\n const match = FACTION_CATEGORY.exec(name);\n if (match) seen.add(match[1].trim());\n }\n });\n }\n }\n return [...seen];\n}\n\ninterface RawRoster {\n name?: unknown;\n costs?: unknown;\n forces?: unknown;\n xmlns?: unknown;\n generatedBy?: unknown;\n}\ninterface RawPayload {\n name?: unknown;\n generatedBy?: unknown;\n roster?: unknown;\n}\n\nfunction rosterOf(decoded: unknown): RawRoster | null {\n if (!decoded || typeof decoded !== \"object\") return null;\n const roster = (decoded as RawPayload).roster;\n if (!roster || typeof roster !== \"object\") return null;\n if (!Array.isArray((roster as RawRoster).forces)) return null;\n return roster as RawRoster;\n}\n\n/** Detect a NewRecruit-flavoured BattleScribe payload. ListForge's matcher\n * excludes these so the greedy first-match dispatcher routes them to the\n * NewRecruit adapter without falling through to here. */\nfunction hasNewRecruitSignature(decoded: unknown, roster: RawRoster): boolean {\n if (asString(roster.xmlns) === NEWRECRUIT_XMLNS) return true;\n const genBy =\n asString((decoded as RawPayload).generatedBy) ?? asString(roster.generatedBy);\n return genBy !== null && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX);\n}\n\nexport const listForgeAdapter: FormatAdapter = {\n id: \"listforge\",\n\n matches(decoded: unknown): boolean {\n const roster = rosterOf(decoded);\n if (!roster) return false;\n return !hasNewRecruitSignature(decoded, roster);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const payload = decoded as RawPayload;\n const roster = rosterOf(decoded);\n if (!roster) {\n throw new Error(\"listforge: payload has no roster.forces array\");\n }\n\n const forces = asArray(roster.forces) as RawSelection[];\n\n // Configuration lives among each force's top-level selections.\n const detachment_raw_names: string[] = [];\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n for (const force of forces) {\n const top = childSelections(force);\n detachment_raw_names.push(...configValues(top, \"Detachment\"));\n battle_size_raw ??= configValue(top, \"Battle Size\");\n for (const sel of top) {\n if (isUnitSelection(sel)) units.push(parseUnit(sel));\n }\n }\n\n const factions = collectFactions(forces);\n const total_reported = pointsOf(roster as RawSelection);\n\n // Honest computed total: sum every cost line in the tree. A unit's own cost\n // and its nested enhancement's cost are distinct lines that together make up\n // the unit's army contribution, so a full walk reproduces the army total.\n let total_computed = 0;\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n const pts = pointsOf(s);\n if (pts) total_computed += pts;\n });\n }\n }\n\n return {\n name: asString(payload.name) ?? asString(roster.name) ?? \"Imported roster\",\n generated_by: asString(payload.generatedBy),\n faction_raw_name: factions[0] ?? null,\n detachment_raw_names,\n battle_size_raw,\n declared_limit: parseLimit(battle_size_raw),\n total_reported,\n total_computed,\n units,\n multi_force: factions.length > 1,\n };\n },\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"newrecruit-json.d.ts","sourceRoot":"","sources":["../../src/import/newrecruit-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"newrecruit-json.d.ts","sourceRoot":"","sources":["../../src/import/newrecruit-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA6OlD,eAAO,MAAM,qBAAqB,EAAE,aA4DnC,CAAC"}
|
|
@@ -106,6 +106,24 @@ function configValue(selections, configName) {
|
|
|
106
106
|
const child = childSelections(node)[0];
|
|
107
107
|
return child ? selectionName(child) : null;
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Every value selected under a named config — across repeated config nodes
|
|
111
|
+
* (one `Detachment` block per detachment) and multiple children of a single
|
|
112
|
+
* block. Source order is preserved. Used for multi-detachment 11e lists.
|
|
113
|
+
*/
|
|
114
|
+
function configValues(selections, configName) {
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const node of selections) {
|
|
117
|
+
if (selectionName(node) !== configName)
|
|
118
|
+
continue;
|
|
119
|
+
for (const child of childSelections(node)) {
|
|
120
|
+
const name = selectionName(child);
|
|
121
|
+
if (name)
|
|
122
|
+
out.push(name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
109
127
|
function parseLimit(label) {
|
|
110
128
|
if (!label)
|
|
111
129
|
return null;
|
|
@@ -181,12 +199,12 @@ export const newRecruitJsonAdapter = {
|
|
|
181
199
|
throw new Error("newrecruit-json: payload has no roster.forces array");
|
|
182
200
|
}
|
|
183
201
|
const forces = asArray(roster.forces);
|
|
184
|
-
|
|
202
|
+
const detachment_raw_names = [];
|
|
185
203
|
let battle_size_raw = null;
|
|
186
204
|
const units = [];
|
|
187
205
|
for (const force of forces) {
|
|
188
206
|
const top = childSelections(force);
|
|
189
|
-
|
|
207
|
+
detachment_raw_names.push(...configValues(top, "Detachment"));
|
|
190
208
|
battle_size_raw ??= configValue(top, "Battle Size");
|
|
191
209
|
for (const sel of top) {
|
|
192
210
|
if (isUnitSelection(sel))
|
|
@@ -211,7 +229,7 @@ export const newRecruitJsonAdapter = {
|
|
|
211
229
|
name: asString(payload.name) ?? asString(roster.name) ?? "Imported roster",
|
|
212
230
|
generated_by,
|
|
213
231
|
faction_raw_name: primaryFaction,
|
|
214
|
-
|
|
232
|
+
detachment_raw_names,
|
|
215
233
|
battle_size_raw,
|
|
216
234
|
declared_limit: parseLimit(battle_size_raw),
|
|
217
235
|
total_reported,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"newrecruit-json.js","sourceRoot":"","sources":["../../src/import/newrecruit-json.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit JSON adapter: lower a decoded NewRecruit roster export (a\n * BattleScribe-derived tree, same outer shape as ListForge) to a {@link ParsedRoster}.\n *\n * NewRecruit-specific signals used to detect the format:\n * - `generatedBy` reports the NewRecruit URL (\"https://newrecruit.eu\"), and/or\n * - `roster.xmlns` is set to the BattleScribe rosterSchema namespace.\n *\n * The primary faction surfaces in `forces[].catalogueName` (e.g.\n * \"Chaos - Chaos Knights\") — we take the segment after the final \" - \". Falls\n * back to the first `\"Faction: X\"` category if no catalogueName is present.\n *\n * The walk reads the same ALLOWLIST as the ListForge adapter — `name`,\n * `number`, `type`, `categories[].name`, `group`, `costs` point values, and\n * `catalogueName`. `rules[].description`, ability `profiles[].characteristics[].$text`,\n * and every other prose field are never touched, so the importer's output is\n * free of copyrighted prose by construction.\n *\n * Selection-tree shape (recursive `selections`) is identical to ListForge:\n * - Configuration nodes (`type: \"upgrade\"`) named \"Detachment\" / \"Battle Size\"\n * carry the chosen value as their first child selection.\n * - Unit nodes (`type: \"model\" | \"unit\"`) carry role categories, a points cost,\n * and — nested anywhere beneath them — their wargear (weapon-category\n * selections), enhancement (a selection whose `group` starts \"Enhancements\"),\n * the \"Warlord\" marker, and model sub-selections.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\n\nconst PTS_COST_NAME = \"pts\";\nconst FACTION_CATEGORY = /^Faction:\\s*(.+)$/;\nconst POINTS_LIMIT = /(\\d[\\d,]*)\\s*Point/i;\nconst ENHANCEMENT_GROUP_PREFIX = \"Enhancements\";\nconst CHARACTER_CATEGORIES = new Set([\"Character\", \"Epic Hero\"]);\nconst WEAPON_CATEGORY_SUFFIX = \" Weapon\"; // \"Ranged Weapon\", \"Melee Weapon\", \"Psychic Weapon\"\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_HOST_PREFIX = \"https://newrecruit\";\n\n// --- Minimal structural views of the parts of the payload we read. ----------\n\ninterface RawCategory {\n name?: unknown;\n}\ninterface RawCost {\n name?: unknown;\n value?: unknown;\n}\ninterface RawSelection {\n name?: unknown;\n type?: unknown;\n number?: unknown;\n group?: unknown;\n categories?: unknown;\n costs?: unknown;\n selections?: unknown;\n catalogueName?: unknown;\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction selectionName(sel: RawSelection): string {\n return asString(sel.name) ?? \"\";\n}\n\nfunction selectionType(sel: RawSelection): string {\n return asString(sel.type) ?? \"\";\n}\n\nfunction selectionCount(sel: RawSelection): number {\n return typeof sel.number === \"number\" && sel.number > 0 ? sel.number : 1;\n}\n\nfunction pointsOf(sel: RawSelection): number | null {\n for (const raw of asArray(sel.costs)) {\n const cost = raw as RawCost;\n if (asString(cost.name) === PTS_COST_NAME && typeof cost.value === \"number\") {\n return cost.value;\n }\n }\n return null;\n}\n\nfunction categoryNames(sel: RawSelection): string[] {\n return asArray(sel.categories)\n .map((c) => asString((c as RawCategory).name))\n .filter((n): n is string => n !== null);\n}\n\nfunction childSelections(sel: RawSelection): RawSelection[] {\n return asArray(sel.selections) as RawSelection[];\n}\n\nfunction walk(sel: RawSelection, visit: (s: RawSelection) => void): void {\n visit(sel);\n for (const child of childSelections(sel)) walk(child, visit);\n}\n\nfunction isUnitSelection(sel: RawSelection): boolean {\n const type = selectionType(sel);\n return type === \"model\" || type === \"unit\";\n}\n\nfunction isCharacter(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => CHARACTER_CATEGORIES.has(n));\n}\n\nfunction isWeaponSelection(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => n.endsWith(WEAPON_CATEGORY_SUFFIX));\n}\n\nfunction isEnhancementSelection(sel: RawSelection): boolean {\n const group = asString(sel.group);\n return group !== null && group.startsWith(ENHANCEMENT_GROUP_PREFIX);\n}\n\nfunction modelCount(unit: RawSelection): number {\n let total = 0;\n walk(unit, (s) => {\n if (selectionType(s) === \"model\") total += selectionCount(s);\n });\n return total > 0 ? total : selectionCount(unit);\n}\n\nfunction parseUnit(unit: RawSelection): ParsedUnit {\n const wargear: ParsedWargear[] = [];\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n let is_warlord = false;\n\n for (const node of childSelections(unit)) {\n walk(node, (s) => {\n if (isEnhancementSelection(s)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = selectionName(s);\n enhancement_points = pointsOf(s);\n }\n return;\n }\n if (selectionName(s) === \"Warlord\") {\n is_warlord = true;\n return;\n }\n if (isWeaponSelection(s)) {\n wargear.push({ raw_name: selectionName(s), count: selectionCount(s) });\n }\n });\n }\n\n return {\n raw_name: selectionName(unit),\n is_character: isCharacter(unit),\n model_count: modelCount(unit),\n points: pointsOf(unit),\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear,\n };\n}\n\nfunction configValue(\n selections: RawSelection[],\n configName: string,\n): string | null {\n const node = selections.find((s) => selectionName(s) === configName);\n if (!node) return null;\n const child = childSelections(node)[0];\n return child ? selectionName(child) : null;\n}\n\nfunction parseLimit(label: string | null): number | null {\n if (!label) return null;\n const match = POINTS_LIMIT.exec(label);\n if (!match) return null;\n return Number.parseInt(match[1].replace(/,/g, \"\"), 10);\n}\n\nfunction collectFactions(forces: RawSelection[]): string[] {\n const seen = new Set<string>();\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n for (const name of categoryNames(s)) {\n const match = FACTION_CATEGORY.exec(name);\n if (match) seen.add(match[1].trim());\n }\n });\n }\n }\n return [...seen];\n}\n\n/** Primary faction from a force's `catalogueName` (e.g. \"Chaos - Chaos Knights\"\n * → \"Chaos Knights\"). Returns null when no force has a catalogueName. */\nfunction primaryFactionFromCatalogue(forces: RawSelection[]): string | null {\n for (const force of forces) {\n const name = asString(force.catalogueName);\n if (!name) continue;\n const parts = name.split(\" - \");\n const last = parts[parts.length - 1]?.trim();\n if (last) return last;\n }\n return null;\n}\n\ninterface RawRoster {\n name?: unknown;\n costs?: unknown;\n forces?: unknown;\n xmlns?: unknown;\n generatedBy?: unknown;\n}\ninterface RawPayload {\n name?: unknown;\n generatedBy?: unknown;\n roster?: unknown;\n}\n\nfunction rosterOf(decoded: unknown): RawRoster | null {\n if (!decoded || typeof decoded !== \"object\") return null;\n const roster = (decoded as RawPayload).roster;\n if (!roster || typeof roster !== \"object\") return null;\n if (!Array.isArray((roster as RawRoster).forces)) return null;\n return roster as RawRoster;\n}\n\n/** Detect a NewRecruit payload: BattleScribe `rosterSchema` xmlns or a\n * `generatedBy` URL pointing at newrecruit.eu. */\nfunction hasNewRecruitSignature(decoded: unknown, roster: RawRoster): boolean {\n const payload = decoded as RawPayload;\n const xmlns = asString(roster.xmlns);\n if (xmlns === NEWRECRUIT_XMLNS) return true;\n const genBy =\n asString(payload.generatedBy) ?? asString(roster.generatedBy);\n if (genBy && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX)) {\n return true;\n }\n return false;\n}\n\nexport const newRecruitJsonAdapter: FormatAdapter = {\n id: \"newrecruit-json\",\n\n matches(decoded: unknown): boolean {\n const roster = rosterOf(decoded);\n if (!roster) return false;\n return hasNewRecruitSignature(decoded, roster);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const payload = decoded as RawPayload;\n const roster = rosterOf(decoded);\n if (!roster) {\n throw new Error(\"newrecruit-json: payload has no roster.forces array\");\n }\n\n const forces = asArray(roster.forces) as RawSelection[];\n\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n for (const force of forces) {\n const top = childSelections(force);\n detachment_raw_name ??= configValue(top, \"Detachment\");\n battle_size_raw ??= configValue(top, \"Battle Size\");\n for (const sel of top) {\n if (isUnitSelection(sel)) units.push(parseUnit(sel));\n }\n }\n\n const factions = collectFactions(forces);\n const primaryFaction = primaryFactionFromCatalogue(forces) ?? factions[0] ?? null;\n const total_reported = pointsOf(roster as RawSelection);\n\n let total_computed = 0;\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n const pts = pointsOf(s);\n if (pts) total_computed += pts;\n });\n }\n }\n\n const generated_by =\n asString(payload.generatedBy) ?? asString(roster.generatedBy);\n\n return {\n name: asString(payload.name) ?? asString(roster.name) ?? \"Imported roster\",\n generated_by,\n faction_raw_name: primaryFaction,\n detachment_raw_name,\n battle_size_raw,\n declared_limit: parseLimit(battle_size_raw),\n total_reported,\n total_computed,\n units,\n multi_force: factions.length > 1,\n };\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"newrecruit-json.js","sourceRoot":"","sources":["../../src/import/newrecruit-json.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit JSON adapter: lower a decoded NewRecruit roster export (a\n * BattleScribe-derived tree, same outer shape as ListForge) to a {@link ParsedRoster}.\n *\n * NewRecruit-specific signals used to detect the format:\n * - `generatedBy` reports the NewRecruit URL (\"https://newrecruit.eu\"), and/or\n * - `roster.xmlns` is set to the BattleScribe rosterSchema namespace.\n *\n * The primary faction surfaces in `forces[].catalogueName` (e.g.\n * \"Chaos - Chaos Knights\") — we take the segment after the final \" - \". Falls\n * back to the first `\"Faction: X\"` category if no catalogueName is present.\n *\n * The walk reads the same ALLOWLIST as the ListForge adapter — `name`,\n * `number`, `type`, `categories[].name`, `group`, `costs` point values, and\n * `catalogueName`. `rules[].description`, ability `profiles[].characteristics[].$text`,\n * and every other prose field are never touched, so the importer's output is\n * free of copyrighted prose by construction.\n *\n * Selection-tree shape (recursive `selections`) is identical to ListForge:\n * - Configuration nodes (`type: \"upgrade\"`) named \"Detachment\" / \"Battle Size\"\n * carry the chosen value as their first child selection.\n * - Unit nodes (`type: \"model\" | \"unit\"`) carry role categories, a points cost,\n * and — nested anywhere beneath them — their wargear (weapon-category\n * selections), enhancement (a selection whose `group` starts \"Enhancements\"),\n * the \"Warlord\" marker, and model sub-selections.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\n\nconst PTS_COST_NAME = \"pts\";\nconst FACTION_CATEGORY = /^Faction:\\s*(.+)$/;\nconst POINTS_LIMIT = /(\\d[\\d,]*)\\s*Point/i;\nconst ENHANCEMENT_GROUP_PREFIX = \"Enhancements\";\nconst CHARACTER_CATEGORIES = new Set([\"Character\", \"Epic Hero\"]);\nconst WEAPON_CATEGORY_SUFFIX = \" Weapon\"; // \"Ranged Weapon\", \"Melee Weapon\", \"Psychic Weapon\"\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_HOST_PREFIX = \"https://newrecruit\";\n\n// --- Minimal structural views of the parts of the payload we read. ----------\n\ninterface RawCategory {\n name?: unknown;\n}\ninterface RawCost {\n name?: unknown;\n value?: unknown;\n}\ninterface RawSelection {\n name?: unknown;\n type?: unknown;\n number?: unknown;\n group?: unknown;\n categories?: unknown;\n costs?: unknown;\n selections?: unknown;\n catalogueName?: unknown;\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction selectionName(sel: RawSelection): string {\n return asString(sel.name) ?? \"\";\n}\n\nfunction selectionType(sel: RawSelection): string {\n return asString(sel.type) ?? \"\";\n}\n\nfunction selectionCount(sel: RawSelection): number {\n return typeof sel.number === \"number\" && sel.number > 0 ? sel.number : 1;\n}\n\nfunction pointsOf(sel: RawSelection): number | null {\n for (const raw of asArray(sel.costs)) {\n const cost = raw as RawCost;\n if (asString(cost.name) === PTS_COST_NAME && typeof cost.value === \"number\") {\n return cost.value;\n }\n }\n return null;\n}\n\nfunction categoryNames(sel: RawSelection): string[] {\n return asArray(sel.categories)\n .map((c) => asString((c as RawCategory).name))\n .filter((n): n is string => n !== null);\n}\n\nfunction childSelections(sel: RawSelection): RawSelection[] {\n return asArray(sel.selections) as RawSelection[];\n}\n\nfunction walk(sel: RawSelection, visit: (s: RawSelection) => void): void {\n visit(sel);\n for (const child of childSelections(sel)) walk(child, visit);\n}\n\nfunction isUnitSelection(sel: RawSelection): boolean {\n const type = selectionType(sel);\n return type === \"model\" || type === \"unit\";\n}\n\nfunction isCharacter(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => CHARACTER_CATEGORIES.has(n));\n}\n\nfunction isWeaponSelection(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => n.endsWith(WEAPON_CATEGORY_SUFFIX));\n}\n\nfunction isEnhancementSelection(sel: RawSelection): boolean {\n const group = asString(sel.group);\n return group !== null && group.startsWith(ENHANCEMENT_GROUP_PREFIX);\n}\n\nfunction modelCount(unit: RawSelection): number {\n let total = 0;\n walk(unit, (s) => {\n if (selectionType(s) === \"model\") total += selectionCount(s);\n });\n return total > 0 ? total : selectionCount(unit);\n}\n\nfunction parseUnit(unit: RawSelection): ParsedUnit {\n const wargear: ParsedWargear[] = [];\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n let is_warlord = false;\n\n for (const node of childSelections(unit)) {\n walk(node, (s) => {\n if (isEnhancementSelection(s)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = selectionName(s);\n enhancement_points = pointsOf(s);\n }\n return;\n }\n if (selectionName(s) === \"Warlord\") {\n is_warlord = true;\n return;\n }\n if (isWeaponSelection(s)) {\n wargear.push({ raw_name: selectionName(s), count: selectionCount(s) });\n }\n });\n }\n\n return {\n raw_name: selectionName(unit),\n is_character: isCharacter(unit),\n model_count: modelCount(unit),\n points: pointsOf(unit),\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear,\n };\n}\n\nfunction configValue(\n selections: RawSelection[],\n configName: string,\n): string | null {\n const node = selections.find((s) => selectionName(s) === configName);\n if (!node) return null;\n const child = childSelections(node)[0];\n return child ? selectionName(child) : null;\n}\n\n/**\n * Every value selected under a named config — across repeated config nodes\n * (one `Detachment` block per detachment) and multiple children of a single\n * block. Source order is preserved. Used for multi-detachment 11e lists.\n */\nfunction configValues(selections: RawSelection[], configName: string): string[] {\n const out: string[] = [];\n for (const node of selections) {\n if (selectionName(node) !== configName) continue;\n for (const child of childSelections(node)) {\n const name = selectionName(child);\n if (name) out.push(name);\n }\n }\n return out;\n}\n\nfunction parseLimit(label: string | null): number | null {\n if (!label) return null;\n const match = POINTS_LIMIT.exec(label);\n if (!match) return null;\n return Number.parseInt(match[1].replace(/,/g, \"\"), 10);\n}\n\nfunction collectFactions(forces: RawSelection[]): string[] {\n const seen = new Set<string>();\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n for (const name of categoryNames(s)) {\n const match = FACTION_CATEGORY.exec(name);\n if (match) seen.add(match[1].trim());\n }\n });\n }\n }\n return [...seen];\n}\n\n/** Primary faction from a force's `catalogueName` (e.g. \"Chaos - Chaos Knights\"\n * → \"Chaos Knights\"). Returns null when no force has a catalogueName. */\nfunction primaryFactionFromCatalogue(forces: RawSelection[]): string | null {\n for (const force of forces) {\n const name = asString(force.catalogueName);\n if (!name) continue;\n const parts = name.split(\" - \");\n const last = parts[parts.length - 1]?.trim();\n if (last) return last;\n }\n return null;\n}\n\ninterface RawRoster {\n name?: unknown;\n costs?: unknown;\n forces?: unknown;\n xmlns?: unknown;\n generatedBy?: unknown;\n}\ninterface RawPayload {\n name?: unknown;\n generatedBy?: unknown;\n roster?: unknown;\n}\n\nfunction rosterOf(decoded: unknown): RawRoster | null {\n if (!decoded || typeof decoded !== \"object\") return null;\n const roster = (decoded as RawPayload).roster;\n if (!roster || typeof roster !== \"object\") return null;\n if (!Array.isArray((roster as RawRoster).forces)) return null;\n return roster as RawRoster;\n}\n\n/** Detect a NewRecruit payload: BattleScribe `rosterSchema` xmlns or a\n * `generatedBy` URL pointing at newrecruit.eu. */\nfunction hasNewRecruitSignature(decoded: unknown, roster: RawRoster): boolean {\n const payload = decoded as RawPayload;\n const xmlns = asString(roster.xmlns);\n if (xmlns === NEWRECRUIT_XMLNS) return true;\n const genBy =\n asString(payload.generatedBy) ?? asString(roster.generatedBy);\n if (genBy && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX)) {\n return true;\n }\n return false;\n}\n\nexport const newRecruitJsonAdapter: FormatAdapter = {\n id: \"newrecruit-json\",\n\n matches(decoded: unknown): boolean {\n const roster = rosterOf(decoded);\n if (!roster) return false;\n return hasNewRecruitSignature(decoded, roster);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const payload = decoded as RawPayload;\n const roster = rosterOf(decoded);\n if (!roster) {\n throw new Error(\"newrecruit-json: payload has no roster.forces array\");\n }\n\n const forces = asArray(roster.forces) as RawSelection[];\n\n const detachment_raw_names: string[] = [];\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n for (const force of forces) {\n const top = childSelections(force);\n detachment_raw_names.push(...configValues(top, \"Detachment\"));\n battle_size_raw ??= configValue(top, \"Battle Size\");\n for (const sel of top) {\n if (isUnitSelection(sel)) units.push(parseUnit(sel));\n }\n }\n\n const factions = collectFactions(forces);\n const primaryFaction = primaryFactionFromCatalogue(forces) ?? factions[0] ?? null;\n const total_reported = pointsOf(roster as RawSelection);\n\n let total_computed = 0;\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n const pts = pointsOf(s);\n if (pts) total_computed += pts;\n });\n }\n }\n\n const generated_by =\n asString(payload.generatedBy) ?? asString(roster.generatedBy);\n\n return {\n name: asString(payload.name) ?? asString(roster.name) ?? \"Imported roster\",\n generated_by,\n faction_raw_name: primaryFaction,\n detachment_raw_names,\n battle_size_raw,\n declared_limit: parseLimit(battle_size_raw),\n total_reported,\n total_computed,\n units,\n multi_force: factions.length > 1,\n };\n },\n};\n"]}
|
|
@@ -200,7 +200,7 @@ export const newRecruitSimpleAdapter = {
|
|
|
200
200
|
name,
|
|
201
201
|
generated_by: null,
|
|
202
202
|
faction_raw_name,
|
|
203
|
-
detachment_raw_name,
|
|
203
|
+
detachment_raw_names: detachment_raw_name ? [detachment_raw_name] : [],
|
|
204
204
|
battle_size_raw,
|
|
205
205
|
declared_limit,
|
|
206
206
|
total_reported,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"newrecruit-simple.js","sourceRoot":"","sources":["../../src/import/newrecruit-simple.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"simple\" markdown-ish text adapter.\n *\n * Shape:\n * ```\n * <breadcrumb> - <faction> - <list name> - [N pts]\n *\n * # ++ Army Roster ++ [N pts]\n * ## Configuration\n * Battle Size: <Label>\n * Detachment: <Name>\n * Show/Hide Options: ...\n *\n * ## <Section> [N pts]\n * <Unit> [N pts]: <wargear>\n * <Unit> [N pts]:\n * • <count>x <ModelType>[ [N pts]]: <wargear>\n * ```\n *\n * Enhancements are inlined in the wargear list as `<Name> [N pts]` — the only\n * wargear token wearing a `[…]` pts suffix. `Warlord` and the detachment\n * \"<X> Character\" keyword are also stripped from the list and set as flags.\n * Per-model-type breakdowns under `•` lines are collapsed onto the parent unit.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport { classifyWargearList, splitWargearList } from \"./newrecruit-text.js\";\n\n// Point brackets may carry comma-separated faction resources after the pts\n// figure (e.g. `[4485pts, 29Cabal Points]`); the tail is recognized and\n// discarded — only the pts figure is consumed.\nconst FIRST_LINE = /^(.+)\\s-\\s\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\]\\s*$/i;\nconst ROSTER_HEADER =\n /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\]\\s*$/i;\nconst SECTION_HEADER = /^##\\s*(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\])?\\s*$/;\nconst UNIT_LINE = /^(.+?)\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\](?:\\s*:\\s*(.*))?$/i;\nconst BULLET =\n /^\\s*•\\s*(\\d+)x\\s+(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\])?(?:\\s*:\\s*(.*))?\\s*$/u;\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n enhancement_pts: number;\n displayed_pts: number | null;\n model_count: number;\n /** Aggregated wargear, keyed by name. Counts sum across `• Nx ModelType` breakdowns. */\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number | null): UnitBuilder {\n return {\n raw_name: name,\n is_character: false,\n is_warlord: false,\n enhancement_raw_name: null,\n enhancement_pts: 0,\n displayed_pts,\n model_count: 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyTokens(unit: UnitBuilder, tokensCsv: string, multiplier = 1): void {\n const tokens = splitWargearList(tokensCsv);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n if (cls.enhancement_raw_name && unit.enhancement_raw_name === null) {\n unit.enhancement_raw_name = cls.enhancement_raw_name;\n unit.enhancement_pts = cls.enhancement_points ?? 0;\n }\n const scaled = cls.wargear.map((w) => ({\n raw_name: w.raw_name,\n count: w.count * multiplier,\n }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const points =\n unit.displayed_pts === null ? null : unit.displayed_pts - unit.enhancement_pts;\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear: [...unit.wargear].map(([raw_name, count]) => ({ raw_name, count })),\n };\n}\n\nfunction parseFirstLine(line: string): { name: string; faction: string | null; declared_limit: number | null } | null {\n const m = FIRST_LINE.exec(line);\n if (!m) return null;\n const declared_limit = Number.parseInt(m[2], 10);\n const parts = m[1].split(\" - \").map((s) => s.trim()).filter((s) => s.length > 0);\n if (parts.length === 0) return null;\n const list_name = parts[parts.length - 1];\n const faction = parts.length >= 2 ? parts[parts.length - 2] : null;\n return { name: list_name, faction, declared_limit };\n}\n\ntype Section = \"preamble\" | \"configuration\" | \"units\";\n\nexport const newRecruitSimpleAdapter: FormatAdapter = {\n id: \"newrecruit-simple\",\n\n matches(decoded: unknown): boolean {\n if (typeof decoded !== \"string\") return false;\n const lines = decoded.split(/\\r?\\n/);\n const firstNonBlank = lines.find((l) => l.trim().length > 0);\n if (!firstNonBlank) return false;\n if (!FIRST_LINE.test(firstNonBlank)) return false;\n // Some exports omit the `# ++ Army Roster ++` line and open straight with\n // a `## Section` heading — accept either marker.\n return (\n /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+/m.test(decoded) ||\n /^##\\s+/m.test(decoded)\n );\n },\n\n parse(decoded: unknown): ParsedRoster {\n if (typeof decoded !== \"string\") {\n throw new Error(\"newrecruit-simple: input is not a string\");\n }\n const lines = decoded.split(/\\r?\\n/);\n\n let name = \"Imported roster\";\n let faction_raw_name: string | null = null;\n let declared_limit: number | null = null;\n let total_reported: number | null = null;\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n let current: UnitBuilder | null = null;\n let multi_force = false;\n let section: Section = \"preamble\";\n const enhancementPts: number[] = [];\n\n const finalize = (): void => {\n if (current) {\n enhancementPts.push(current.enhancement_pts);\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (let i = 0; i < lines.length; i += 1) {\n const raw = lines[i];\n const line = raw.trim();\n if (!line) continue;\n\n // First non-blank line carries `<breadcrumb> - <faction> - <list> - [N pts]`.\n if (section === \"preamble\" && name === \"Imported roster\") {\n const first = parseFirstLine(line);\n if (first) {\n name = first.name;\n faction_raw_name = first.faction;\n declared_limit = first.declared_limit;\n continue;\n }\n }\n\n const rosterMatch = ROSTER_HEADER.exec(line);\n if (rosterMatch) {\n total_reported = Number.parseInt(rosterMatch[1], 10);\n continue;\n }\n\n const sectionMatch = SECTION_HEADER.exec(line);\n if (sectionMatch) {\n finalize();\n const heading = sectionMatch[1].trim().toLowerCase();\n if (heading === \"configuration\") {\n section = \"configuration\";\n } else {\n section = \"units\";\n if (heading.includes(\"allied\")) multi_force = true;\n }\n continue;\n }\n\n if (section === \"configuration\") {\n // Some exports list units directly after Configuration with no units\n // section heading; a `Name [N pts]` line ends the configuration block.\n if (UNIT_LINE.test(line)) {\n section = \"units\";\n } else {\n const idx = line.indexOf(\":\");\n if (idx > 0) {\n const key = line.slice(0, idx).trim().toLowerCase();\n const value = line.slice(idx + 1).trim();\n if (key === \"battle size\") battle_size_raw = value;\n else if (key === \"detachment\") detachment_raw_name = value;\n }\n continue;\n }\n }\n\n // Unit section. A bullet line extends the *current* unit.\n const bulletMatch = BULLET.exec(raw);\n if (bulletMatch && current) {\n const count = Number.parseInt(bulletMatch[1], 10);\n // Bullets may add to the unit's model count beyond the implicit 1 we\n // set when we created it from the unit header.\n if (current.wargear.size === 0 && current.model_count === 1) {\n // First bullet: replace the implicit single-model assumption.\n current.model_count = count;\n } else {\n current.model_count += count;\n }\n if (bulletMatch[4]) applyTokens(current, bulletMatch[4], count);\n continue;\n }\n\n const unitMatch = UNIT_LINE.exec(line);\n if (unitMatch) {\n finalize();\n const unitName = unitMatch[1].trim();\n const pts = Number.parseInt(unitMatch[2], 10);\n current = newUnit(unitName, pts);\n const inlineWargear = unitMatch[3]?.trim() ?? \"\";\n if (inlineWargear.length > 0) {\n applyTokens(current, inlineWargear, 1);\n }\n // Leave model_count at the default 1. If `•` bullet lines follow, the\n // bullet handler resets model_count to the (summed) bullet counts.\n continue;\n }\n }\n finalize();\n\n let total_computed = 0;\n for (let i = 0; i < units.length; i += 1) {\n total_computed += units[i].points ?? 0;\n total_computed += enhancementPts[i] ?? 0;\n }\n\n return {\n name,\n generated_by: null,\n faction_raw_name,\n detachment_raw_name,\n battle_size_raw,\n declared_limit,\n total_reported,\n total_computed,\n units,\n multi_force,\n };\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"newrecruit-simple.js","sourceRoot":"","sources":["../../src/import/newrecruit-simple.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"simple\" markdown-ish text adapter.\n *\n * Shape:\n * ```\n * <breadcrumb> - <faction> - <list name> - [N pts]\n *\n * # ++ Army Roster ++ [N pts]\n * ## Configuration\n * Battle Size: <Label>\n * Detachment: <Name>\n * Show/Hide Options: ...\n *\n * ## <Section> [N pts]\n * <Unit> [N pts]: <wargear>\n * <Unit> [N pts]:\n * • <count>x <ModelType>[ [N pts]]: <wargear>\n * ```\n *\n * Enhancements are inlined in the wargear list as `<Name> [N pts]` — the only\n * wargear token wearing a `[…]` pts suffix. `Warlord` and the detachment\n * \"<X> Character\" keyword are also stripped from the list and set as flags.\n * Per-model-type breakdowns under `•` lines are collapsed onto the parent unit.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport { classifyWargearList, splitWargearList } from \"./newrecruit-text.js\";\n\n// Point brackets may carry comma-separated faction resources after the pts\n// figure (e.g. `[4485pts, 29Cabal Points]`); the tail is recognized and\n// discarded — only the pts figure is consumed.\nconst FIRST_LINE = /^(.+)\\s-\\s\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\]\\s*$/i;\nconst ROSTER_HEADER =\n /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\]\\s*$/i;\nconst SECTION_HEADER = /^##\\s*(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\])?\\s*$/;\nconst UNIT_LINE = /^(.+?)\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\](?:\\s*:\\s*(.*))?$/i;\nconst BULLET =\n /^\\s*•\\s*(\\d+)x\\s+(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\])?(?:\\s*:\\s*(.*))?\\s*$/u;\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n enhancement_pts: number;\n displayed_pts: number | null;\n model_count: number;\n /** Aggregated wargear, keyed by name. Counts sum across `• Nx ModelType` breakdowns. */\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number | null): UnitBuilder {\n return {\n raw_name: name,\n is_character: false,\n is_warlord: false,\n enhancement_raw_name: null,\n enhancement_pts: 0,\n displayed_pts,\n model_count: 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyTokens(unit: UnitBuilder, tokensCsv: string, multiplier = 1): void {\n const tokens = splitWargearList(tokensCsv);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n if (cls.enhancement_raw_name && unit.enhancement_raw_name === null) {\n unit.enhancement_raw_name = cls.enhancement_raw_name;\n unit.enhancement_pts = cls.enhancement_points ?? 0;\n }\n const scaled = cls.wargear.map((w) => ({\n raw_name: w.raw_name,\n count: w.count * multiplier,\n }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const points =\n unit.displayed_pts === null ? null : unit.displayed_pts - unit.enhancement_pts;\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear: [...unit.wargear].map(([raw_name, count]) => ({ raw_name, count })),\n };\n}\n\nfunction parseFirstLine(line: string): { name: string; faction: string | null; declared_limit: number | null } | null {\n const m = FIRST_LINE.exec(line);\n if (!m) return null;\n const declared_limit = Number.parseInt(m[2], 10);\n const parts = m[1].split(\" - \").map((s) => s.trim()).filter((s) => s.length > 0);\n if (parts.length === 0) return null;\n const list_name = parts[parts.length - 1];\n const faction = parts.length >= 2 ? parts[parts.length - 2] : null;\n return { name: list_name, faction, declared_limit };\n}\n\ntype Section = \"preamble\" | \"configuration\" | \"units\";\n\nexport const newRecruitSimpleAdapter: FormatAdapter = {\n id: \"newrecruit-simple\",\n\n matches(decoded: unknown): boolean {\n if (typeof decoded !== \"string\") return false;\n const lines = decoded.split(/\\r?\\n/);\n const firstNonBlank = lines.find((l) => l.trim().length > 0);\n if (!firstNonBlank) return false;\n if (!FIRST_LINE.test(firstNonBlank)) return false;\n // Some exports omit the `# ++ Army Roster ++` line and open straight with\n // a `## Section` heading — accept either marker.\n return (\n /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+/m.test(decoded) ||\n /^##\\s+/m.test(decoded)\n );\n },\n\n parse(decoded: unknown): ParsedRoster {\n if (typeof decoded !== \"string\") {\n throw new Error(\"newrecruit-simple: input is not a string\");\n }\n const lines = decoded.split(/\\r?\\n/);\n\n let name = \"Imported roster\";\n let faction_raw_name: string | null = null;\n let declared_limit: number | null = null;\n let total_reported: number | null = null;\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n let current: UnitBuilder | null = null;\n let multi_force = false;\n let section: Section = \"preamble\";\n const enhancementPts: number[] = [];\n\n const finalize = (): void => {\n if (current) {\n enhancementPts.push(current.enhancement_pts);\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (let i = 0; i < lines.length; i += 1) {\n const raw = lines[i];\n const line = raw.trim();\n if (!line) continue;\n\n // First non-blank line carries `<breadcrumb> - <faction> - <list> - [N pts]`.\n if (section === \"preamble\" && name === \"Imported roster\") {\n const first = parseFirstLine(line);\n if (first) {\n name = first.name;\n faction_raw_name = first.faction;\n declared_limit = first.declared_limit;\n continue;\n }\n }\n\n const rosterMatch = ROSTER_HEADER.exec(line);\n if (rosterMatch) {\n total_reported = Number.parseInt(rosterMatch[1], 10);\n continue;\n }\n\n const sectionMatch = SECTION_HEADER.exec(line);\n if (sectionMatch) {\n finalize();\n const heading = sectionMatch[1].trim().toLowerCase();\n if (heading === \"configuration\") {\n section = \"configuration\";\n } else {\n section = \"units\";\n if (heading.includes(\"allied\")) multi_force = true;\n }\n continue;\n }\n\n if (section === \"configuration\") {\n // Some exports list units directly after Configuration with no units\n // section heading; a `Name [N pts]` line ends the configuration block.\n if (UNIT_LINE.test(line)) {\n section = \"units\";\n } else {\n const idx = line.indexOf(\":\");\n if (idx > 0) {\n const key = line.slice(0, idx).trim().toLowerCase();\n const value = line.slice(idx + 1).trim();\n if (key === \"battle size\") battle_size_raw = value;\n else if (key === \"detachment\") detachment_raw_name = value;\n }\n continue;\n }\n }\n\n // Unit section. A bullet line extends the *current* unit.\n const bulletMatch = BULLET.exec(raw);\n if (bulletMatch && current) {\n const count = Number.parseInt(bulletMatch[1], 10);\n // Bullets may add to the unit's model count beyond the implicit 1 we\n // set when we created it from the unit header.\n if (current.wargear.size === 0 && current.model_count === 1) {\n // First bullet: replace the implicit single-model assumption.\n current.model_count = count;\n } else {\n current.model_count += count;\n }\n if (bulletMatch[4]) applyTokens(current, bulletMatch[4], count);\n continue;\n }\n\n const unitMatch = UNIT_LINE.exec(line);\n if (unitMatch) {\n finalize();\n const unitName = unitMatch[1].trim();\n const pts = Number.parseInt(unitMatch[2], 10);\n current = newUnit(unitName, pts);\n const inlineWargear = unitMatch[3]?.trim() ?? \"\";\n if (inlineWargear.length > 0) {\n applyTokens(current, inlineWargear, 1);\n }\n // Leave model_count at the default 1. If `•` bullet lines follow, the\n // bullet handler resets model_count to the (summed) bullet counts.\n continue;\n }\n }\n finalize();\n\n let total_computed = 0;\n for (let i = 0; i < units.length; i += 1) {\n total_computed += units[i].points ?? 0;\n total_computed += enhancementPts[i] ?? 0;\n }\n\n return {\n name,\n generated_by: null,\n faction_raw_name,\n detachment_raw_names: detachment_raw_name ? [detachment_raw_name] : [],\n battle_size_raw,\n declared_limit,\n total_reported,\n total_computed,\n units,\n multi_force,\n };\n },\n};\n"]}
|
|
@@ -314,7 +314,7 @@ function parseWith(text, format) {
|
|
|
314
314
|
name: header.name,
|
|
315
315
|
generated_by: null,
|
|
316
316
|
faction_raw_name: header.faction_raw_name,
|
|
317
|
-
|
|
317
|
+
detachment_raw_names: header.detachment_raw_name ? [header.detachment_raw_name] : [],
|
|
318
318
|
battle_size_raw: header.battle_size_raw,
|
|
319
319
|
declared_limit: header.declared_limit,
|
|
320
320
|
total_reported: header.total_reported,
|