+14
-3
@@ -1358,8 +1358,15 @@ const makeKeywordsBold = (text) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace words wrapped in ^^ ^^ with <strong> tags
|
// replace words wrapped in ^^ ^^ with <strong> tags. ^^ is the exporter's
|
||||||
newValue = newValue.replace(/\^\^([^\^]+)\^\^/g, "<strong>$1</strong>");
|
// datasheet-keyword marker, and the printed cards set keywords in bold
|
||||||
|
// uppercase - the same treatment boldKeywords gives the ones it knows. The
|
||||||
|
// case comes from CSS rather than toUpperCase() because the replacements above
|
||||||
|
// have already put HTML in here, and var(--primary-color) is case-sensitive.
|
||||||
|
newValue = newValue.replace(
|
||||||
|
/\^\^([^\^]+)\^\^/g,
|
||||||
|
'<strong style="text-transform: uppercase;">$1</strong>',
|
||||||
|
);
|
||||||
|
|
||||||
// replace words wrapped in ** ** with <strong> tags
|
// replace words wrapped in ** ** with <strong> tags
|
||||||
newValue = newValue.replace(/\*\*([^\*]+)\*\*/g, "<strong>$1</strong>");
|
newValue = newValue.replace(/\*\*([^\*]+)\*\*/g, "<strong>$1</strong>");
|
||||||
@@ -1484,7 +1491,11 @@ const OtherAbilities = ({ abilities }) => {
|
|||||||
paddingBottom: 4,
|
paddingBottom: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontWeight: 700 }}>{name}:</span> {value}
|
<span style={{ fontWeight: 700 }}>{name}:</span>{" "}
|
||||||
|
<span
|
||||||
|
className="whitespace-pre-line"
|
||||||
|
dangerouslySetInnerHTML={{ __html: makeKeywordsBold(value) }}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -25,14 +25,7 @@ export const defaultConfig = {
|
|||||||
|
|
||||||
// Abilities to split into per-option profiles, matched against the
|
// Abilities to split into per-option profiles, matched against the
|
||||||
// profile's ``name`` attribute.
|
// profile's ``name`` attribute.
|
||||||
abilitiesToConvert: ["Canticles of the Omnissiah", "Battle Protocols"],
|
abilitiesToConvert: ["Canticles of the Omnissiah", "Battle Protocols", "Icon of War"],
|
||||||
|
|
||||||
// Title of the generated profile group, per ability. Anything not listed
|
|
||||||
// here uses the ability's own name, which is what the datasheets do.
|
|
||||||
groupTitleOverrides: {
|
|
||||||
"Canticles of the Omnissiah": "CANTICLES OF THE OMNISSIAH",
|
|
||||||
"Battle Protocols": "BATTLE PROTOCOLS",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Which transforms run by default when a roster is loaded.
|
// Which transforms run by default when a roster is loaded.
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
// * the original ``Abilities`` profile is kept, trimmed to the intro
|
// * the original ``Abilities`` profile is kept, trimmed to the intro
|
||||||
// paragraph (the "select one of the following" rule), and
|
// paragraph (the "select one of the following" rule), and
|
||||||
// * each option becomes its own profile under a new profile type named after
|
// * each option becomes its own profile under a new profile type named after
|
||||||
// the ability, so the renderer groups them into a single titled table.
|
// the ability, in capitals, so the renderer groups them into a single table
|
||||||
|
// titled the way the datasheets title it.
|
||||||
//
|
//
|
||||||
// Running it twice is a no-op: a converted ability has only an intro paragraph
|
// Running it twice is a no-op: a converted ability has only an intro paragraph
|
||||||
// left, so there is nothing further to split.
|
// left, so there is nothing further to split.
|
||||||
@@ -76,16 +77,23 @@ const cleanOptionName = (name) =>
|
|||||||
* over from mis-nested pairs goes too - a stray ``**`` is punctuation the
|
* over from mis-nested pairs goes too - a stray ``**`` is punctuation the
|
||||||
* reader never wanted to see either way.
|
* reader never wanted to see either way.
|
||||||
*
|
*
|
||||||
* The generated group renders through the renderer's generic ability table,
|
* Only the option *name* goes through this. It ends up as a profile ``name``
|
||||||
* which prints its text verbatim, so the markers would otherwise show up as
|
* attribute, which the renderer prints as plain text, so a marker left in there
|
||||||
* literal punctuation. The intro profile keeps its original typeName, renders
|
* would show up as literal punctuation. The option text keeps its markers:
|
||||||
* through the normal path, and is therefore left alone.
|
* ``makeKeywordsBold`` in 10th/Roster.jsx formats them into the bold uppercase
|
||||||
|
* keywords and bold emphasis the printed card uses, which is more than
|
||||||
|
* upper-casing here could manage. The intro profile keeps its original
|
||||||
|
* typeName, renders through the normal path, and was never touched either way.
|
||||||
|
*
|
||||||
|
* The Python original also stripped the option text, so this is the one place
|
||||||
|
* the port deliberately differs from it; transforms.test.js resolves markers on
|
||||||
|
* both sides when it compares against the fixtures.
|
||||||
*
|
*
|
||||||
* Unlike the Python original this needs no entity bookkeeping: the DOM hands us
|
* Unlike the Python original this needs no entity bookkeeping: the DOM hands us
|
||||||
* resolved text, so there is no ``"`` here that upper-casing could turn
|
* resolved text, so there is no ``"`` here that upper-casing could turn
|
||||||
* into an entity no parser would recognise.
|
* into an entity no parser would recognise.
|
||||||
*/
|
*/
|
||||||
const stripMarkup = (text) =>
|
export const stripMarkup = (text) =>
|
||||||
text
|
text
|
||||||
.replace(KEYWORD_RE, (_, keyword) =>
|
.replace(KEYWORD_RE, (_, keyword) =>
|
||||||
keyword.replaceAll("*", "").toUpperCase(),
|
keyword.replaceAll("*", "").toUpperCase(),
|
||||||
@@ -201,7 +209,6 @@ function buildProfile(
|
|||||||
*/
|
*/
|
||||||
export function convertChoiceAbilities(doc, config) {
|
export function convertChoiceAbilities(doc, config) {
|
||||||
const wanted = new Set(config.abilitiesToConvert);
|
const wanted = new Set(config.abilitiesToConvert);
|
||||||
const overrides = config.groupTitleOverrides ?? {};
|
|
||||||
const root = doc.documentElement;
|
const root = doc.documentElement;
|
||||||
if (!root) return [];
|
if (!root) return [];
|
||||||
|
|
||||||
@@ -234,7 +241,10 @@ export function convertChoiceAbilities(doc, config) {
|
|||||||
);
|
);
|
||||||
if (options.length === 0) continue; // Already converted, or a plain ability.
|
if (options.length === 0) continue; // Already converted, or a plain ability.
|
||||||
|
|
||||||
const groupTitle = overrides[ability] ?? ability;
|
// The group's title becomes the generated profileType's typeName, which the
|
||||||
|
// renderer prints verbatim as the table heading - and the datasheets set
|
||||||
|
// that heading in capitals.
|
||||||
|
const groupTitle = ability.toUpperCase();
|
||||||
const typeId = makeId("profileType", groupTitle);
|
const typeId = makeId("profileType", groupTitle);
|
||||||
const charTypeId = makeId(
|
const charTypeId = makeId(
|
||||||
"characteristicType",
|
"characteristicType",
|
||||||
@@ -265,7 +275,7 @@ export function convertChoiceAbilities(doc, config) {
|
|||||||
typeId,
|
typeId,
|
||||||
typeName: groupTitle,
|
typeName: groupTitle,
|
||||||
charTypeId,
|
charTypeId,
|
||||||
description: stripMarkup(option.text),
|
description: option.text,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,10 +100,12 @@ describe("splitting choice abilities", () => {
|
|||||||
expect(group).toBeTruthy();
|
expect(group).toBeTruthy();
|
||||||
expect(group.size).toBeGreaterThan(1);
|
expect(group.size).toBeGreaterThan(1);
|
||||||
|
|
||||||
// Options print as plain text: GW's ^^keyword^^ / **emphasis** markers
|
// The option text keeps GW's ^^keyword^^ / **emphasis** markers for the
|
||||||
// are resolved, not passed through as punctuation.
|
// renderer to format; only the row titles are plain text, since those
|
||||||
for (const text of group.values()) {
|
// render as-is.
|
||||||
expect(JSON.stringify(text)).not.toMatch(/\^\^|\*\*/);
|
expect([...group.values()].join("\n")).toMatch(/\^\^/);
|
||||||
|
for (const name of group.keys()) {
|
||||||
|
expect(name).not.toMatch(/\^\^|\*\*/);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { defaultConfig } from "./config.js";
|
|||||||
import {
|
import {
|
||||||
convertChoiceAbilities,
|
convertChoiceAbilities,
|
||||||
splitDescription,
|
splitDescription,
|
||||||
|
stripMarkup,
|
||||||
} from "./convertChoiceAbilities.js";
|
} from "./convertChoiceAbilities.js";
|
||||||
import { applyTransforms } from "./index.js";
|
import { applyTransforms } from "./index.js";
|
||||||
import { mergeDuplicateUnits } from "./mergeDuplicateUnits.js";
|
import { mergeDuplicateUnits } from "./mergeDuplicateUnits.js";
|
||||||
@@ -52,6 +53,12 @@ const load = (name) =>
|
|||||||
* tokens preserve, not that the two implementations agree on a hash function.
|
* tokens preserve, not that the two implementations agree on a hash function.
|
||||||
* Every other attribute - including `entryId` and `publicationId`, which are
|
* Every other attribute - including `entryId` and `publicationId`, which are
|
||||||
* never generated - has to match exactly.
|
* never generated - has to match exactly.
|
||||||
|
*
|
||||||
|
* Text goes through `stripMarkup` for the same reason. Python resolved GW's
|
||||||
|
* ^^keyword^^ / **emphasis** markers in the option text it split out, because
|
||||||
|
* the renderer printed that text verbatim; the renderer now formats the markers
|
||||||
|
* itself, so the transform leaves them in place. Resolving them on both sides
|
||||||
|
* keeps the fixtures an oracle for everything else about the split.
|
||||||
*/
|
*/
|
||||||
function canonicalize(doc) {
|
function canonicalize(doc) {
|
||||||
const tokens = new Map();
|
const tokens = new Map();
|
||||||
@@ -72,11 +79,13 @@ function canonicalize(doc) {
|
|||||||
.map(([name, value]) => `${name}=${JSON.stringify(value)}`)
|
.map(([name, value]) => `${name}=${JSON.stringify(value)}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
const text = Array.from(element.childNodes)
|
const text = stripMarkup(
|
||||||
|
Array.from(element.childNodes)
|
||||||
.filter((node) => node.nodeType === 3 /* Text */)
|
.filter((node) => node.nodeType === 3 /* Text */)
|
||||||
.map((node) => node.data)
|
.map((node) => node.data)
|
||||||
.join("")
|
.join("")
|
||||||
.trim();
|
.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
lines.push(
|
lines.push(
|
||||||
`${" ".repeat(depth)}<${element.localName} ${attributes}>` +
|
`${" ".repeat(depth)}<${element.localName} ${attributes}>` +
|
||||||
|
|||||||
Reference in New Issue
Block a user