feat(nikki): track Infinity Nikki

Adds the game and its Game8 events page. One `SOURCES` entry against the
existing game8 parser, plus one date shape that page needs:
`parseLabelledStartEnd` reads a duration cell holding `Start: <date>` and
`End: <date>` split by a `<br>`, which a tag-stripping reader flattens
into one run of text.

Four of the seven events say `End: Permanent`. That is the source telling
us there is no deadline, so they publish with `endsAt: null` and
`endPrecision: "unknown"` rather than a plausible date. The labelled cell
is also structure end to end, so it never becomes a summary — stripping
the leading half would leave "End: Permanent" standing where a
description belongs.

Verified by re-extracting the page with a throwaway script independent of
the parser: 7 rows in, 7 events out, every date matching.

Note the source itself is stale — its "Current Events" table still lists
January-May 2025 and "Upcoming Events" says there are none. The adapter is
right; the page looks abandoned, and Nikki coverage worth relying on needs
a second source at a higher priority.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-17 17:20:54 +02:00
co-authored by Claude Opus 5
parent 243c7d70dd
commit 67951e81b4
9 changed files with 1380 additions and 0 deletions
+28
View File
@@ -147,6 +147,34 @@ export function parseShortSlashRange(
};
}
/**
* "Start: January 24, 2025 End: Permanent" → the start, and whatever the end
* half turns out to be.
*
* A labelled cell rather than a range: the two halves are separated by a `<br>`
* and each carries its own word. The colons are required, which is what stops
* the word "end" inside a description from splitting a cell that is really
* prose.
*
* An end half that is not a date ("Permanent", "TBD", "After maintenance")
* yields a null end rather than an invented one — the same outcome as
* `parseOpenRange`, and for the same reason.
*/
export function parseLabelledStartEnd(
input: string,
): { start: ParsedInstant; end: ParsedInstant | null } | null {
const m = /^\s*start\s*[:]\s*(.+?)(?:\s+end\s*[:]\s*(.*?))?\s*$/i.exec(
input,
);
if (!m) return null;
const start = parseMonthDayYear(m[1] ?? "");
if (start === null) return null;
const endHalf = m[2];
return { start, end: endHalf === undefined ? null : parseMonthDayYear(endHalf) };
}
/**
* A range whose start is a real date but whose end is not: "July 10, 2026 -
* Permanent", "Jul. 24, 2026 - End of 4.6", or a lone start date.