aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/wmap.js250
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(`