/** * Composer 指令高亮:textarea 下叠镜像层,悬停预览替换效果(QQ 式)。 */ import { findDirectiveHits, renderDirectiveBackdropHtml, } from "./directive-parse.js"; function escapeHtml(text) { return String(text) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function ensureTip() { let tip = document.getElementById("directive-preview-tip"); if (tip) return tip; tip = document.createElement("div"); tip.id = "directive-preview-tip"; tip.className = "directive-preview-tip"; tip.hidden = true; tip.setAttribute("role", "tooltip"); document.body.appendChild(tip); return tip; } function hideTip() { const tip = document.getElementById("directive-preview-tip"); if (!tip) return; tip.hidden = true; tip.innerHTML = ""; } /** * @param {HTMLTextAreaElement} input * @param {{ getPersona?: () => { name?: string, description?: string } | null }} [opts] */ export function wireDirectiveHighlight(input, opts = {}) { if (!input || input.dataset.directiveHl === "1") return; input.dataset.directiveHl = "1"; const field = input.closest(".composer-input-field"); const backdrop = field?.querySelector(".composer-input-backdrop"); if (!field || !backdrop) return; const getPersona = opts.getPersona || (() => null); let tipVisible = false; function syncEmptyClass() { input.classList.toggle("is-empty", !String(input.value ?? "")); } function refresh() { const text = String(input.value ?? ""); syncEmptyClass(); const hits = findDirectiveHits(text, { persona: getPersona() }); backdrop.innerHTML = renderDirectiveBackdropHtml(text, hits, escapeHtml); // 末尾换行时 textarea 多出一行高度,镜像补一个 br 对齐 if (text.endsWith("\n")) { backdrop.appendChild(document.createElement("br")); } backdrop.scrollTop = input.scrollTop; backdrop.scrollLeft = input.scrollLeft; } function showTipFor(mark, clientX, clientY) { const tip = ensureTip(); const title = mark.getAttribute("data-dir-title") || ""; const body = mark.getAttribute("data-dir-body") || ""; tip.innerHTML = `