diff --git a/.claude/skills/orphan-content/audit-orphans.ts b/.claude/skills/orphan-content/audit-orphans.ts index 11c782df..b4962306 100644 --- a/.claude/skills/orphan-content/audit-orphans.ts +++ b/.claude/skills/orphan-content/audit-orphans.ts @@ -377,10 +377,9 @@ function mentionEdges({ /** * Which bodies `remarkProseLinks` would turn into a link to each target. * - * Only `characters`, `houses`, `weapons` and `dragons` render markdown with a - * prose-link index; `app/battles`, `app/castles` and `app/events` call - * `renderMarkdown` with no index, so their bodies emit no links at all and are - * excluded as sources. + * Every detail page except `app/castles` renders markdown with a prose-link + * index, so castle bodies emit no links at all and are excluded as sources. + * Castles, battles and events are still link targets from every other body. * * The index is built with an empty `mentions` list on purpose. Mentions only * widen a target's surface forms (a bare first name, a bare house name), and @@ -394,6 +393,9 @@ function proseEdges(collections: Collections): Edge[] { allHouses: collections.houses, allWeapons: collections.weapons, allDragons: collections.dragons, + allCastles: collections.castles, + allBattles: collections.battles, + allEvents: collections.events, current: { kind: "character", slug: "", mentions: [] }, }); @@ -402,6 +404,9 @@ function proseEdges(collections: Collections): Edge[] { house: "houses", weapon: "weapons", dragon: "dragons", + castle: "castles", + battle: "battles", + event: "events", } as const satisfies Record; // First form wins, matching `compileIndex` in `lib/prose-links.ts`. Two @@ -452,6 +457,16 @@ function proseEdges(collections: Collections): Edge[] { slug: entry.slug, body: entry.body, })), + ...collections.battles.map((entry) => ({ + collection: "battles" as const, + slug: entry.slug, + body: entry.body, + })), + ...collections.events.map((entry) => ({ + collection: "events" as const, + slug: entry.slug, + body: entry.body, + })), ]; return linkingBodies.flatMap(({ collection, slug, body }) => { @@ -459,9 +474,9 @@ function proseEdges(collections: Collections): Edge[] { const matched = body.match(pattern) ?? []; const hit = matched.reduce>((seen, form) => { const to = formToKey.get(form); - // `compileIndex` drops any target sharing the page's own slug, across - // collections, because `selfSlug` is a bare slug. - if (to && !to.endsWith(`/${slug}`)) seen.add(to); + // `compileIndex` drops only the page's own entry; another collection's + // entry with the same slug still links, matching `targetKey`. + if (to && to !== from) seen.add(to); return seen; }, new Set()); return [...hit].map((to) => ({ from, field: "prose", to })); diff --git a/.claude/skills/populate-entry/SKILL.md b/.claude/skills/populate-entry/SKILL.md index e8a20324..942742ff 100644 --- a/.claude/skills/populate-entry/SKILL.md +++ b/.claude/skills/populate-entry/SKILL.md @@ -21,12 +21,11 @@ The research half of the job is identical across all five: one AWOIAF article, o **The core insight: an empty field is not automatically a gap.** Several schema fields are empty on every entry in their collection because nothing renders them, so filling one in is noise, not progress: -| Field | Filled | Why it stays empty | -| ---------------------- | ------ | ------------------------------------------------------------------------------------ | -| `castles.sworn-houses` | 0/146 | Only `lib/relations.ts` reads it, and nothing in `app/` calls `buildRelationGraph`. | -| `battles.mentions` | 0/72 | `buildProseLinkIndex` never runs on a battle page, so `mentions` cannot do anything. | -| `events.participants` | 0/53 | The event page renders subtitle, body, and sources. Nothing else. | -| `events.casualties` | 0/53 | Same. | +| Field | Filled | Why it stays empty | +| ---------------------- | ------ | ----------------------------------------------------------------------------------- | +| `castles.sworn-houses` | 0/146 | Only `lib/relations.ts` reads it, and nothing in `app/` calls `buildRelationGraph`. | +| `events.participants` | 0/53 | The event page renders subtitle, body, and sources. Nothing else. | +| `events.casualties` | 0/53 | Same. | `audit-entries.ts` encodes this: a field only counts against an entry when the rest of its own collection fills it. Score is deviation from the collection norm, not distance from the schema. @@ -75,7 +74,11 @@ Voice notes, matching `harrenhal.md`, `dreadfort.md`, and `battle-of-the-blackwa - Never invent canon. If AWOIAF does not record it, leave it out. - No em dashes or en dashes anywhere in this repo. -Prose auto-linking runs on characters, houses, weapons, and dragons only. `app/castles/[slug]`, `app/battles/[slug]`, and `app/events/[slug]` call `renderMarkdown` without a `proseLinks` index, so nothing in those three bodies becomes a link. Write names freely there; also do not expect the reader to be able to click them. +Prose auto-linking runs on character, house, weapon, dragon, battle, and event pages. Only `app/castles/[slug]` still calls `renderMarkdown` without a `proseLinks` index, so a castle body never links out. The link targets are characters, houses, weapons, dragons, castles, battles, and events; each is matched on its `name` and `aliases`, and a name that starts with "The" also matches without the article, so "the Twins" and "the Red Wedding" link. Three rules follow from `lib/prose-links.ts`: + +- `mentions` widens the match. A character listed there also links on first name alone ("Lord Tywin"), and a house listed there links on its bare name ("the Lannister host"). List one character per first name: a shared first name goes to whichever target registers first, so two Aegons in one `mentions` list means one of them never links. +- A castle that shares its name with a house (Darry, Rosby, the Hightower) never auto-links, because `mentions` carries bare slugs and cannot tell the castle from the house. Write an explicit markdown link when the castle is meant. +- Write `aliases` without a leading article ("Doom", not "the Doom"): the alias would otherwise win the match at "the" and swallow the longer "Doom of Valyria". ## Step 3: frontmatter, per collection @@ -113,8 +116,8 @@ The most complex frontmatter in the repo, and every field below renders in `Batt | `victor` | no | 63/72. The winning `side` label, spelled the same way. Omit only when the outcome was genuinely undecided. | | `outcome` | no | 69/72. One sentence, present tense. | | `casualties[]` | no | 33/72. Character slugs. Renders as "Fallen". | -| `aliases[]` | no | 23/72. Renders as "Also called". | -| `mentions[]` | no | Leave empty. See the overview table. | +| `aliases[]` | no | 23/72. Renders as "Also called", and every alias is a prose-link surface form. | +| `mentions[]` | no | Characters the body names by first name only, and houses it names by bare name. One character per first name. | **The `region` and Essos trap.** `landmassForBattle` in `lib/timeline.ts` puts a battle in the Westeros timeline column when it has any `region`, and otherwise checks a hardcoded `ESSOS_SLUGS` set. So an Essos battle needs **both** no `region` **and** an entry in `ESSOS_SLUGS`. Adding `region: crownlands` to `battle-of-meereen` to clear an audit gap would silently move it to the wrong column. Battles beyond the Wall and realm-wide wars correctly carry no `region` either. @@ -127,6 +130,8 @@ The most complex frontmatter in the repo, and every field below renders in `Batt | `location` | yes | 53/53 store a **display string** ("King's Landing", "Vaes Dothrak"), never a slug. | | `landmass` | yes | `westeros`, `essos`, `summer-isles`. This alone picks the timeline column; events need no `ESSOS_SLUGS`. | | `outcome` | no | 53/53 carry it even though nothing renders it. Keep the convention. | +| `aliases` | no | Prose-link surface forms for the event, written without a leading article. | +| `mentions` | no | Same rule as battles: first-name characters and bare-name houses the body uses. One character per first name. | `buildRelationGraph` in `lib/relations.ts` keys `eventsByLocation` off `location` as though it were a castle slug. No entry stores a slug there, so that map is empty. Do not "fix" one entry to a slug; the page prints `location` verbatim in the subtitle. @@ -170,6 +175,7 @@ Seven entries, all populated. Use this section when adding an eighth. | `dragons.house` | `content/houses/` | | `dragons.riders[]` | `content/characters/` | | `weapons.mentions[]`, `dragons.mentions[]` | any entity slug | +| `battles.mentions[]`, `events.mentions[]` | any entity slug | Check before writing a slug: `ls content/houses/.md`. Do not create a stub in another collection just to satisfy a reference; drop the reference instead. @@ -224,20 +230,21 @@ bun run build # static export; run it after touching cont ## Common mistakes -| Mistake | Why it goes wrong | -| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -| Filling every empty field the schema allows | `sworn-houses`, `battles.mentions`, `events.participants`, and `events.casualties` are empty corpus-wide. | -| Adding `region` to an Essos battle to clear a gap | Moves it to the Westeros timeline column. `region` plus `ESSOS_SLUGS` decide the column together. | -| Writing a castle slug into `events.location` | All 53 entries store a display string, and the page prints it verbatim. | -| Putting `## ` headings in a battle, event, weapon, or dragon body | 0 of those 162 entries use headings. Only castles do. | -| Writing a castle body of three or four sections | The longest castle in the repo is 1,097 non-whitespace characters. Median is 593. | -| `precision: exact` on a legendary date | Drops the asterisk the timeline uses to mark approximate dates. | -| Referencing a character or house slug that does not exist | `lib/content-integrity.test.ts` fails the build. There is no graceful fallback here. | -| Expecting names in a castle or battle body to auto-link | Those pages call `renderMarkdown` without a `proseLinks` index. | -| Retrying plain `WebFetch` on awoiaf.westeros.org | Cloudflare 403s every page. Go straight to the CDX pipeline. | -| Citing the `web.archive.org` URL in `sources` | The mirror is the fetch mechanism, not the citation. | -| `bun test` instead of `bun run test` | The script is `bun test --isolate --dots`; the bare form mis-reports the DOM suite. | -| Committing without `bun format` | `oxfmt` covers `.claude/**/*.ts` and markdown, and CI fails on drift. | +| Mistake | Why it goes wrong | +| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Filling every empty field the schema allows | `sworn-houses`, `events.participants`, and `events.casualties` are empty corpus-wide. | +| Adding `region` to an Essos battle to clear a gap | Moves it to the Westeros timeline column. `region` plus `ESSOS_SLUGS` decide the column together. | +| Writing a castle slug into `events.location` | All 53 entries store a display string, and the page prints it verbatim. | +| Putting `## ` headings in a battle, event, weapon, or dragon body | 0 of those 162 entries use headings. Only castles do. | +| Writing a castle body of three or four sections | The longest castle in the repo is 1,097 non-whitespace characters. Median is 593. | +| `precision: exact` on a legendary date | Drops the asterisk the timeline uses to mark approximate dates. | +| Referencing a character or house slug that does not exist | `lib/content-integrity.test.ts` fails the build. There is no graceful fallback here. | +| Expecting names in a castle body to auto-link | The castle page calls `renderMarkdown` without a `proseLinks` index. Battles and events do link. | +| Listing two characters who share a first name in `mentions` | The first to register takes the bare name; the other never links on it. | +| Retrying plain `WebFetch` on awoiaf.westeros.org | Cloudflare 403s every page. Go straight to the CDX pipeline. | +| Citing the `web.archive.org` URL in `sources` | The mirror is the fetch mechanism, not the citation. | +| `bun test` instead of `bun run test` | The script is `bun test --isolate --dots`; the bare form mis-reports the DOM suite. | +| Committing without `bun format` | `oxfmt` covers `.claude/**/*.ts` and markdown, and CI fails on drift. | ## Related skills diff --git a/app/battles/[slug]/page.tsx b/app/battles/[slug]/page.tsx index f7b9232b..f8b51f85 100644 --- a/app/battles/[slug]/page.tsx +++ b/app/battles/[slug]/page.tsx @@ -6,8 +6,13 @@ import { loadAllBattles, loadAllHouses, loadAllCharacters, + loadAllWeapons, + loadAllDragons, + loadAllCastles, + loadAllEvents, renderMarkdown, } from "@/lib/content"; +import { buildProseLinkIndex } from "@/lib/prose-links"; import { PlateLayout } from "@/components/PlateLayout"; import { Sources } from "@/components/Sources"; import { BattleInfobox } from "@/components/BattleInfobox"; @@ -42,10 +47,25 @@ export default async function BattlePage({ params: Promise<{ slug: string }>; }) { const { slug } = await params; - const [battle, allHouses, allCharacters, image] = await Promise.all([ + const [ + battle, + allHouses, + allCharacters, + allWeapons, + allDragons, + allCastles, + allBattles, + allEvents, + image, + ] = await Promise.all([ loadBattle(slug).catch(() => null), loadAllHouses(), loadAllCharacters(), + loadAllWeapons(), + loadAllDragons(), + loadAllCastles(), + loadAllBattles(), + loadAllEvents(), findBattleImage(slug), ]); if (!battle) notFound(); @@ -54,7 +74,19 @@ export default async function BattlePage({ const charactersBySlug = bySlug(allCharacters); const fm = battle.frontmatter; - const html = battle.body.trim() ? await renderMarkdown(battle.body) : ""; + const proseLinks = buildProseLinkIndex({ + allCharacters, + allHouses, + allWeapons, + allDragons, + allCastles, + allBattles, + allEvents, + current: { kind: "battle", slug, mentions: fm.mentions }, + }); + const html = battle.body.trim() + ? await renderMarkdown(battle.body, { proseLinks }) + : ""; const subtitle = [formatBattleWhen(fm.start, fm.end), fm.war] .filter(Boolean) .join(" ยท "); diff --git a/app/characters/[slug]/page.tsx b/app/characters/[slug]/page.tsx index 5bd5e515..9769cd7e 100644 --- a/app/characters/[slug]/page.tsx +++ b/app/characters/[slug]/page.tsx @@ -5,6 +5,9 @@ import { loadAllHouses, loadAllWeapons, loadAllDragons, + loadAllCastles, + loadAllBattles, + loadAllEvents, loadCharacter, renderMarkdown, } from "@/lib/content"; @@ -89,14 +92,25 @@ export default async function CharacterPage({ const fm = character.frontmatter; - const [allCharacters, allHouses, allWeapons, allDragons, portraits] = - await Promise.all([ - loadAllCharacters(), - loadAllHouses(), - loadAllWeapons(), - loadAllDragons(), - findPortraitVariants({ slug, name: fm.name, sex: fm.sex }), - ]); + const [ + allCharacters, + allHouses, + allWeapons, + allDragons, + allCastles, + allBattles, + allEvents, + portraits, + ] = await Promise.all([ + loadAllCharacters(), + loadAllHouses(), + loadAllWeapons(), + loadAllDragons(), + loadAllCastles(), + loadAllBattles(), + loadAllEvents(), + findPortraitVariants({ slug, name: fm.name, sex: fm.sex }), + ]); const charactersBySlug = bySlug(allCharacters); const housesBySlug = bySlug(allHouses); @@ -152,6 +166,9 @@ export default async function CharacterPage({ slug: d.slug, frontmatter: d.frontmatter, })), + allCastles, + allBattles, + allEvents, current: { kind: "character", slug, mentions: fm.mentions }, }); const html = character.body.trim() diff --git a/app/dragons/[slug]/page.tsx b/app/dragons/[slug]/page.tsx index d4b73d3c..9289e1e3 100644 --- a/app/dragons/[slug]/page.tsx +++ b/app/dragons/[slug]/page.tsx @@ -6,6 +6,9 @@ import { loadAllWeapons, loadAllHouses, loadAllCharacters, + loadAllCastles, + loadAllBattles, + loadAllEvents, renderMarkdown, } from "@/lib/content"; import { buildProseLinkIndex } from "@/lib/prose-links"; @@ -42,14 +45,25 @@ export default async function DragonPage({ params: Promise<{ slug: string }>; }) { const { slug } = await params; - const [dragon, allHouses, allCharacters, allWeapons, allDragons] = - await Promise.all([ - loadDragon(slug).catch(() => null), - loadAllHouses(), - loadAllCharacters(), - loadAllWeapons(), - loadAllDragons(), - ]); + const [ + dragon, + allHouses, + allCharacters, + allWeapons, + allDragons, + allCastles, + allBattles, + allEvents, + ] = await Promise.all([ + loadDragon(slug).catch(() => null), + loadAllHouses(), + loadAllCharacters(), + loadAllWeapons(), + loadAllDragons(), + loadAllCastles(), + loadAllBattles(), + loadAllEvents(), + ]); if (!dragon) notFound(); const housesBySlug = bySlug(allHouses); @@ -73,6 +87,9 @@ export default async function DragonPage({ slug: d.slug, frontmatter: d.frontmatter, })), + allCastles, + allBattles, + allEvents, current: { kind: "dragon", slug, mentions: dragon.frontmatter.mentions }, }); const html = diff --git a/app/events/[slug]/page.tsx b/app/events/[slug]/page.tsx index ba48c9bf..d8ccc65e 100644 --- a/app/events/[slug]/page.tsx +++ b/app/events/[slug]/page.tsx @@ -1,6 +1,17 @@ import { notFound } from "next/navigation"; import Link from "next/link"; -import { loadEvent, loadAllEvents, renderMarkdown } from "@/lib/content"; +import { + loadEvent, + loadAllEvents, + loadAllHouses, + loadAllCharacters, + loadAllWeapons, + loadAllDragons, + loadAllCastles, + loadAllBattles, + renderMarkdown, +} from "@/lib/content"; +import { buildProseLinkIndex } from "@/lib/prose-links"; import { PlateLayout } from "@/components/PlateLayout"; import { Sources } from "@/components/Sources"; import { formatBattleWhen } from "@/lib/battle-date"; @@ -32,11 +43,41 @@ export default async function EventPage({ params: Promise<{ slug: string }>; }) { const { slug } = await params; - const event = await loadEvent(slug).catch(() => null); + const [ + event, + allHouses, + allCharacters, + allWeapons, + allDragons, + allCastles, + allBattles, + allEvents, + ] = await Promise.all([ + loadEvent(slug).catch(() => null), + loadAllHouses(), + loadAllCharacters(), + loadAllWeapons(), + loadAllDragons(), + loadAllCastles(), + loadAllBattles(), + loadAllEvents(), + ]); if (!event) notFound(); const fm = event.frontmatter; - const html = event.body.trim() ? await renderMarkdown(event.body) : ""; + const proseLinks = buildProseLinkIndex({ + allCharacters, + allHouses, + allWeapons, + allDragons, + allCastles, + allBattles, + allEvents, + current: { kind: "event", slug, mentions: fm.mentions }, + }); + const html = event.body.trim() + ? await renderMarkdown(event.body, { proseLinks }) + : ""; const typeLabel = fm.type[0].toUpperCase() + fm.type.slice(1); const subtitle = [ typeLabel, diff --git a/app/houses/[slug]/page.tsx b/app/houses/[slug]/page.tsx index c66e96f7..86a33c51 100644 --- a/app/houses/[slug]/page.tsx +++ b/app/houses/[slug]/page.tsx @@ -7,6 +7,8 @@ import { loadAllCharacters, loadAllWeapons, loadAllDragons, + loadAllBattles, + loadAllEvents, renderMarkdown, } from "@/lib/content"; import { PlateLayout } from "@/components/PlateLayout"; @@ -55,15 +57,25 @@ export default async function HousePage({ params: Promise<{ slug: string }>; }) { const { slug } = await params; - const [house, allHouses, castles, characters, allWeapons, allDragons] = - await Promise.all([ - loadHouse(slug).catch(() => null), - loadAllHouses(), - loadAllCastles(), - loadAllCharacters(), - loadAllWeapons(), - loadAllDragons(), - ]); + const [ + house, + allHouses, + castles, + characters, + allWeapons, + allDragons, + allBattles, + allEvents, + ] = await Promise.all([ + loadHouse(slug).catch(() => null), + loadAllHouses(), + loadAllCastles(), + loadAllCharacters(), + loadAllWeapons(), + loadAllDragons(), + loadAllBattles(), + loadAllEvents(), + ]); if (!house) notFound(); const housesBySlug = bySlug(allHouses); @@ -101,6 +113,9 @@ export default async function HousePage({ slug: d.slug, frontmatter: d.frontmatter, })), + allCastles: castles, + allBattles, + allEvents, current: { kind: "house", slug, mentions: house.frontmatter.mentions }, }); const html = await renderMarkdown(house.body, { proseLinks }); diff --git a/app/weapons/[slug]/page.tsx b/app/weapons/[slug]/page.tsx index 8549ec93..d7bbe05f 100644 --- a/app/weapons/[slug]/page.tsx +++ b/app/weapons/[slug]/page.tsx @@ -5,6 +5,9 @@ import { loadWeapon, loadAllWeapons, loadAllDragons, + loadAllCastles, + loadAllBattles, + loadAllEvents, loadAllHouses, loadAllCharacters, renderMarkdown, @@ -63,15 +66,27 @@ export default async function WeaponPage({ params: Promise<{ slug: string }>; }) { const { slug } = await params; - const [weapon, allHouses, allCharacters, allWeapons, allDragons, image] = - await Promise.all([ - loadWeapon(slug).catch(() => null), - loadAllHouses(), - loadAllCharacters(), - loadAllWeapons(), - loadAllDragons(), - findWeaponImage(slug), - ]); + const [ + weapon, + allHouses, + allCharacters, + allWeapons, + allDragons, + allCastles, + allBattles, + allEvents, + image, + ] = await Promise.all([ + loadWeapon(slug).catch(() => null), + loadAllHouses(), + loadAllCharacters(), + loadAllWeapons(), + loadAllDragons(), + loadAllCastles(), + loadAllBattles(), + loadAllEvents(), + findWeaponImage(slug), + ]); if (!weapon) notFound(); const housesBySlug = bySlug(allHouses); @@ -95,6 +110,9 @@ export default async function WeaponPage({ slug: d.slug, frontmatter: d.frontmatter, })), + allCastles, + allBattles, + allEvents, current: { kind: "weapon", slug, mentions: weapon.frontmatter.mentions }, }); const html = diff --git a/content/battles/andal-wars-of-conquest.md b/content/battles/andal-wars-of-conquest.md index 088a2849..1e357c01 100644 --- a/content/battles/andal-wars-of-conquest.md +++ b/content/battles/andal-wars-of-conquest.md @@ -21,6 +21,7 @@ commanders: [tristifer-iv-mudd, theon-stark] victor: "The Andals" outcome: "The Andals overwhelmed the First Men kingdoms south of the Neck and shattered the children of the forest, but the Kings of Winter turned them back at the Neck, so the North alone kept the old blood and the old gods." casualties: [tristifer-iv-mudd] +mentions: [mudd] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Andal_Invasion diff --git a/content/battles/battle-at-duskendale.md b/content/battles/battle-at-duskendale.md index e3914962..615c9c15 100644 --- a/content/battles/battle-at-duskendale.md +++ b/content/battles/battle-at-duskendale.md @@ -26,6 +26,7 @@ victor: "the Iron Throne" outcome: "A northern foraging host was trapped and destroyed at Duskendale, costing Robb Stark a large part of his foot before the Red Wedding." casualties: [helman-tallhart] aliases: [] +mentions: [tywin-lannister, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_at_Duskendale diff --git a/content/battles/battle-at-rooks-rest.md b/content/battles/battle-at-rooks-rest.md index 12e5f7b8..f860540a 100644 --- a/content/battles/battle-at-rooks-rest.md +++ b/content/battles/battle-at-rooks-rest.md @@ -29,6 +29,7 @@ victor: "greens" outcome: "A green trap that saw Princess Rhaenys and her dragon Meleys slain, though Aegon II and Sunfyre were both grievously maimed." casualties: [rhaenys-targaryen-queen-who-never-was] aliases: ["Battle of Rook's Rest"] +mentions: [aegon-ii-targaryen, rhaenyra-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_at_Rook's_Rest diff --git a/content/battles/battle-beneath-the-gods-eye.md b/content/battles/battle-beneath-the-gods-eye.md index 51f902ed..606e5c62 100644 --- a/content/battles/battle-beneath-the-gods-eye.md +++ b/content/battles/battle-beneath-the-gods-eye.md @@ -23,6 +23,13 @@ victor: "Maegor I" outcome: "Maegor slays his nephew Aegon and the dragon Quicksilver above the God's Eye, ending the rival claim to the Iron Throne." casualties: [aegon-the-uncrowned] aliases: ["The Battle Beneath the God's Eye"] +mentions: + [ + aegon-the-uncrowned, + jaehaerys-i-targaryen, + maegor-i-targaryen, + aenys-i-targaryen, + ] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_Beneath_the_Gods_Eye diff --git a/content/battles/battle-in-the-gullet.md b/content/battles/battle-in-the-gullet.md index 75ae9c52..5406a7c8 100644 --- a/content/battles/battle-in-the-gullet.md +++ b/content/battles/battle-in-the-gullet.md @@ -23,6 +23,7 @@ victor: "blacks" outcome: "A ruinously costly black victory that broke the Triarchy fleet but claimed Prince Jacaerys Velaryon and his dragon Vermax." casualties: [jacaerys-velaryon] aliases: ["Battle of the Gullet"] +mentions: [rhaenyra-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_in_the_Gullet diff --git a/content/battles/battle-in-the-whispering-wood.md b/content/battles/battle-in-the-whispering-wood.md index c1630776..f6b76ab2 100644 --- a/content/battles/battle-in-the-whispering-wood.md +++ b/content/battles/battle-in-the-whispering-wood.md @@ -25,6 +25,7 @@ commanders: [robb-stark, brynden-tully, jaime-lannister] victor: "House Stark" outcome: "Robb Stark's night ambush destroyed Jaime Lannister's cavalry and took Jaime himself captive, a devastating blow to the Lannister cause in the riverlands." casualties: [daryn-hornwood] +mentions: [tywin-lannister, stark, karstark, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_in_the_Whispering_Wood diff --git a/content/battles/battle-of-ashford.md b/content/battles/battle-of-ashford.md index 96ef44d6..9af276fd 100644 --- a/content/battles/battle-of-ashford.md +++ b/content/battles/battle-of-ashford.md @@ -25,6 +25,7 @@ commanders: [robert-baratheon, randyll-tarly] victor: "loyalists" outcome: "Randyll Tarly's vanguard falls upon Robert Baratheon before the main Reach host arrives and drives him from the field, the only defeat Robert suffers in the war." aliases: [] +mentions: [aerys-ii-targaryen, tyrell, tarly] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_Ashford diff --git a/content/battles/battle-of-fair-isle.md b/content/battles/battle-of-fair-isle.md index 0423b1d8..07deb8c5 100644 --- a/content/battles/battle-of-fair-isle.md +++ b/content/battles/battle-of-fair-isle.md @@ -21,6 +21,7 @@ participants: commanders: [stannis-baratheon, victarion-greyjoy] victor: "the Iron Throne" outcome: "Stannis Baratheon destroyed the Iron Fleet at sea, ending ironborn command of the western waters and dooming Balon Greyjoy's rebellion." +mentions: [stannis-baratheon, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_Fair_Isle diff --git a/content/battles/battle-of-ice.md b/content/battles/battle-of-ice.md index 4ce7b36f..8cb7b1cf 100644 --- a/content/battles/battle-of-ice.md +++ b/content/battles/battle-of-ice.md @@ -21,6 +21,7 @@ participants: commanders: [stannis-baratheon, roose-bolton] outcome: "The engagement is still unresolved and has not yet occurred in the published novels." aliases: [] +mentions: [frey, bolton, manderly] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_Ice diff --git a/content/battles/battle-of-oxcross.md b/content/battles/battle-of-oxcross.md index ec17f47a..7972c9f2 100644 --- a/content/battles/battle-of-oxcross.md +++ b/content/battles/battle-of-oxcross.md @@ -22,6 +22,7 @@ commanders: [robb-stark, stafford-lannister] victor: "House Stark" outcome: "Robb Stark destroyed Ser Stafford Lannister's raw host in a night attack, slew Stafford, and threw open the westerlands to northern raiders." casualties: [stafford-lannister] +mentions: [tywin-lannister, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_Oxcross diff --git a/content/battles/battle-of-summerhall.md b/content/battles/battle-of-summerhall.md index 84fd9804..d058ed97 100644 --- a/content/battles/battle-of-summerhall.md +++ b/content/battles/battle-of-summerhall.md @@ -25,6 +25,7 @@ commanders: [robert-baratheon] victor: "rebels" outcome: "Robert Baratheon defeats three loyalist hosts in a single day, scattering the lords Grandison, Cafferen, and Fell before they can combine against him." aliases: [] +mentions: [cafferen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_Summerhall diff --git a/content/battles/battle-of-the-bells.md b/content/battles/battle-of-the-bells.md index cab2ece3..ca658254 100644 --- a/content/battles/battle-of-the-bells.md +++ b/content/battles/battle-of-the-bells.md @@ -26,6 +26,7 @@ victor: "rebels" outcome: "The Stark and Tully hosts storm Stoney Sept as its bells ring the alarm, breaking Jon Connington's search for the wounded Robert and saving the rebellion at its lowest ebb." casualties: [denys-arryn, myles-mooton] aliases: [] +mentions: [catelyn-stark, aerys-ii-targaryen, connington] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_the_Bells diff --git a/content/battles/battle-of-the-blackwater.md b/content/battles/battle-of-the-blackwater.md index f4d0b31c..41e70ea9 100644 --- a/content/battles/battle-of-the-blackwater.md +++ b/content/battles/battle-of-the-blackwater.md @@ -32,6 +32,15 @@ commanders: victor: "House Lannister and House Tyrell" outcome: "Stannis Baratheon's great assault on King's Landing was shattered by wildfire and a boom-chain across the river, then routed when the Lannister and Tyrell host struck his landed army from behind." aliases: ["The Battle of the Blackwater Rush"] +mentions: + [ + joffrey-baratheon, + margaery-tyrell, + renly-baratheon, + tyrell, + baratheon, + lannister, + ] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_the_Blackwater diff --git a/content/battles/battle-of-the-burning-mill.md b/content/battles/battle-of-the-burning-mill.md index 3503a145..a076f7e8 100644 --- a/content/battles/battle-of-the-burning-mill.md +++ b/content/battles/battle-of-the-burning-mill.md @@ -22,6 +22,7 @@ commanders: [samwell-blackwood, amos-bracken] outcome: "A bloody and indecisive clash that drew the riverlands into the war; Lord Samwell Blackwood was slain by Ser Amos Bracken." casualties: [samwell-blackwood] aliases: ["Battle of the Burning Mill"] +mentions: [aegon-ii-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_the_Burning_Mill diff --git a/content/battles/battle-of-the-camps.md b/content/battles/battle-of-the-camps.md index a176ad5c..8d658778 100644 --- a/content/battles/battle-of-the-camps.md +++ b/content/battles/battle-of-the-camps.md @@ -24,6 +24,7 @@ participants: commanders: [robb-stark, brynden-tully, edmure-tully] victor: "House Stark" outcome: "Robb Stark stormed the leaderless Lannister camps and lifted the siege of Riverrun, uniting the northern and river hosts and freeing House Tully." +mentions: [jaime-lannister, tully, stark, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_the_Camps diff --git a/content/battles/battle-of-the-fords.md b/content/battles/battle-of-the-fords.md index a887686b..c133ee38 100644 --- a/content/battles/battle-of-the-fords.md +++ b/content/battles/battle-of-the-fords.md @@ -22,6 +22,7 @@ commanders: [edmure-tully, tywin-lannister, gregor-clegane] victor: "House Tully" outcome: "Edmure Tully held every crossing of the Red Fork against Lord Tywin's host, a river victory that nonetheless spoiled Robb Stark's plan to lure the enemy west." aliases: ["The Battle of the Stone Mill"] +mentions: [robb-stark, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_the_Fords diff --git a/content/battles/battle-of-the-golden-tooth.md b/content/battles/battle-of-the-golden-tooth.md index a90ec011..6fe5ea50 100644 --- a/content/battles/battle-of-the-golden-tooth.md +++ b/content/battles/battle-of-the-golden-tooth.md @@ -24,6 +24,7 @@ participants: commanders: [jaime-lannister] victor: "House Lannister" outcome: "Jaime Lannister broke the river lords holding the pass and opened the road into the riverlands, allowing him to march on Riverrun." +mentions: [piper, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_the_Golden_Tooth diff --git a/content/battles/battle-of-the-green-fork.md b/content/battles/battle-of-the-green-fork.md index d51c0a22..cf2eb90d 100644 --- a/content/battles/battle-of-the-green-fork.md +++ b/content/battles/battle-of-the-green-fork.md @@ -21,6 +21,7 @@ participants: commanders: [roose-bolton, tywin-lannister, tyrion-lannister] victor: "House Lannister" outcome: "Tywin Lannister drove Roose Bolton's northern foot from the field, but the fight was a feint that masked Robb Stark's true march west against Jaime at Riverrun." +mentions: [jaime-lannister, bolton, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_the_Green_Fork diff --git a/content/battles/battle-of-the-shield-islands.md b/content/battles/battle-of-the-shield-islands.md index c9f5d509..6e125e60 100644 --- a/content/battles/battle-of-the-shield-islands.md +++ b/content/battles/battle-of-the-shield-islands.md @@ -22,6 +22,7 @@ commanders: [euron-greyjoy, victarion-greyjoy] victor: "the ironborn" outcome: "Euron Greyjoy seizes the four Shield Islands and looses the Iron Fleet up the Mander into the heart of the Reach." aliases: [] +mentions: [victarion-greyjoy, redwyne] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Shield_Islands diff --git a/content/battles/battle-of-the-trident.md b/content/battles/battle-of-the-trident.md index 38e136dc..e54e6c2a 100644 --- a/content/battles/battle-of-the-trident.md +++ b/content/battles/battle-of-the-trident.md @@ -31,6 +31,7 @@ victor: "rebels" outcome: "Robert Baratheon slays Prince Rhaegar Targaryen in the waters of the Trident, and the loyalist host breaks, deciding the war in the rebels' favor." casualties: [rhaegar-targaryen, lewyn-martell] aliases: [Battle of the Ruby Ford] +mentions: [aerys-ii-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Battle_of_the_Trident diff --git a/content/battles/burning-of-harrenhal.md b/content/battles/burning-of-harrenhal.md index efc492a5..48000a51 100644 --- a/content/battles/burning-of-harrenhal.md +++ b/content/battles/burning-of-harrenhal.md @@ -25,6 +25,7 @@ commanders: [aegon-i-targaryen, harren-hoare] victor: "House Targaryen" outcome: "Aegon burns Harren the Black and all his line within their newly raised castle, ending House Hoare and freeing the riverlands, whose lords bend the knee." casualties: [harren-hoare] +mentions: [aegon-i-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Harrenhal diff --git a/content/battles/capture-and-sack-of-winterfell.md b/content/battles/capture-and-sack-of-winterfell.md index ffb8732a..da16378f 100644 --- a/content/battles/capture-and-sack-of-winterfell.md +++ b/content/battles/capture-and-sack-of-winterfell.md @@ -26,6 +26,7 @@ participants: commanders: [theon-greyjoy, ramsay-bolton] outcome: "Theon Greyjoy seized the lightly held seat of the Starks, only for Ramsay Snow's men to storm it, butcher its people, and leave the castle a gutted ruin." aliases: [] +mentions: [balon-greyjoy, bran-stark, ramsay-bolton, stark, bolton] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Sack_of_Winterfell diff --git a/content/battles/daeron-is-conquest-of-dorne.md b/content/battles/daeron-is-conquest-of-dorne.md index 30c47b7e..317f65da 100644 --- a/content/battles/daeron-is-conquest-of-dorne.md +++ b/content/battles/daeron-is-conquest-of-dorne.md @@ -22,6 +22,7 @@ commanders: [daeron-i-targaryen, baelor-i-targaryen, alyn-velaryon] victor: "Iron Throne" outcome: "The Young Dragon broke the Dornish in the passes and took Sunspear, bringing Dorne under the Iron Throne for the first time." casualties: [rickon-stark-son-of-cregan] +mentions: [aegon-i-targaryen, fowler, targaryen, yronwood] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Conquest_of_Dorne diff --git a/content/battles/death-of-rhaenys-at-the-hellholt.md b/content/battles/death-of-rhaenys-at-the-hellholt.md index 5a2ef80a..ddc890c7 100644 --- a/content/battles/death-of-rhaenys-at-the-hellholt.md +++ b/content/battles/death-of-rhaenys-at-the-hellholt.md @@ -25,6 +25,7 @@ commanders: [rhaenys-targaryen] victor: "Dorne" outcome: "Queen Rhaenys and Meraxes are slain over the Hellholt; the loss shatters Aegon's restraint and begins the Dragon's Wroth." casualties: [rhaenys-targaryen] +mentions: [aegon-i-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Meraxes diff --git a/content/battles/disaster-at-the-fist-of-the-first-men.md b/content/battles/disaster-at-the-fist-of-the-first-men.md index d8eaad33..53c6d224 100644 --- a/content/battles/disaster-at-the-fist-of-the-first-men.md +++ b/content/battles/disaster-at-the-fist-of-the-first-men.md @@ -21,6 +21,7 @@ commanders: [jeor-mormont] victor: "the Others" outcome: "The Great Ranging is shattered atop the Fist and its survivors flee south in disarray." aliases: [] +mentions: [others] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Fist_of_the_First_Men diff --git a/content/battles/dornish-uprising.md b/content/battles/dornish-uprising.md index 215aa66c..d6b8939c 100644 --- a/content/battles/dornish-uprising.md +++ b/content/battles/dornish-uprising.md @@ -22,6 +22,7 @@ commanders: [daeron-i-targaryen] victor: "Dorne" outcome: "The Dornish threw off Targaryen rule and slew King Daeron I beneath a peace banner, restoring Dorne's independence." casualties: [daeron-i-targaryen] +mentions: [baelor-i-targaryen, daeron-i-targaryen, targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Conquest_of_Dorne diff --git a/content/battles/faith-militant-uprising.md b/content/battles/faith-militant-uprising.md index 60a2a6a6..dee04054 100644 --- a/content/battles/faith-militant-uprising.md +++ b/content/battles/faith-militant-uprising.md @@ -21,6 +21,15 @@ participants: commanders: [maegor-i-targaryen, aenys-i-targaryen, jaehaerys-i-targaryen] victor: "Iron Throne" outcome: "The militant orders of the Faith are crushed and later disbanded, and the Faith renounces the sword in exchange for a royal pardon." +mentions: + [ + aegon-the-uncrowned, + jaehaerys-i-targaryen, + maegor-i-targaryen, + rhaena-targaryen-daughter-of-aenys, + alys-harroway, + aenys-i-targaryen, + ] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Faith_Militant_uprising diff --git a/content/battles/field-of-fire.md b/content/battles/field-of-fire.md index 54a57a48..413a42ca 100644 --- a/content/battles/field-of-fire.md +++ b/content/battles/field-of-fire.md @@ -22,6 +22,7 @@ commanders: [aegon-i-targaryen, loren-i-lannister, mern-ix-gardener] victor: "House Targaryen" outcome: "Aegon's three dragons burn the largest host ever raised in Westeros; King Mern dies and the Gardener line is extinguished, while King Loren yields and keeps Casterly Rock." casualties: [mern-ix-gardener] +mentions: [rhaenys-targaryen, visenya-targaryen, gardener, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Field_of_Fire diff --git a/content/battles/first-battle-of-the-last-storm.md b/content/battles/first-battle-of-the-last-storm.md index 8fffa8fa..dff72f52 100644 --- a/content/battles/first-battle-of-the-last-storm.md +++ b/content/battles/first-battle-of-the-last-storm.md @@ -25,6 +25,7 @@ commanders: [orys-baratheon, argilac-durrandon] victor: "House Targaryen" outcome: "Orys Baratheon slays Argilac the Arrogant, ending the line of the Storm Kings and winning Storm's End, the Durrandon arms, and Argilac's daughter." casualties: [argilac-durrandon] +mentions: [targaryen, baratheon] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Last_Storm diff --git a/content/battles/first-battle-of-tumbleton.md b/content/battles/first-battle-of-tumbleton.md index 33964f77..25a40ca6 100644 --- a/content/battles/first-battle-of-tumbleton.md +++ b/content/battles/first-battle-of-tumbleton.md @@ -27,6 +27,7 @@ victor: "greens" outcome: "The Two Betrayers turned their dragons upon Rhaenyra's defenders, shattering the black host and delivering Tumbleton to fire and sack." casualties: [roderick-dustin] aliases: ["Fall of Tumbleton", "First Tumbleton"] +mentions: [aegon-ii-targaryen, rhaenyra-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/First_Battle_of_Tumbleton diff --git a/content/battles/first-blackfyre-rebellion.md b/content/battles/first-blackfyre-rebellion.md index 3b3dc1ee..8d4d23c3 100644 --- a/content/battles/first-blackfyre-rebellion.md +++ b/content/battles/first-blackfyre-rebellion.md @@ -37,6 +37,14 @@ casualties: quentyn-ball, ] aliases: ["Battle of the Redgrass Field"] +mentions: + [ + aegon-iv-targaryen, + aemon-blackfyre-son-of-daemon-i, + daemon-i-blackfyre, + daeron-ii-targaryen, + blackfyre, + ] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/First_Blackfyre_Rebellion diff --git a/content/battles/first-dornish-war-invasion.md b/content/battles/first-dornish-war-invasion.md index 14d3d31b..be87f97f 100644 --- a/content/battles/first-dornish-war-invasion.md +++ b/content/battles/first-dornish-war-invasion.md @@ -35,6 +35,7 @@ commanders: victor: "Dorne" outcome: "After nearly a decade of raids, burnings, and reprisals, the war ends in stalemate, and Dorne remains unconquered and independent." casualties: [rhaenys-targaryen] +mentions: [aegon-i-targaryen, rhaenys-targaryen, daeron-ii-targaryen, yronwood] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/First_Dornish_War diff --git a/content/battles/ironborn-wars-of-the-greyiron-and-hoare-kings.md b/content/battles/ironborn-wars-of-the-greyiron-and-hoare-kings.md index 96ee3e1d..69774718 100644 --- a/content/battles/ironborn-wars-of-the-greyiron-and-hoare-kings.md +++ b/content/battles/ironborn-wars-of-the-greyiron-and-hoare-kings.md @@ -21,6 +21,7 @@ participants: commanders: [urron-greyiron, harwyn-hoare, harren-hoare] victor: "The Ironborn" outcome: "The ironborn extended their reaving across the sunset sea, and under the black-blooded Hoare kings conquered the riverlands, ruling them until Aegon's Conquest." +mentions: [aegon-i-targaryen, greyiron] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Ironborn diff --git a/content/battles/raids-in-the-riverlands.md b/content/battles/raids-in-the-riverlands.md index 61732460..f07c373a 100644 --- a/content/battles/raids-in-the-riverlands.md +++ b/content/battles/raids-in-the-riverlands.md @@ -21,6 +21,7 @@ participants: commanders: [gregor-clegane, beric-dondarrion, tywin-lannister] victor: "House Lannister" outcome: "Lannister raiders under Ser Gregor Clegane burned the southern riverlands unopposed and ambushed the party sent to bring them to justice, opening the wider war." +mentions: [tyrion-lannister, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Raids_in_the_riverlands diff --git a/content/battles/red-wedding.md b/content/battles/red-wedding.md index 3efb601b..c0930705 100644 --- a/content/battles/red-wedding.md +++ b/content/battles/red-wedding.md @@ -26,6 +26,7 @@ victor: "House Frey and House Bolton" outcome: "Robb Stark, his lady mother, and the flower of his host were slaughtered under guest right at a wedding feast, breaking the northern cause in a single night." casualties: [robb-stark, catelyn-stark] aliases: [] +mentions: [catelyn-stark, frey, bolton] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Red_Wedding diff --git a/content/battles/sack-of-harrenhal.md b/content/battles/sack-of-harrenhal.md index 3a76618a..e0b12042 100644 --- a/content/battles/sack-of-harrenhal.md +++ b/content/battles/sack-of-harrenhal.md @@ -25,6 +25,7 @@ commanders: [roose-bolton] victor: "House Bolton" outcome: "Harrenhal passed into northern hands when the sellsword Brave Companions turned on their Lannister paymasters and opened the gates to Roose Bolton." aliases: [] +mentions: [lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Harrenhal diff --git a/content/battles/sack-of-kings-landing.md b/content/battles/sack-of-kings-landing.md index 40808917..58eaecd6 100644 --- a/content/battles/sack-of-kings-landing.md +++ b/content/battles/sack-of-kings-landing.md @@ -32,6 +32,7 @@ casualties: elia-martell, ] aliases: [] +mentions: [aegon-son-of-rhaegar, rhaenys-daughter-of-rhaegar] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Sack_of_King's_Landing diff --git a/content/battles/sack-of-meereen.md b/content/battles/sack-of-meereen.md index 25883db3..9205d444 100644 --- a/content/battles/sack-of-meereen.md +++ b/content/battles/sack-of-meereen.md @@ -21,6 +21,7 @@ commanders: [daenerys-targaryen, grey-worm, jorah-mormont, barristan-selmy] victor: "Daenerys Targaryen and the Unsullied" outcome: "Daenerys took Meereen by a rising of its own slaves, hanged the ruling Great Masters, and made the city her seat." aliases: [] +mentions: [daenerys-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Sack_of_Meereen diff --git a/content/battles/second-battle-of-tumbleton.md b/content/battles/second-battle-of-tumbleton.md index 6052c8c5..a6a968b5 100644 --- a/content/battles/second-battle-of-tumbleton.md +++ b/content/battles/second-battle-of-tumbleton.md @@ -26,6 +26,7 @@ commanders: outcome: "Addam Velaryon fell upon the encamped green host to redeem the dragonseeds, and in the ensuing carnage its leadership was gutted and the march on King's Landing broken." casualties: [addam-velaryon, ormund-hightower, jon-roxton] aliases: ["Second Tumbleton"] +mentions: [daeron-targaryen-son-of-viserys-i, targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Second_Battle_of_Tumbleton diff --git a/content/battles/second-blackfyre-rebellion.md b/content/battles/second-blackfyre-rebellion.md index c4998d8d..47f46934 100644 --- a/content/battles/second-blackfyre-rebellion.md +++ b/content/battles/second-blackfyre-rebellion.md @@ -22,6 +22,7 @@ commanders: [daemon-ii-blackfyre, brynden-rivers, gormon-peake] victor: "Iron Throne" outcome: "The plot to crown Daemon II collapsed when Bloodraven descended on Whitewalls, took the pretender captive, and beheaded the ringleaders." casualties: [gormon-peake] +mentions: [aerys-i-targaryen, blackfyre] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Second_Blackfyre_Rebellion diff --git a/content/battles/siege-of-riverrun.md b/content/battles/siege-of-riverrun.md index 6fdca35f..899f9511 100644 --- a/content/battles/siege-of-riverrun.md +++ b/content/battles/siege-of-riverrun.md @@ -25,6 +25,7 @@ commanders: [jaime-lannister, edmure-tully] victor: "House Lannister" outcome: "Jaime Lannister scattered Edmure Tully's host before the walls and ringed Riverrun with three camps, penning the Tullys in their own castle until Robb Stark broke the siege." aliases: ["The First Siege of Riverrun"] +mentions: [lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Siege_of_Riverrun diff --git a/content/battles/siege-of-storms-end.md b/content/battles/siege-of-storms-end.md index ab514355..6a4fb43c 100644 --- a/content/battles/siege-of-storms-end.md +++ b/content/battles/siege-of-storms-end.md @@ -25,6 +25,7 @@ commanders: [stannis-baratheon, mace-tyrell, paxter-redwyne] victor: "rebels" outcome: "Mace Tyrell besieges Storm's End for nearly a year but never breaks it; Stannis Baratheon's garrison, saved from starvation by an onion smuggler, holds until the war's end lifts the siege." aliases: [] +mentions: [stannis-baratheon, redwyne] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Siege_of_Storm's_End diff --git a/content/battles/skagosi-rebellion.md b/content/battles/skagosi-rebellion.md index 76e9d495..ba910ec4 100644 --- a/content/battles/skagosi-rebellion.md +++ b/content/battles/skagosi-rebellion.md @@ -20,6 +20,7 @@ participants: houses: [] victor: "House Stark" outcome: "The Skagosi rose against Winterfell and were brought back beneath the direwolf's rule by a King of Winter, though at great cost." +mentions: [stark] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Skagos diff --git a/content/battles/skirmish-at-the-tower-of-joy.md b/content/battles/skirmish-at-the-tower-of-joy.md index 6b813698..fc5482a8 100644 --- a/content/battles/skirmish-at-the-tower-of-joy.md +++ b/content/battles/skirmish-at-the-tower-of-joy.md @@ -23,6 +23,7 @@ victor: "rebels" outcome: "Eddard Stark and his companions overcome three knights of the Kingsguard guarding the Tower of Joy; only Ned and Howland Reed survive the clash." casualties: [arthur-dayne, oswell-whent, willam-dustin] aliases: [Battle of the Tower of Joy] +mentions: [lyanna-stark, barbrey-ryswell, rhaegar-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Tower_of_Joy diff --git a/content/battles/stark-wars-of-unification.md b/content/battles/stark-wars-of-unification.md index 6c991b5f..883d707f 100644 --- a/content/battles/stark-wars-of-unification.md +++ b/content/battles/stark-wars-of-unification.md @@ -21,6 +21,7 @@ commanders: [jon-stark, theon-stark, rickard-stark-the-laughing-wolf, rodrik-stark] victor: "House Stark" outcome: "Over many lifetimes the Kings of Winter broke or subdued every rival power in the North, uniting the whole of it under Winterfell." +mentions: [stark] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Kings_of_Winter diff --git a/content/battles/storm-king-reach-wars.md b/content/battles/storm-king-reach-wars.md index fc0d1b81..1713d8af 100644 --- a/content/battles/storm-king-reach-wars.md +++ b/content/battles/storm-king-reach-wars.md @@ -19,6 +19,7 @@ participants: - side: House Gardener houses: [gardener] outcome: "The Marches changed hands again and again over thousands of years, with neither realm able to hold them for long, until both fell at last to Aegon the Conqueror." +mentions: [mern-ix-gardener, argilac-durrandon] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Dornish_Marches diff --git a/content/battles/storming-of-pyke.md b/content/battles/storming-of-pyke.md index ca32e341..6c04e49c 100644 --- a/content/battles/storming-of-pyke.md +++ b/content/battles/storming-of-pyke.md @@ -25,6 +25,7 @@ commanders: [robert-baratheon, stannis-baratheon, balon-greyjoy] victor: "the Iron Throne" outcome: "Robert Baratheon's host stormed Pyke and broke the rebellion; Balon Greyjoy bent the knee and gave up his last surviving son, Theon, as a hostage and ward." casualties: [maron-greyjoy] +mentions: [rodrik-greyjoy, theon-greyjoy, stannis-baratheon] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Siege_of_Pyke diff --git a/content/battles/storming-of-the-dragonpit.md b/content/battles/storming-of-the-dragonpit.md index 7c95300d..e826d31a 100644 --- a/content/battles/storming-of-the-dragonpit.md +++ b/content/battles/storming-of-the-dragonpit.md @@ -24,6 +24,7 @@ participants: victor: "Smallfolk of King's Landing" outcome: "A crazed mob broke into the Dragonpit and slew the chained dragons, gutting Targaryen power and turning the capital against Queen Rhaenyra." aliases: ["Storming of the Dragonpit"] +mentions: [joffrey-velaryon, helaena-targaryen, rhaenyra-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Storming_of_the_Dragonpit diff --git a/content/battles/taking-and-recapture-of-moat-cailin.md b/content/battles/taking-and-recapture-of-moat-cailin.md index 069f0eda..ea0bc64f 100644 --- a/content/battles/taking-and-recapture-of-moat-cailin.md +++ b/content/battles/taking-and-recapture-of-moat-cailin.md @@ -25,6 +25,7 @@ commanders: [ramsay-bolton] victor: "House Bolton and House Frey" outcome: "The ironborn seized the ruined causeway fortress to choke the neck, then were tricked into yielding it to Ramsay Bolton, reopening the only overland road into the north." aliases: [] +mentions: [frey, bolton] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Moat_Cailin diff --git a/content/battles/the-butchers-ball.md b/content/battles/the-butchers-ball.md index f799949e..b1792f3e 100644 --- a/content/battles/the-butchers-ball.md +++ b/content/battles/the-butchers-ball.md @@ -23,6 +23,7 @@ victor: "blacks" outcome: "Ser Criston Cole's host was surrounded and slaughtered near the God's Eye, and the Kingmaker himself was cut down after refusing to yield." casualties: [criston-cole] aliases: ["Battle by the Lakeshore"] +mentions: [aegon-ii-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Butcher's_Ball diff --git a/content/battles/the-dragons-wroth.md b/content/battles/the-dragons-wroth.md index 1711a221..65be1d4c 100644 --- a/content/battles/the-dragons-wroth.md +++ b/content/battles/the-dragons-wroth.md @@ -21,6 +21,7 @@ participants: commanders: [aegon-i-targaryen, visenya-targaryen] victor: "Dorne" outcome: "Aegon and Visenya burn the castles of Dorne for three years in vengeance for Rhaenys, but the Dornish endure until a peace is agreed." +mentions: [rhaenys-targaryen, visenya-targaryen, yronwood] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Dragon%27s_Wroth diff --git a/content/battles/the-fishfeed.md b/content/battles/the-fishfeed.md index 84a5e209..8a37c390 100644 --- a/content/battles/the-fishfeed.md +++ b/content/battles/the-fishfeed.md @@ -21,6 +21,7 @@ participants: commanders: [roderick-dustin, benjicot-blackwood] victor: "blacks" outcome: "Three converging black hosts annihilated a leaderless westerman army, casting so many dead into the rivers that the fish grew fat." +mentions: [aegon-ii-targaryen, rhaenyra-targaryen, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Fishfeed diff --git a/content/battles/the-muddy-mess.md b/content/battles/the-muddy-mess.md index 2db2743d..6eb9245e 100644 --- a/content/battles/the-muddy-mess.md +++ b/content/battles/the-muddy-mess.md @@ -22,6 +22,7 @@ commanders: [borros-baratheon, benjicot-blackwood] victor: "blacks" outcome: "The river lords broke Lord Borros Baratheon's stormland host in the rain and mud, and his death ended the last organized green resistance." aliases: ["Battle at the Kingsroad"] +mentions: [aegon-ii-targaryen, frey] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Muddy_Mess diff --git a/content/battles/third-and-fourth-blackfyre-rebellions.md b/content/battles/third-and-fourth-blackfyre-rebellions.md index 4ba1dc66..9727d192 100644 --- a/content/battles/third-and-fourth-blackfyre-rebellions.md +++ b/content/battles/third-and-fourth-blackfyre-rebellions.md @@ -23,6 +23,7 @@ commanders: victor: "Iron Throne" outcome: "Both risings were crushed along the Dornish Marches, and neither Haegon nor Daemon III came near the crown their forebears had sought." casualties: [haegon-blackfyre, daemon-iii-blackfyre] +mentions: [aegon-v-targaryen, blackfyre] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Blackfyre_Rebellion diff --git a/content/battles/tristifer-iv-mudds-hundred-battles.md b/content/battles/tristifer-iv-mudds-hundred-battles.md index 1a408fba..2bb45994 100644 --- a/content/battles/tristifer-iv-mudds-hundred-battles.md +++ b/content/battles/tristifer-iv-mudds-hundred-battles.md @@ -22,6 +22,7 @@ commanders: [tristifer-iv-mudd] victor: "Andal invaders" outcome: "Tristifer IV won ninety-nine victories but lost his hundredth battle and his life; his heir could not hold the realm, and the kingdom of the rivers passed to the Andals." casualties: [tristifer-iv-mudd] +mentions: [mudd] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Tristifer_IV_Mudd diff --git a/content/battles/vulture-kings-uprising.md b/content/battles/vulture-kings-uprising.md index cd069974..c5e5d6cc 100644 --- a/content/battles/vulture-kings-uprising.md +++ b/content/battles/vulture-kings-uprising.md @@ -21,6 +21,7 @@ participants: commanders: [orys-baratheon, wyl-of-wyl] victor: "Iron Throne" outcome: "The first Vulture King's host is broken and scattered, though Orys Baratheon is taken by the Wyl of Wyl and loses a hand to ransom." +mentions: [caron, wyl, dondarrion] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Vulture_King diff --git a/content/battles/war-of-the-ninepenny-kings.md b/content/battles/war-of-the-ninepenny-kings.md index 704cd8b0..87653431 100644 --- a/content/battles/war-of-the-ninepenny-kings.md +++ b/content/battles/war-of-the-ninepenny-kings.md @@ -22,6 +22,7 @@ victor: "Iron Throne" outcome: "The realm broke the Band of Nine on the Stepstones, and Barristan Selmy slew Maelys the Monstrous, ending the male line of the Blackfyres." casualties: [maelys-i-blackfyre, ormund-baratheon] aliases: ["Fifth Blackfyre Rebellion"] +mentions: [jaehaerys-ii-targaryen, maelys-i-blackfyre, blackfyre] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/War_of_the_Ninepenny_Kings diff --git a/content/events/assassination-of-jon-snow.md b/content/events/assassination-of-jon-snow.md index 8b653623..d2e3fbdc 100644 --- a/content/events/assassination-of-jon-snow.md +++ b/content/events/assassination-of-jon-snow.md @@ -12,6 +12,7 @@ coords: x: 460 y: 108 outcome: "Sworn brothers put their knives in the Lord Commander for the Watch; he falls in the snow at Castle Black, his fate unwritten." +mentions: [ramsay-bolton] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Jon_Snow diff --git a/content/events/birth-of-the-dragons.md b/content/events/birth-of-the-dragons.md index 6b22077e..16e003a3 100644 --- a/content/events/birth-of-the-dragons.md +++ b/content/events/birth-of-the-dragons.md @@ -9,6 +9,7 @@ date: location: "The Dothraki sea" landmass: essos outcome: "Daenerys walks into Drogo's pyre and out of the ashes unburnt, with three living dragons, the first in a century and a half." +mentions: [jorah-mormont] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Daenerys_Targaryen diff --git a/content/events/death-of-jon-arryn.md b/content/events/death-of-jon-arryn.md index 107162ea..8e280d9f 100644 --- a/content/events/death-of-jon-arryn.md +++ b/content/events/death-of-jon-arryn.md @@ -12,6 +12,7 @@ coords: x: 590 y: 830 outcome: "The Hand of the King dies of a sudden fever (poison, in truth) and Robert rides north to make Eddard Stark his Hand." +mentions: [robert-baratheon] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Jon_Arryn diff --git a/content/events/death-of-king-robert.md b/content/events/death-of-king-robert.md index 2635a401..80d37cd6 100644 --- a/content/events/death-of-king-robert.md +++ b/content/events/death-of-king-robert.md @@ -9,6 +9,7 @@ date: location: "The Kingswood" landmass: westeros outcome: "A boar, strongwine, and a queen's design end the first Baratheon king; the peace he won at the Trident dies with him." +mentions: [robert-baratheon, cersei-lannister, lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Robert_I_Baratheon diff --git a/content/events/death-of-the-dragons.md b/content/events/death-of-the-dragons.md index 7fc1dea9..caa1de41 100644 --- a/content/events/death-of-the-dragons.md +++ b/content/events/death-of-the-dragons.md @@ -12,6 +12,7 @@ coords: x: 590 y: 830 outcome: "The last dragon dies stunted and sickly under Aegon III, and House Targaryen rules on by right and habit alone." +mentions: [aegon-iii-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Last_dragon diff --git a/content/events/death-of-tywin-lannister.md b/content/events/death-of-tywin-lannister.md index 813dd3fb..4884f029 100644 --- a/content/events/death-of-tywin-lannister.md +++ b/content/events/death-of-tywin-lannister.md @@ -12,6 +12,7 @@ coords: x: 592 y: 830 outcome: "Tyrion puts a crossbow bolt through his father on the privy and flees across the narrow sea; the lion's realm begins to unravel." +mentions: [oberyn-martell, tywin-lannister, jaime-lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Tywin_Lannister diff --git a/content/events/doom-of-valyria.md b/content/events/doom-of-valyria.md index 98d34246..bdf72422 100644 --- a/content/events/doom-of-valyria.md +++ b/content/events/doom-of-valyria.md @@ -9,6 +9,7 @@ date: location: "Valyria" landmass: essos outcome: "The Fourteen Flames erupt and the Freehold perishes in a day; the Lands of the Long Summer shatter into the Smoking Sea." +mentions: [aegon-i-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Doom_of_Valyria diff --git a/content/events/election-of-jon-snow.md b/content/events/election-of-jon-snow.md index 5b34cbd5..7c0f15ee 100644 --- a/content/events/election-of-jon-snow.md +++ b/content/events/election-of-jon-snow.md @@ -12,6 +12,7 @@ coords: x: 460 y: 108 outcome: "Ned Stark's bastard becomes the nine hundred and ninety-eighth Lord Commander of the Night's Watch, and lets the wildlings through the Wall." +mentions: [stannis-baratheon] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Jon_Snow diff --git a/content/events/execution-of-eddard-stark.md b/content/events/execution-of-eddard-stark.md index a1c0765e..14e7f81a 100644 --- a/content/events/execution-of-eddard-stark.md +++ b/content/events/execution-of-eddard-stark.md @@ -12,6 +12,7 @@ coords: x: 590 y: 830 outcome: "Joffrey takes Lord Stark's head on the steps of the Great Sept, and all hope of peace between wolf and lion with it." +mentions: [joffrey-baratheon, cersei-lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Eddard_Stark diff --git a/content/events/exile-of-jalabhar-xho.md b/content/events/exile-of-jalabhar-xho.md index 50452be8..2de8f007 100644 --- a/content/events/exile-of-jalabhar-xho.md +++ b/content/events/exile-of-jalabhar-xho.md @@ -9,6 +9,7 @@ date: location: "Red Flower Vale" landmass: summer-isles outcome: "The defeated Prince of Red Flower Vale flees to Robert's court, forever petitioning for swords to win back his vale." +mentions: [robert-baratheon] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Jalabhar_Xho diff --git a/content/events/exodus-of-house-targaryen.md b/content/events/exodus-of-house-targaryen.md index 77e589c3..f002df27 100644 --- a/content/events/exodus-of-house-targaryen.md +++ b/content/events/exodus-of-house-targaryen.md @@ -12,6 +12,7 @@ coords: x: 660 y: 870 outcome: "Warned by Daenys the Dreamer, the Targaryens quit Valyria for Dragonstone twelve years before the Doom, and alone of the dragonlords survive it." +mentions: [aegon-i-targaryen, daenys-targaryen, aenar-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Aenar_Targaryen diff --git a/content/events/fall-of-bran-stark.md b/content/events/fall-of-bran-stark.md index 114b7da1..d5b289c0 100644 --- a/content/events/fall-of-bran-stark.md +++ b/content/events/fall-of-bran-stark.md @@ -12,6 +12,7 @@ coords: x: 400 y: 430 outcome: "A boy who saw too much is thrown from a tower and lives, crippled; the things done to silence him set the realm alight." +mentions: [robert-baratheon] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Bran_Stark diff --git a/content/events/flight-from-daznaks-pit.md b/content/events/flight-from-daznaks-pit.md index 77d94aaf..9cec8aaf 100644 --- a/content/events/flight-from-daznaks-pit.md +++ b/content/events/flight-from-daznaks-pit.md @@ -9,6 +9,7 @@ date: location: "Meereen" landmass: essos outcome: "Drogon descends upon the fighting pit and carries the queen away to the Dothraki sea, while a slaver host closes on Meereen." +mentions: [daenerys-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Daznak%27s_Pit diff --git a/content/events/flight-of-the-last-targaryens.md b/content/events/flight-of-the-last-targaryens.md index cdc22059..0d4a890b 100644 --- a/content/events/flight-of-the-last-targaryens.md +++ b/content/events/flight-of-the-last-targaryens.md @@ -9,6 +9,7 @@ date: location: "Braavos and the Free Cities" landmass: essos outcome: "Viserys and the infant Daenerys are spirited across the narrow sea, beggar heirs of a fallen dynasty." +mentions: [robert-baratheon, viserys-iii-targaryen, rhaella-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Daenerys_Targaryen diff --git a/content/events/founding-of-kings-landing.md b/content/events/founding-of-kings-landing.md index 47b37f45..279b2468 100644 --- a/content/events/founding-of-kings-landing.md +++ b/content/events/founding-of-kings-landing.md @@ -12,6 +12,8 @@ coords: x: 590 y: 830 outcome: "Aegon is anointed by the Faith, the Iron Throne is forged from the swords of his enemies, and a city rises where he first came ashore." +mentions: + [aegon-i-targaryen, rhaenys-targaryen, visenya-targaryen, maegor-i-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/King%27s_Landing diff --git a/content/events/garth-greenhand-seeds-the-reach.md b/content/events/garth-greenhand-seeds-the-reach.md index 0d2b0730..26bf5127 100644 --- a/content/events/garth-greenhand-seeds-the-reach.md +++ b/content/events/garth-greenhand-seeds-the-reach.md @@ -9,6 +9,7 @@ date: location: "The Reach" landmass: westeros outcome: "The Reach flowers, and from Garth's many children spring the great houses of the south." +mentions: [gardener, rowan] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Garth_Greenhand diff --git a/content/events/golden-crown-of-viserys.md b/content/events/golden-crown-of-viserys.md index f89cbdca..1462487e 100644 --- a/content/events/golden-crown-of-viserys.md +++ b/content/events/golden-crown-of-viserys.md @@ -9,6 +9,7 @@ date: location: "Vaes Dothrak" landmass: essos outcome: "Drogo crowns the beggar king with molten gold in the sacred city; Daenerys watches, and knows he was no dragon." +mentions: [daenerys-targaryen, viserys-iii-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Viserys_Targaryen diff --git a/content/events/grant-of-the-new-gift.md b/content/events/grant-of-the-new-gift.md index 6f7e0429..7dc7942b 100644 --- a/content/events/grant-of-the-new-gift.md +++ b/content/events/grant-of-the-new-gift.md @@ -9,6 +9,7 @@ date: location: "The Wall" landmass: westeros outcome: "Good Queen Alysanne doubles the Night's Watch lands, the crowning kindness of the golden reign of Jaehaerys the Conciliator." +mentions: [jaehaerys-i-targaryen, alysanne-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/New_Gift diff --git a/content/events/great-council-of-101.md b/content/events/great-council-of-101.md index c6cb1cc2..b2f3c3a6 100644 --- a/content/events/great-council-of-101.md +++ b/content/events/great-council-of-101.md @@ -12,6 +12,13 @@ coords: x: 555 y: 765 outcome: "The lords choose Viserys over Rhaenys's line, fixing the precedent that the Iron Throne passes by the male line, the seed of the Dance." +mentions: + [ + viserys-i-targaryen, + jaehaerys-i-targaryen, + rhaenys-targaryen-queen-who-never-was, + baelon-targaryen, + ] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Great_Council_of_101 diff --git a/content/events/house-of-the-undying.md b/content/events/house-of-the-undying.md index daf5b40e..aced776e 100644 --- a/content/events/house-of-the-undying.md +++ b/content/events/house-of-the-undying.md @@ -9,6 +9,7 @@ date: location: "Qarth" landmass: essos outcome: "The warlocks' prophecies (three fires, three mounts, three treasons) are paid for in dragonflame, and Qarth is fled." +mentions: [daenerys-targaryen, rhaegar-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/House_of_the_Undying diff --git a/content/events/murder-of-renly-baratheon.md b/content/events/murder-of-renly-baratheon.md index 7b010bf2..dd8a0e30 100644 --- a/content/events/murder-of-renly-baratheon.md +++ b/content/events/murder-of-renly-baratheon.md @@ -12,6 +12,7 @@ coords: x: 645 y: 935 outcome: "A shadow with Stannis's face kills the king of summer in his own pavilion, and his hundred thousand melt away." +mentions: [stannis-baratheon] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Renly_Baratheon diff --git a/content/events/passage-of-the-sorrows.md b/content/events/passage-of-the-sorrows.md index fd4fc835..eb8587a5 100644 --- a/content/events/passage-of-the-sorrows.md +++ b/content/events/passage-of-the-sorrows.md @@ -9,6 +9,7 @@ date: location: "Chroyane" landmass: essos outcome: "The Shy Maid slips through the drowned ruin of Chroyane; the stone men attack, and Jon Connington takes the greyscale in silence." +mentions: [rhaegar-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Chroyane diff --git a/content/events/the-purple-wedding.md b/content/events/the-purple-wedding.md index 820ecf68..73cdfd25 100644 --- a/content/events/the-purple-wedding.md +++ b/content/events/the-purple-wedding.md @@ -12,6 +12,7 @@ coords: x: 590 y: 830 outcome: "King Joffrey is poisoned at his own wedding feast; Tyrion Lannister is seized for the murder, and Sansa Stark vanishes." +mentions: [tyrion-lannister, cersei-lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Joffrey_I_Baratheon diff --git a/content/events/the-red-comet.md b/content/events/the-red-comet.md index 5ce7cb40..a5585c60 100644 --- a/content/events/the-red-comet.md +++ b/content/events/the-red-comet.md @@ -9,6 +9,7 @@ date: location: "The skies of the world" landmass: westeros outcome: "A bleeding star spans the sky from Winterfell to Qarth, and every people reads in it the omen it desires." +mentions: [joffrey-baratheon] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Red_comet diff --git a/content/events/tragedy-at-summerhall.md b/content/events/tragedy-at-summerhall.md index 9e3b7900..53971af8 100644 --- a/content/events/tragedy-at-summerhall.md +++ b/content/events/tragedy-at-summerhall.md @@ -12,6 +12,7 @@ coords: x: 555 y: 1060 outcome: "Fire consumes the summer palace, King Aegon V, and Prince Duncan; Rhaegar Targaryen is born amid the smoke and grief." +mentions: [aegon-v-targaryen, rhaella-targaryen, rhaegar-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Tragedy_at_Summerhall diff --git a/content/events/union-of-dorne-and-the-iron-throne.md b/content/events/union-of-dorne-and-the-iron-throne.md index 778154de..d70be2ba 100644 --- a/content/events/union-of-dorne-and-the-iron-throne.md +++ b/content/events/union-of-dorne-and-the-iron-throne.md @@ -9,6 +9,7 @@ date: location: "Dorne and King's Landing" landmass: westeros outcome: "Dorne joins the realm by marriage rather than conquest, and the Seven Kingdoms are whole at last." +mentions: [daenerys-daughter-of-aegon-iv, daeron-i-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Daeron_II_Targaryen diff --git a/content/events/voyage-of-the-cinnamon-wind.md b/content/events/voyage-of-the-cinnamon-wind.md index b5a497bf..326b4902 100644 --- a/content/events/voyage-of-the-cinnamon-wind.md +++ b/content/events/voyage-of-the-cinnamon-wind.md @@ -9,6 +9,7 @@ date: location: "The Summer Sea" landmass: summer-isles outcome: "The swan ship of Tall Trees Town carries Samwell Tarly toward Oldtown; Maester Aemon Targaryen dies at sea, aged one hundred and two." +mentions: [aemon-targaryen-maester] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Cinnamon_Wind diff --git a/content/events/wedding-of-daenerys-and-hizdahr.md b/content/events/wedding-of-daenerys-and-hizdahr.md index 08bccdd8..cc9d6468 100644 --- a/content/events/wedding-of-daenerys-and-hizdahr.md +++ b/content/events/wedding-of-daenerys-and-hizdahr.md @@ -9,6 +9,7 @@ date: location: "Meereen" landmass: essos outcome: "The dragon queen weds a Ghiscari noble to buy peace for Meereen; the murders stop, and the fighting pits reopen." +mentions: [daenerys-targaryen] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Hizdahr_zo_Loraq diff --git a/content/events/wedding-of-robert-and-cersei.md b/content/events/wedding-of-robert-and-cersei.md index 210e979d..f0ee6e95 100644 --- a/content/events/wedding-of-robert-and-cersei.md +++ b/content/events/wedding-of-robert-and-cersei.md @@ -12,6 +12,7 @@ coords: x: 590 y: 830 outcome: "The stag weds the lion to bind Casterly Rock to the new dynasty; the realm settles into an uneasy, indebted peace." +mentions: [cersei-lannister] sources: - type: awoiaf url: https://awoiaf.westeros.org/index.php/Robert_I_Baratheon diff --git a/lib/content-integrity.test.ts b/lib/content-integrity.test.ts index a50384d6..caf9d41a 100644 --- a/lib/content-integrity.test.ts +++ b/lib/content-integrity.test.ts @@ -313,3 +313,27 @@ describe("content integrity", () => { expect(errors).toEqual([]); }); }); + +describe("event mentions", () => { + it("validates event mentions the same way as battle mentions", () => { + const errors = contentIntegrityErrors({ + ...emptyCollections(), + events: [ + { + body: "", + slug: "e", + frontmatter: EventSchema.parse({ + slug: "e", + name: "E", + type: "wedding", + date: LEGEND_DATE, + location: "nowhere", + landmass: "westeros", + mentions: ["no-such-entry"], + }), + }, + ], + }); + expect(errors).toEqual(["events/e.mentions: missing no-such-entry"]); + }); +}); diff --git a/lib/content-integrity.ts b/lib/content-integrity.ts index 745b4bb8..c3fd580f 100644 --- a/lib/content-integrity.ts +++ b/lib/content-integrity.ts @@ -224,7 +224,10 @@ function referenceErrors(collections: Collections): string[] { name: "events", entries: collections.events, targets, - rules: [{ to: "houses", read: participantHouses }], + rules: [ + { to: "houses", read: participantHouses }, + { to: "all", read: (fm) => ref("mentions", fm.mentions) }, + ], }), } satisfies Record; diff --git a/lib/content.test.ts b/lib/content.test.ts index 1533c84d..ef83ec87 100644 --- a/lib/content.test.ts +++ b/lib/content.test.ts @@ -65,7 +65,7 @@ describe("renderMarkdown", () => { surfaceForms: ["Catelyn Tully"], }, ], - selfSlug: null, + self: null, }, }); expect(html).toContain( diff --git a/lib/prose-links.test.ts b/lib/prose-links.test.ts index 9a0d51fc..fe841230 100644 --- a/lib/prose-links.test.ts +++ b/lib/prose-links.test.ts @@ -1,12 +1,30 @@ import { describe, it, expect } from "bun:test"; -import { buildProseLinkIndex, type ProseLinkIndex } from "@/lib/prose-links"; +import { + buildProseLinkIndex, + type ProseLinkIndex, + type ProseLinkKind, +} from "@/lib/prose-links"; import { renderMarkdown } from "@/lib/content"; -import type { Character, House, Weapon, Dragon } from "@/lib/schemas"; +import { + BattleSchema, + CastleSchema, + EventSchema, + type Battle, + type Castle, + type Character, + type Dragon, + type Event, + type House, + type Weapon, +} from "@/lib/schemas"; type CharacterFixture = { slug: string; frontmatter: Character }; type HouseFixture = { slug: string; frontmatter: House }; type WeaponFixture = { slug: string; frontmatter: Weapon }; type DragonFixture = { slug: string; frontmatter: Dragon }; +type CastleFixture = { slug: string; frontmatter: Castle }; +type BattleFixture = { slug: string; frontmatter: Battle }; +type EventFixture = { slug: string; frontmatter: Event }; function character( partial: Partial & Pick, @@ -83,9 +101,81 @@ const AERYS_II = character({ const STARK = house({ slug: "stark", name: "House Stark" }); const TARGARYEN = house({ slug: "targaryen", name: "House Targaryen" }); +const DATE = { year: 299, era: "AC", precision: "year" } as const; + +function castle(partial: { + slug: string; + name: string; + draft?: boolean; +}): CastleFixture { + const fm = CastleSchema.parse({ + slug: partial.slug, + name: partial.name, + type: "castle", + coords: { x: 100, y: 100 }, + draft: partial.draft ?? false, + }); + return { slug: fm.slug, frontmatter: fm }; +} + +function battle(partial: { + slug: string; + name: string; + aliases?: string[]; + draft?: boolean; +}): BattleFixture { + const fm = BattleSchema.parse({ + slug: partial.slug, + name: partial.name, + type: "battle", + start: DATE, + end: DATE, + aliases: partial.aliases ?? [], + draft: partial.draft ?? false, + }); + return { slug: fm.slug, frontmatter: fm }; +} + +function event(partial: { + slug: string; + name: string; + aliases?: string[]; + draft?: boolean; +}): EventFixture { + const fm = EventSchema.parse({ + slug: partial.slug, + name: partial.name, + type: "other", + date: DATE, + location: "Westeros", + landmass: "westeros", + aliases: partial.aliases ?? [], + draft: partial.draft ?? false, + }); + return { slug: fm.slug, frontmatter: fm }; +} + +const HARRENHAL = castle({ slug: "harrenhal", name: "Harrenhal" }); +const TWINS = castle({ slug: "the-twins", name: "The Twins" }); +const ASHFORD = castle({ slug: "ashford", name: "Ashford" }); +const PYKE = castle({ slug: "pyke", name: "Pyke" }); +const DARRY_CASTLE = castle({ slug: "darry", name: "Darry" }); +const DARRY_HOUSE = house({ slug: "darry", name: "House Darry" }); +const RED_WEDDING = battle({ slug: "red-wedding", name: "The Red Wedding" }); +const BATTLE_OF_ASHFORD = battle({ + slug: "battle-of-ashford", + name: "The Battle of Ashford", +}); +const STORMING_OF_PYKE = battle({ slug: "pyke", name: "The Storming of Pyke" }); +const DOOM = event({ + slug: "doom-of-valyria", + name: "The Doom of Valyria", + aliases: ["Doom"], +}); + function indexFor(args: { current: { - kind: "character" | "house" | "weapon" | "dragon"; + kind: ProseLinkKind; slug: string; mentions?: readonly string[]; }; @@ -93,6 +183,9 @@ function indexFor(args: { houses?: HouseFixture[]; weapons?: WeaponFixture[]; dragons?: DragonFixture[]; + castles?: CastleFixture[]; + battles?: BattleFixture[]; + events?: EventFixture[]; }): ProseLinkIndex { return buildProseLinkIndex({ allCharacters: args.characters ?? [ @@ -106,6 +199,9 @@ function indexFor(args: { allHouses: args.houses ?? [STARK, TARGARYEN], allWeapons: args.weapons ?? [], allDragons: args.dragons ?? [], + allCastles: args.castles ?? [], + allBattles: args.battles ?? [], + allEvents: args.events ?? [], current: { ...args.current, mentions: args.current.mentions ?? [] }, }); } @@ -342,6 +438,9 @@ describe("buildProseLinkIndex", () => { allHouses: [], allWeapons: [], allDragons: [], + allCastles: [], + allBattles: [], + allEvents: [], current: { kind: "character", slug: "self", mentions: ["rickard-stark"] }, }); const rickard = out.targets.find((t) => t.slug === "rickard-stark"); @@ -358,6 +457,9 @@ describe("buildProseLinkIndex", () => { allHouses: [STARK, TARGARYEN], allWeapons: [], allDragons: [], + allCastles: [], + allBattles: [], + allEvents: [], current: { kind: "house", slug: "self", mentions: ["stark"] }, }); const stark = out.targets.find((t) => t.slug === "stark"); @@ -405,6 +507,9 @@ describe("buildProseLinkIndex (weapons and dragons)", () => { allHouses: [], allWeapons: [{ slug: "blackfyre", frontmatter: weaponBase }], allDragons: [], + allCastles: [], + allBattles: [], + allEvents: [], current: { kind: "house", slug: "targaryen", mentions: [] }, }); const target = out.targets.find((t) => t.slug === "blackfyre"); @@ -418,6 +523,9 @@ describe("buildProseLinkIndex (weapons and dragons)", () => { allHouses: [], allWeapons: [], allDragons: [{ slug: "vhagar", frontmatter: dragonBase }], + allCastles: [], + allBattles: [], + allEvents: [], current: { kind: "house", slug: "targaryen", mentions: [] }, }); const target = out.targets.find((t) => t.slug === "vhagar"); @@ -431,8 +539,216 @@ describe("buildProseLinkIndex (weapons and dragons)", () => { allHouses: [], allWeapons: [{ slug: "blackfyre", frontmatter: weaponBase }], allDragons: [], + allCastles: [], + allBattles: [], + allEvents: [], current: { kind: "weapon", slug: "blackfyre", mentions: [] }, }); - expect(out.selfSlug).toBe("blackfyre"); + expect(out.self).toEqual({ kind: "weapon", slug: "blackfyre" }); + }); +}); + +describe("prose-links: castles, battles, and events", () => { + it("links a castle by name", async () => { + const index = indexFor({ + current: { kind: "battle", slug: "burning-of-harrenhal" }, + castles: [HARRENHAL], + }); + const html = await renderWith( + "Harren's host sheltered inside Harrenhal.", + index, + ); + expect(html).toContain('Harrenhal'); + }); + + it("links a castle written with a lowercase article", async () => { + const index = indexFor({ + current: { kind: "event", slug: "the-purple-wedding" }, + castles: [TWINS], + }); + const html = await renderWith("Robb Stark rode for the Twins.", index); + expect(html).toContain('the Twins'); + }); + + it("never links a castle whose name is also a house's short name", async () => { + const index = indexFor({ + current: { kind: "battle", slug: "sack-of-darry", mentions: ["darry"] }, + houses: [DARRY_HOUSE], + castles: [DARRY_CASTLE], + }); + const html = await renderWith( + "Lord Darry held the castle of Darry.", + index, + ); + expect(html).toContain('Darry'); + expect(html).not.toContain('href="/castles/darry/"'); + }); + + it("links a battle by its article-stripped name", async () => { + const index = indexFor({ + current: { kind: "character", slug: "robb-stark" }, + battles: [RED_WEDDING], + }); + const html = await renderWith("He was slain at the Red Wedding.", index); + expect(html).toContain( + 'the Red Wedding', + ); + }); + + it("links an event by name", async () => { + const index = indexFor({ + current: { kind: "house", slug: "targaryen" }, + events: [DOOM], + }); + const html = await renderWith( + "They fled before the Doom of Valyria.", + index, + ); + expect(html).toContain( + 'the Doom of Valyria', + ); + }); + + it("links an event by alias", async () => { + const index = indexFor({ + current: { kind: "house", slug: "targaryen" }, + events: [DOOM], + }); + const html = await renderWith( + "After the Doom, Dragonstone stood alone.", + index, + ); + expect(html).toContain('the Doom'); + }); + + it("prefers the battle over the castle inside the battle's own name", async () => { + const index = indexFor({ + current: { kind: "character", slug: "baelor-hightower" }, + castles: [ASHFORD], + battles: [BATTLE_OF_ASHFORD], + }); + const html = await renderWith("He fell at the Battle of Ashford.", index); + expect(html).toContain( + 'Battle of Ashford', + ); + expect(html).not.toContain('href="/castles/ashford/"'); + }); + + it("suppresses a battle page's link to itself", async () => { + const index = indexFor({ + current: { kind: "battle", slug: "red-wedding" }, + battles: [RED_WEDDING], + }); + const html = await renderWith("The Red Wedding was a massacre.", index); + expect(html).not.toContain('href="/battles/red-wedding/"'); + }); + + it("keys self-suppression by kind, so a castle sharing the page's slug still links", async () => { + const index = indexFor({ + current: { kind: "battle", slug: "pyke" }, + castles: [PYKE], + battles: [STORMING_OF_PYKE], + }); + const html = await renderWith("The walls of Pyke were breached.", index); + expect(html).toContain('Pyke'); + }); + + it("links a castle and a battle that share a slug once each", async () => { + const index = indexFor({ + current: { kind: "character", slug: "robert-baratheon" }, + castles: [PYKE], + battles: [STORMING_OF_PYKE], + }); + const html = await renderWith( + "The Storming of Pyke ended the rebellion, and Pyke was left in ruins.", + index, + ); + expect(html).toContain('The Storming of Pyke'); + expect(html).toContain('Pyke'); + }); + + it("skips draft castles, battles, and events", async () => { + const index = indexFor({ + current: { kind: "character", slug: "nobody" }, + castles: [ + castle({ slug: "ghost-keep", name: "Ghost Keep", draft: true }), + ], + battles: [ + battle({ slug: "ghost-fight", name: "The Ghost Fight", draft: true }), + ], + events: [ + event({ slug: "ghost-feast", name: "The Ghost Feast", draft: true }), + ], + }); + const html = await renderWith( + "Ghost Keep, the Ghost Fight, and the Ghost Feast.", + index, + ); + expect(html).not.toContain("href="); + }); + + it("emits article-stripped forms for castles, battles, and events", () => { + const out = buildProseLinkIndex({ + allCharacters: [], + allHouses: [], + allWeapons: [], + allDragons: [], + allCastles: [TWINS], + allBattles: [RED_WEDDING], + allEvents: [DOOM], + current: { kind: "battle", slug: "nobody", mentions: [] }, + }); + expect(out.targets.map((t) => [t.kind, t.href, t.surfaceForms])).toEqual([ + ["castle", "/castles/the-twins/", ["The Twins", "Twins"]], + ["battle", "/battles/red-wedding/", ["The Red Wedding", "Red Wedding"]], + [ + "event", + "/events/doom-of-valyria/", + ["The Doom of Valyria", "Doom", "Doom of Valyria"], + ], + ]); + }); +}); + +describe("prose-links: shared names", () => { + const ELDER = character({ + slug: "rhaenys-targaryen", + name: "Rhaenys Targaryen", + }); + const YOUNGER = character({ + slug: "rhaenys-targaryen-queen-who-never-was", + name: "Rhaenys Targaryen", + }); + + it("gives a shared name to the first entry when neither is mentioned", async () => { + const index = indexFor({ + current: { kind: "dragon", slug: "meleys" }, + characters: [ELDER, YOUNGER], + }); + const html = await renderWith( + "Claimed by Princess Rhaenys Targaryen.", + index, + ); + expect(html).toContain( + 'Rhaenys Targaryen', + ); + }); + + it("lets `mentions` pick the winner when two entries share a name", async () => { + const index = indexFor({ + current: { + kind: "dragon", + slug: "meleys", + mentions: ["rhaenys-targaryen-queen-who-never-was"], + }, + characters: [ELDER, YOUNGER], + }); + const html = await renderWith( + "Claimed by Princess Rhaenys Targaryen.", + index, + ); + expect(html).toContain( + 'Rhaenys Targaryen', + ); }); }); diff --git a/lib/prose-links.ts b/lib/prose-links.ts index ac5fb294..70b0de8f 100644 --- a/lib/prose-links.ts +++ b/lib/prose-links.ts @@ -1,21 +1,49 @@ import type { Plugin } from "unified"; import type { Root, Text, Link, Parent } from "mdast"; import { visitParents, SKIP } from "unist-util-visit-parents"; -import type { Character, House, Weapon, Dragon } from "@/lib/schemas"; +import type { + Battle, + Castle, + Character, + Dragon, + Event, + House, + Weapon, +} from "@/lib/schemas"; + +export type ProseLinkKind = + | "character" + | "house" + | "weapon" + | "dragon" + | "castle" + | "battle" + | "event"; export type ProseLinkTarget = { slug: string; - kind: "character" | "house" | "weapon" | "dragon"; + kind: ProseLinkKind; href: string; surfaceForms: string[]; }; export type ProseLinkIndex = { targets: ProseLinkTarget[]; - selfSlug: string | null; + self: { kind: ProseLinkKind; slug: string } | null; }; +const KIND_PATH = { + character: "characters", + house: "houses", + weapon: "weapons", + dragon: "dragons", + castle: "castles", + battle: "battles", + event: "events", +} as const satisfies Record; + const HOUSE_PREFIX = /^House\s+/i; +const ARTICLE_PREFIX = /^The\s+/; const SKIP_ANCESTOR_TYPES = new Set([ "link", "linkReference", @@ -34,6 +62,12 @@ function shortHouseName(name: string): string { return name.replace(HOUSE_PREFIX, ""); } +// The match is case-sensitive and prose writes "the Twins", so a name that +// carries its own article also needs the bare form. +function stripArticle(name: string): string { + return name.replace(ARTICLE_PREFIX, ""); +} + function uniqueOrdered(forms: string[]): string[] { const seen = new Set(); return forms.reduce((acc, f) => { @@ -44,85 +78,139 @@ function uniqueOrdered(forms: string[]): string[] { }, []); } +function targetKey(target: { kind: ProseLinkKind; slug: string }): string { + return `${target.kind}/${target.slug}`; +} + +function targetsOf({ + kind, + entries, + forms, + mentioned, +}: { + kind: ProseLinkKind; + entries: ReadonlyArray<{ frontmatter: T }>; + forms: (frontmatter: T) => string[]; + mentioned: ReadonlySet; +}): ProseLinkTarget[] { + // The first target to register a surface form keeps it, so an entry the + // page lists in `mentions` goes first: three characters are named + // "Rhaenys Targaryen", and only the page knows which one it means. + const ordered = entries.toSorted( + (a, b) => + Number(mentioned.has(b.frontmatter.slug)) - + Number(mentioned.has(a.frontmatter.slug)), + ); + return ordered.flatMap(({ frontmatter: fm }) => { + if (fm.draft) return []; + const surfaceForms = uniqueOrdered(forms(fm)); + if (surfaceForms.length === 0) return []; + return [ + { + slug: fm.slug, + kind, + href: `/${KIND_PATH[kind]}/${fm.slug}/`, + surfaceForms, + }, + ]; + }); +} + export function buildProseLinkIndex(args: { allCharacters: ReadonlyArray<{ slug: string; frontmatter: Character }>; allHouses: ReadonlyArray<{ slug: string; frontmatter: House }>; allWeapons: ReadonlyArray<{ slug: string; frontmatter: Weapon }>; allDragons: ReadonlyArray<{ slug: string; frontmatter: Dragon }>; + allCastles: ReadonlyArray<{ slug: string; frontmatter: Castle }>; + allBattles: ReadonlyArray<{ slug: string; frontmatter: Battle }>; + allEvents: ReadonlyArray<{ slug: string; frontmatter: Event }>; current: { - kind: "character" | "house" | "weapon" | "dragon"; + kind: ProseLinkKind; slug: string; mentions: readonly string[]; }; }): ProseLinkIndex { - const { allCharacters, allHouses, allWeapons, allDragons, current } = args; + const { + allCharacters, + allHouses, + allWeapons, + allDragons, + allCastles, + allBattles, + allEvents, + current, + } = args; const mentioned = new Set(current.mentions); - const characterTargets = allCharacters.flatMap((c) => { - const fm = c.frontmatter; - if (fm.placeholder || fm.draft) return []; - const forms = [fm.name, ...fm.aliases]; - if (mentioned.has(fm.slug)) forms.push(firstNameToken(fm.name)); - const surfaceForms = uniqueOrdered(forms); - if (surfaceForms.length === 0) return []; - return [ - { - slug: fm.slug, - kind: "character", - href: `/characters/${fm.slug}/`, - surfaceForms, - }, - ]; + const characterTargets = targetsOf({ + kind: "character", + entries: allCharacters, + mentioned, + forms: (fm) => { + if (fm.placeholder) return []; + const forms = [fm.name, ...fm.aliases]; + if (mentioned.has(fm.slug)) forms.push(firstNameToken(fm.name)); + return forms; + }, }); - const houseTargets = allHouses.flatMap((h) => { - const fm = h.frontmatter; - if (fm.draft) return []; - const forms = [fm.name]; - if (mentioned.has(fm.slug)) { - const short = shortHouseName(fm.name); - if (short && short !== fm.name) forms.push(short); - } - const surfaceForms = uniqueOrdered(forms); - if (surfaceForms.length === 0) return []; - return [ - { - slug: fm.slug, - kind: "house", - href: `/houses/${fm.slug}/`, - surfaceForms, - }, - ]; + const houseTargets = targetsOf({ + kind: "house", + entries: allHouses, + mentioned, + forms: (fm) => { + const forms = [fm.name]; + if (mentioned.has(fm.slug)) { + const short = shortHouseName(fm.name); + if (short && short !== fm.name) forms.push(short); + } + return forms; + }, }); - const weaponTargets = allWeapons.flatMap((w) => { - const fm = w.frontmatter; - if (fm.draft) return []; - const surfaceForms = uniqueOrdered([fm.name, ...fm.aliases]); - if (surfaceForms.length === 0) return []; - return [ - { - slug: fm.slug, - kind: "weapon", - href: `/weapons/${fm.slug}/`, - surfaceForms, - }, - ]; + const weaponTargets = targetsOf({ + kind: "weapon", + entries: allWeapons, + mentioned, + forms: (fm) => [fm.name, ...fm.aliases], }); - const dragonTargets = allDragons.flatMap((d) => { - const fm = d.frontmatter; - if (fm.draft) return []; - const surfaceForms = uniqueOrdered([fm.name, ...fm.aliases]); - if (surfaceForms.length === 0) return []; - return [ - { - slug: fm.slug, - kind: "dragon", - href: `/dragons/${fm.slug}/`, - surfaceForms, - }, - ]; + const dragonTargets = targetsOf({ + kind: "dragon", + entries: allDragons, + mentioned, + forms: (fm) => [fm.name, ...fm.aliases], + }); + + // A castle that shares its name with a house (Darry, Rosby, the Hightower) + // reads as the house or its lord in most sentences, and `mentions` cannot + // separate the two because both carry the same slug. Such castles never + // auto-link; an explicit markdown link in the body still does. + const houseShortNames = new Set( + allHouses.map((h) => shortHouseName(h.frontmatter.name)), + ); + const castleTargets = targetsOf({ + kind: "castle", + entries: allCastles, + mentioned, + forms: (fm) => { + const forms = [fm.name, stripArticle(fm.name)]; + return forms.some((f) => houseShortNames.has(f)) ? [] : forms; + }, + }); + + const battleTargets = targetsOf({ + kind: "battle", + entries: allBattles, + mentioned, + forms: (fm) => [fm.name, ...fm.aliases, stripArticle(fm.name)], + }); + + const eventTargets = targetsOf({ + kind: "event", + entries: allEvents, + mentioned, + forms: (fm) => [fm.name, ...fm.aliases, stripArticle(fm.name)], }); return { @@ -131,8 +219,11 @@ export function buildProseLinkIndex(args: { ...houseTargets, ...weaponTargets, ...dragonTargets, + ...castleTargets, + ...battleTargets, + ...eventTargets, ], - selfSlug: current.slug, + self: { kind: current.kind, slug: current.slug }, }; } @@ -143,13 +234,13 @@ function escapeRegex(s: string): string { type CompiledIndex = { pattern: RegExp; formToTarget: Map; - selfSlug: string | null; }; function compileIndex(index: ProseLinkIndex): CompiledIndex | null { + const selfKey = index.self ? targetKey(index.self) : null; const formToTarget = new Map(); const allForms = index.targets - .filter((t) => t.slug !== index.selfSlug) + .filter((t) => targetKey(t) !== selfKey) .reduce((acc, t) => { t.surfaceForms.forEach((f) => { if (formToTarget.has(f)) return; @@ -164,7 +255,7 @@ function compileIndex(index: ProseLinkIndex): CompiledIndex | null { "\\b(" + allForms.map(escapeRegex).join("|") + ")\\b", "g", ); - return { pattern, formToTarget, selfSlug: index.selfSlug }; + return { pattern, formToTarget }; } export function remarkProseLinks(index: ProseLinkIndex): Plugin<[], Root> { @@ -172,13 +263,13 @@ export function remarkProseLinks(index: ProseLinkIndex): Plugin<[], Root> { const compiled = compileIndex(index); return function transformer(tree: Root) { if (!compiled) return; - const usedSlugs = new Set(); + const usedKeys = new Set(); visitParents(tree, "text", (node: Text, ancestors: Parent[]) => { if (ancestors.some((a) => SKIP_ANCESTOR_TYPES.has(a.type))) return SKIP; const parent = ancestors[ancestors.length - 1]; if (!parent) return; - const replacements = scanText(node, compiled, usedSlugs); + const replacements = scanText(node, compiled, usedKeys); if (replacements === null) return; const idx = parent.children.indexOf(node as never); if (idx === -1) return; @@ -192,7 +283,7 @@ export function remarkProseLinks(index: ProseLinkIndex): Plugin<[], Root> { function scanText( node: Text, compiled: CompiledIndex, - usedSlugs: Set, + usedKeys: Set, ): (Text | Link)[] | null { const value = node.value; if (!value) return null; @@ -205,8 +296,8 @@ function scanText( const matched = match[1]; const target = compiled.formToTarget.get(matched); if (!target) continue; - if (target.slug === compiled.selfSlug) continue; - if (usedSlugs.has(target.slug)) continue; + const key = targetKey(target); + if (usedKeys.has(key)) continue; const start = match.index; const end = start + matched.length; if (start > lastIndex) { @@ -218,7 +309,7 @@ function scanText( title: null, children: [{ type: "text", value: matched }], }); - usedSlugs.add(target.slug); + usedKeys.add(key); lastIndex = end; produced = true; } diff --git a/lib/schemas.test.ts b/lib/schemas.test.ts index d68da74c..7bbcdbea 100644 --- a/lib/schemas.test.ts +++ b/lib/schemas.test.ts @@ -366,3 +366,33 @@ describe("DragonSchema", () => { expect(() => DragonSchema.parse(input)).toThrow(); }); }); + +describe("EventSchema prose-link fields", () => { + it("defaults aliases and mentions to empty arrays", () => { + const parsed = EventSchema.parse({ + slug: "doom-of-valyria", + name: "The Doom of Valyria", + type: "disaster", + date: { year: 102, era: "BC", precision: "year" }, + location: "Valyria", + landmass: "essos", + }); + expect(parsed.aliases).toEqual([]); + expect(parsed.mentions).toEqual([]); + }); + + it("keeps the aliases and mentions it is given", () => { + const parsed = EventSchema.parse({ + slug: "doom-of-valyria", + name: "The Doom of Valyria", + type: "disaster", + date: { year: 102, era: "BC", precision: "year" }, + location: "Valyria", + landmass: "essos", + aliases: ["Doom"], + mentions: ["targaryen"], + }); + expect(parsed.aliases).toEqual(["Doom"]); + expect(parsed.mentions).toEqual(["targaryen"]); + }); +}); diff --git a/lib/schemas.ts b/lib/schemas.ts index ad3eaa52..5bf4e0eb 100644 --- a/lib/schemas.ts +++ b/lib/schemas.ts @@ -217,6 +217,8 @@ export const EventSchema = z.object({ participants: z.array(ParticipantSchema).default([]), outcome: z.string().optional(), casualties: z.array(z.string()).default([]), + aliases: z.array(z.string()).default([]), + mentions: z.array(z.string()).default([]), sources: z.array(SourceSchema).default([]), draft: z.boolean().default(false), }); diff --git a/lib/timeline.test.ts b/lib/timeline.test.ts index 379c72e3..e372e20e 100644 --- a/lib/timeline.test.ts +++ b/lib/timeline.test.ts @@ -65,6 +65,8 @@ const makeEvent = ({ landmass, participants: [], casualties: [], + aliases: [], + mentions: [], sources: [], draft: false, });