130 lines
4.2 KiB
TypeScript
130 lines
4.2 KiB
TypeScript
import type { Area } from "./api";
|
|
|
|
// Named colors used by the guide markup; anything else that is 3/6 hex chars
|
|
// is treated as a hex color.
|
|
const COLOR_NAMES: Record<string, string> = {
|
|
red: "#e05555",
|
|
lime: "#7CFC00",
|
|
aqua: "#55e0e0",
|
|
yellow: "#e0c84a",
|
|
green: "#5fbf5f",
|
|
white: "#ffffff",
|
|
orange: "#e08a3c",
|
|
};
|
|
|
|
function resolveColor(name: string): string {
|
|
if (COLOR_NAMES[name]) return COLOR_NAMES[name];
|
|
if (/^[0-9a-fA-F]{6}$/.test(name) || /^[0-9a-fA-F]{3}$/.test(name)) return "#" + name;
|
|
return "inherit";
|
|
}
|
|
|
|
function escapeHtml(s: string): string {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
|
|
// Icon names available under static/icons/leveling/ (from the original AHK
|
|
// "leveling tracker" assets). Only these render as real <img>; anything else
|
|
// falls back to a text chip so unknown markup never shows a broken image.
|
|
const ICONS = new Set([
|
|
"0", "1", "2", "3", "4", "5", "6", "7",
|
|
"arena", "artificer", "b-rune", "checkpoint", "craft", "exa", "flasks",
|
|
"gcp", "hideout", "in-out", "in-out2", "jeweller", "lab", "portal",
|
|
"quest", "quest_2", "regal", "ring", "rune", "skill", "skill2", "skip",
|
|
"spirit", "spirit2", "support", "support2", "town", "waypoint",
|
|
]);
|
|
|
|
// Underscores stand in for spaces in plain guide text.
|
|
function plain(text: string, color: string): string {
|
|
const t = escapeHtml(text.replace(/_/g, " "));
|
|
if (!t) return "";
|
|
if (color === "inherit") return t;
|
|
return `<span style="color:${color}">${t}</span>`;
|
|
}
|
|
|
|
export interface RenderedLine {
|
|
html: string;
|
|
kind: "normal" | "hint" | "optional" | "info";
|
|
indent: number;
|
|
}
|
|
|
|
const TOKEN_RE = /\(([a-z]+)(?::([^)]*))?\)|areaid([a-z0-9_]+)/gi;
|
|
|
|
/** Render a single guide line (with embedded markup) into HTML + metadata. */
|
|
export function renderLine(raw: string, areas: Record<string, Area>): RenderedLine {
|
|
let line = raw;
|
|
|
|
// Classify the line for styling.
|
|
let kind: RenderedLine["kind"] = "normal";
|
|
let indent = 0;
|
|
const lower = line.toLowerCase();
|
|
if (lower.startsWith("(hint)")) {
|
|
kind = "hint";
|
|
line = line.slice("(hint)".length);
|
|
// leading underscores after (hint) indicate indentation
|
|
const m = line.match(/^_+/);
|
|
if (m) {
|
|
indent = 1;
|
|
line = line.slice(m[0].length);
|
|
}
|
|
} else if (lower.startsWith("optional:")) {
|
|
kind = "optional";
|
|
} else if (lower.includes("info:")) {
|
|
kind = "info";
|
|
}
|
|
|
|
// Pull out the trailing " ;; area name" annotation.
|
|
let areaName: string | null = null;
|
|
const sc = line.indexOf(";;");
|
|
if (sc >= 0) {
|
|
areaName = line.slice(sc + 2).trim();
|
|
line = line.slice(0, sc).trim();
|
|
}
|
|
|
|
let out = "";
|
|
let color = "inherit";
|
|
let last = 0;
|
|
let m: RegExpExecArray | null;
|
|
TOKEN_RE.lastIndex = 0;
|
|
while ((m = TOKEN_RE.exec(line)) !== null) {
|
|
out += plain(line.slice(last, m.index), color);
|
|
last = m.index + m[0].length;
|
|
|
|
const kindTok = m[1]?.toLowerCase();
|
|
const val = m[2] ?? "";
|
|
const areaId = m[3];
|
|
|
|
if (areaId !== undefined) {
|
|
const name = areaName || areas[areaId]?.name || areaId.replace(/_/g, " ");
|
|
out += `<span class="area">${escapeHtml(name)}</span>`;
|
|
areaName = null; // consume once
|
|
} else if (kindTok === "color") {
|
|
color = resolveColor(val.trim());
|
|
} else if (kindTok === "img") {
|
|
const name = val.trim();
|
|
const label = name.replace(/_/g, " ");
|
|
if (ICONS.has(name.toLowerCase())) {
|
|
out += `<img class="icon" src="/icons/leveling/${encodeURIComponent(
|
|
name.toLowerCase()
|
|
)}.png" alt="${escapeHtml(label)}" title="${escapeHtml(label)}" />`;
|
|
} else {
|
|
out += `<span class="chip img" data-img="${escapeHtml(name)}">${escapeHtml(
|
|
label
|
|
)}</span>`;
|
|
}
|
|
} else if (kindTok === "quest") {
|
|
out += `<span class="chip quest">${escapeHtml(val.replace(/_/g, " "))}</span>`;
|
|
} else if (kindTok === "emph") {
|
|
// ignored: treated as a plain emphasis marker
|
|
}
|
|
// (hint) handled at line level above
|
|
}
|
|
out += plain(line.slice(last), color);
|
|
|
|
// Any leftover area name annotation with no areaid token: append it.
|
|
if (areaName) {
|
|
out += ` <span class="area">${escapeHtml(areaName)}</span>`;
|
|
}
|
|
|
|
return { html: out, kind, indent };
|
|
}
|