diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/wmap.js | 250 |
1 files changed, 247 insertions, 3 deletions
diff --git a/src/wmap.js b/src/wmap.js index 6e9606e..4a07269 100644 --- a/src/wmap.js +++ b/src/wmap.js @@ -2,6 +2,242 @@ import { parse } from "wmap-parser"; import { renderToSVG, StageType } from "wmap-renderer-svg"; import DOMPurify from "isomorphic-dompurify"; +//////////////////////////////////////////////////////////////////////////////// +// Sourcing //////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// Walks the lineup of items preceding $item and collects each +// .wmap-source's already-expanded text. Returns [] outside the wiki +// (no lineup to walk). +function lineup($item) { + if (typeof wiki === "undefined") return []; + const results = []; + const candidates = $(".item:lt(" + $(".item").index($item) + ")"); + const sources = candidates.filter(".wmap-source"); + for (const div of sources) { + results.push(div.wmapData()); + } + return results; +} + +// Replaces every bare "LINEUP" line with the concatenated text of every +// .wmap-source item earlier in the lineup, then returns the joined text. +// Each upstream wmapData() has already done its own expansion, so the +// recursion is implicit. The wmap-parser is tolerant of duplicates and +// unknown lines, so we don't pre-parse or dedupe here. +function expandLineup(text, $item) { + return text + .split("\n") + .map((line) => + line.trim() === "LINEUP" ? lineup($item).join("\n") : line, + ) + .join("\n"); +} + +// Finds the name for this map by walking the DOM to the nearest preceding +// heading on the same page. When multiple .wmap blocks share that +// heading, suffixes " #2", " #3", … in document order. +function findName($element) { + const page = $element.closest(".page"); + if (!page) return "Unknown"; + + const headings = page.querySelectorAll("h1, h2, h3, h4, h5, h6"); + let closestHeading = null; + for (const heading of headings) { + const elementFollowsHeading = + heading.compareDocumentPosition($element) & + Node.DOCUMENT_POSITION_FOLLOWING; + if (elementFollowsHeading) closestHeading = heading; + } + if (!closestHeading) return "Unknown"; + + const baseName = closestHeading.textContent.trim(); + const wmaps = page.querySelectorAll(".wmap.item"); + let position = 0; + for (const wmap of wmaps) { + const wmapFollowsHeading = + closestHeading.compareDocumentPosition(wmap) & + Node.DOCUMENT_POSITION_FOLLOWING; + if (!wmapFollowsHeading) continue; + position++; + if (wmap === $element) break; + } + return position <= 1 ? baseName : `${baseName} #${position}`; +} + +// Default stage boundaries (in 0-1 component-coordinate units), matching +// wmap-renderer-svg's defaults [25, 50, 75] divided by 100. +const kDefaultStageBoundaries = [0.25, 0.5, 0.75]; +const kStageNameToIndex = { i: 0, ii: 1, iii: 2 }; + +// Returns [b0, b1, b2] — the upper x boundaries of stages 1, 2, and 3. +// Falls back to defaults for any boundary the parser didn't override. +function stageBoundaries(map) { + const boundaries = [...kDefaultStageBoundaries]; + for (const override of map.stages) { + const index = kStageNameToIndex[override.stage]; + if (index !== undefined) boundaries[index] = override.value; + } + return boundaries; +} + +// Maps an x coordinate to its 1-indexed evolution stage. +function stageFor(x, boundaries) { + if (x < boundaries[0]) return 1; + if (x < boundaries[1]) return 2; + if (x < boundaries[2]) return 3; + return 4; +} + +// Pre-built per-stage description tables keyed by StageType.name. Stage N's +// table is at index N-1. +const kStageDescriptions = [1, 2, 3, 4].map((stage) => { + const descriptions = {}; + for (const stageType of Object.values(StageType)) { + descriptions[stageType.name] = stageType.stages[stage - 1]; + } + return descriptions; +}); + +// Creates the aspect data type out of the existing text. +// tAspectData := { +// +name: String, +// +graph: tAspectGraph +// } +// tAspectGraph := { +// nodes: Array<tAspectNode>, +// rels: Array<tAspectRelation> +// } +// tAspectNode := { +// type: String, +// in: Array<Number>, +// out: Array<Number>, +// props: Object<String, Any> +// } +// tAspectRelation := { +// type: String, +// from: Number, +// to: Number, +// props: Object<String, Any> +// } +function aspectData($element, text) { + const $item = $($element); + const map = parse(expandLineup(text, $item)); + const boundaries = stageBoundaries(map); + + const aspectNodes = []; + const rels = []; + const componentIndexByName = new Map(); + + for (const component of map.components) { + const index = aspectNodes.length; + componentIndexByName.set(component.label, index); + const stage = stageFor(component.coordinates[0], boundaries); + aspectNodes.push({ + type: "component", + in: [], + out: [], + props: { + name: component.label, + position: component.coordinates, + evolutionStage: stage, + ...kStageDescriptions[stage - 1], + }, + }); + } + + for (const dep of map.dependencies) { + const from = componentIndexByName.get(dep.from); + const to = componentIndexByName.get(dep.to); + if (from === undefined || to === undefined) continue; + + const forwardIndex = rels.length; + rels.push({ type: "depends on", from, to, props: {} }); + aspectNodes[from].out.push(forwardIndex); + aspectNodes[to].in.push(forwardIndex); + + if (!dep.isDirected) { + const reverseIndex = rels.length; + rels.push({ + type: "depends on", + from: to, + to: from, + props: {}, + }); + aspectNodes[to].out.push(reverseIndex); + aspectNodes[from].in.push(reverseIndex); + } + } + + for (const inertia of map.inertias) { + const targetIndex = componentIndexByName.get(inertia.component); + if (targetIndex === undefined) continue; + const inertiaIndex = aspectNodes.length; + aspectNodes.push({ + type: "inertia", + in: [], + out: [], + props: { name: inertia.component }, + }); + const relIndex = rels.length; + rels.push({ + type: "obstructs", + from: inertiaIndex, + to: targetIndex, + props: {}, + }); + aspectNodes[inertiaIndex].out.push(relIndex); + aspectNodes[targetIndex].in.push(relIndex); + } + + for (const evolution of map.evolutions) { + const targetIndex = componentIndexByName.get(evolution.component); + if (targetIndex === undefined) continue; + const [targetX, targetY] = aspectNodes[targetIndex].props.position; + const newX = targetX + evolution.value; + const stage = stageFor(newX, boundaries); + const evolutionIndex = aspectNodes.length; + aspectNodes.push({ + type: "evolution of", + in: [], + out: [], + props: { + name: evolution.component, + position: [newX, targetY], + evolutionStage: stage, + ...kStageDescriptions[stage - 1], + }, + }); + const relIndex = rels.length; + rels.push({ + type: "evolves from", + from: evolutionIndex, + to: targetIndex, + props: {}, + }); + aspectNodes[evolutionIndex].out.push(relIndex); + aspectNodes[targetIndex].in.push(relIndex); + } + + return [ + { + name: findName($element), + graph: { nodes: aspectNodes, rels }, + }, + ]; +} + +// Returns this map's text with every LINEUP expanded, ready for upstream +// consumers to splice in. Kept as a string (not a parsed map) so the +// downstream wmap-parser only runs once at the top of the chain. +function wmapData(item, $item) { + return expandLineup(item.text, $item); +} + +//////////////////////////////////////////////////////////////////////////////// +// Internals /////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + const internals = { kStyleSheet: "/plugins/wmap/wmap.css", kDimensions: { @@ -142,7 +378,7 @@ const bind = async function bind($item, item) { case "download": { let imageData = await internals.renderImage( - item.text, + expandLineup(item.text, $item), internals.kDimensions.full, ); const slug = $item.parents(".page").attr("id"); @@ -165,7 +401,7 @@ const bind = async function bind($item, item) { } let imageData = await internals.renderImage( - item.text, + expandLineup(item.text, $item), internals.kDimensions.full, ); @@ -195,8 +431,16 @@ const bind = async function bind($item, item) { }); try { + // Register the source hooks before rendering so any sibling + // binding concurrently can resolve this item via lineup(). + const root = $item.get(0); + root.classList.add("aspect-source"); + root.classList.add("wmap-source"); + root.aspectData = () => aspectData(root, item.text); + root.wmapData = () => wmapData(item, $item); + let thumbnailData = await internals.renderImage( - item.text, + expandLineup(item.text, $item), internals.kDimensions.thumbnail, ); $item.find(".viewer").html(` |