+14
-3
@@ -1358,8 +1358,15 @@ const makeKeywordsBold = (text) => {
|
||||
}
|
||||
}
|
||||
|
||||
// replace words wrapped in ^^ ^^ with <strong> tags
|
||||
newValue = newValue.replace(/\^\^([^\^]+)\^\^/g, "<strong>$1</strong>");
|
||||
// replace words wrapped in ^^ ^^ with <strong> tags. ^^ is the exporter's
|
||||
// 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
|
||||
newValue = newValue.replace(/\*\*([^\*]+)\*\*/g, "<strong>$1</strong>");
|
||||
@@ -1484,7 +1491,11 @@ const OtherAbilities = ({ abilities }) => {
|
||||
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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -25,14 +25,7 @@ export const defaultConfig = {
|
||||
|
||||
// Abilities to split into per-option profiles, matched against the
|
||||
// profile's ``name`` attribute.
|
||||
abilitiesToConvert: ["Canticles of the Omnissiah", "Battle Protocols"],
|
||||
|
||||
// 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",
|
||||
},
|
||||
abilitiesToConvert: ["Canticles of the Omnissiah", "Battle Protocols", "Icon of War"],
|
||||
};
|
||||
|
||||
// Which transforms run by default when a roster is loaded.
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
// * the original ``Abilities`` profile is kept, trimmed to the intro
|
||||
// paragraph (the "select one of the following" rule), and
|
||||
// * 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
|
||||
// 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
|
||||
* reader never wanted to see either way.
|
||||
*
|
||||
* The generated group renders through the renderer's generic ability table,
|
||||
* which prints its text verbatim, so the markers would otherwise show up as
|
||||
* literal punctuation. The intro profile keeps its original typeName, renders
|
||||
* through the normal path, and is therefore left alone.
|
||||
* Only the option *name* goes through this. It ends up as a profile ``name``
|
||||
* attribute, which the renderer prints as plain text, so a marker left in there
|
||||
* would show up as literal punctuation. The option text keeps its markers:
|
||||
* ``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
|
||||
* resolved text, so there is no ``"`` here that upper-casing could turn
|
||||
* into an entity no parser would recognise.
|
||||
*/
|
||||
const stripMarkup = (text) =>
|
||||
export const stripMarkup = (text) =>
|
||||
text
|
||||
.replace(KEYWORD_RE, (_, keyword) =>
|
||||
keyword.replaceAll("*", "").toUpperCase(),
|
||||
@@ -201,7 +209,6 @@ function buildProfile(
|
||||
*/
|
||||
export function convertChoiceAbilities(doc, config) {
|
||||
const wanted = new Set(config.abilitiesToConvert);
|
||||
const overrides = config.groupTitleOverrides ?? {};
|
||||
const root = doc.documentElement;
|
||||
if (!root) return [];
|
||||
|
||||
@@ -234,7 +241,10 @@ export function convertChoiceAbilities(doc, config) {
|
||||
);
|
||||
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 charTypeId = makeId(
|
||||
"characteristicType",
|
||||
@@ -265,7 +275,7 @@ export function convertChoiceAbilities(doc, config) {
|
||||
typeId,
|
||||
typeName: groupTitle,
|
||||
charTypeId,
|
||||
description: stripMarkup(option.text),
|
||||
description: option.text,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,10 +100,12 @@ describe("splitting choice abilities", () => {
|
||||
expect(group).toBeTruthy();
|
||||
expect(group.size).toBeGreaterThan(1);
|
||||
|
||||
// Options print as plain text: GW's ^^keyword^^ / **emphasis** markers
|
||||
// are resolved, not passed through as punctuation.
|
||||
for (const text of group.values()) {
|
||||
expect(JSON.stringify(text)).not.toMatch(/\^\^|\*\*/);
|
||||
// The option text keeps GW's ^^keyword^^ / **emphasis** markers for the
|
||||
// renderer to format; only the row titles are plain text, since those
|
||||
// render as-is.
|
||||
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 {
|
||||
convertChoiceAbilities,
|
||||
splitDescription,
|
||||
stripMarkup,
|
||||
} from "./convertChoiceAbilities.js";
|
||||
import { applyTransforms } from "./index.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.
|
||||
* Every other attribute - including `entryId` and `publicationId`, which are
|
||||
* 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) {
|
||||
const tokens = new Map();
|
||||
@@ -72,11 +79,13 @@ function canonicalize(doc) {
|
||||
.map(([name, value]) => `${name}=${JSON.stringify(value)}`)
|
||||
.join(" ");
|
||||
|
||||
const text = Array.from(element.childNodes)
|
||||
.filter((node) => node.nodeType === 3 /* Text */)
|
||||
.map((node) => node.data)
|
||||
.join("")
|
||||
.trim();
|
||||
const text = stripMarkup(
|
||||
Array.from(element.childNodes)
|
||||
.filter((node) => node.nodeType === 3 /* Text */)
|
||||
.map((node) => node.data)
|
||||
.join("")
|
||||
.trim(),
|
||||
);
|
||||
|
||||
lines.push(
|
||||
`${" ".repeat(depth)}<${element.localName} ${attributes}>` +
|
||||
|
||||
Reference in New Issue
Block a user