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 = { 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, ">"); } // Icon names available under static/icons/leveling/ (from the original AHK // "leveling tracker" assets). Only these render as real ; 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 `${t}`; } 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): 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 += `${escapeHtml(name)}`; 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 += `${escapeHtml(label)}`; } else { out += `${escapeHtml( label )}`; } } else if (kindTok === "quest") { out += `${escapeHtml(val.replace(/_/g, " "))}`; } 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 += ` ${escapeHtml(areaName)}`; } return { html: out, kind, indent }; }