feat: adapt Exile-ui for linux
This commit is contained in:
129
src/lib/markup.ts
Normal file
129
src/lib/markup.ts
Normal file
@ -0,0 +1,129 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user