+199
@@ -0,0 +1,199 @@
|
||||
/** Optional Markdown display — escaped first, then a small safe subset. */
|
||||
|
||||
const STORAGE_KEY = "wa-markdown-render";
|
||||
|
||||
export function getStoredMarkdownRender() {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isMarkdownRenderEnabled() {
|
||||
return document.documentElement.getAttribute("data-markdown-render") === "1";
|
||||
}
|
||||
|
||||
export function applyMarkdownRender(enabled) {
|
||||
const on = Boolean(enabled);
|
||||
document.documentElement.setAttribute("data-markdown-render", on ? "1" : "0");
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, on ? "1" : "0");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return on;
|
||||
}
|
||||
|
||||
export function initMarkdownRender() {
|
||||
return applyMarkdownRender(getStoredMarkdownRender());
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
/** Safe subset: fences, inline code, headings, emphasis, links, lists, quotes, hr, breaks. */
|
||||
export function renderMarkdownToHtml(raw) {
|
||||
const text = String(raw ?? "");
|
||||
if (!text) return "";
|
||||
|
||||
const fences = [];
|
||||
let src = text.replace(/```([a-zA-Z0-9_-]*)\n?([\s\S]*?)```/g, (_, lang, code) => {
|
||||
const i = fences.length;
|
||||
const langClass = lang ? ` class="language-${esc(lang)}"` : "";
|
||||
fences.push(
|
||||
`<pre class="md-pre"><code${langClass}>${esc(code.replace(/\n$/, ""))}</code></pre>`,
|
||||
);
|
||||
return `\u0000FENCE${i}\u0000`;
|
||||
});
|
||||
|
||||
const lines = src.split(/\n/);
|
||||
const out = [];
|
||||
let i = 0;
|
||||
let para = [];
|
||||
let listType = null; // "ul" | "ol"
|
||||
let listItems = [];
|
||||
|
||||
const flushPara = () => {
|
||||
if (!para.length) return;
|
||||
const body = para.map(inlineMarkdown).join("<br>");
|
||||
out.push(`<p class="md-p">${body}</p>`);
|
||||
para = [];
|
||||
};
|
||||
|
||||
const flushList = () => {
|
||||
if (!listType || !listItems.length) {
|
||||
listType = null;
|
||||
listItems = [];
|
||||
return;
|
||||
}
|
||||
const tag = listType;
|
||||
out.push(
|
||||
`<${tag} class="md-${tag}">${listItems
|
||||
.map((item) => `<li>${inlineMarkdown(item)}</li>`)
|
||||
.join("")}</${tag}>`,
|
||||
);
|
||||
listType = null;
|
||||
listItems = [];
|
||||
};
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
const fenceMatch = line.match(/^\u0000FENCE(\d+)\u0000$/);
|
||||
if (fenceMatch) {
|
||||
flushPara();
|
||||
flushList();
|
||||
out.push(fences[Number(fenceMatch[1])] ?? "");
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\s*([-*_])\1{2,}\s*$/.test(line)) {
|
||||
flushPara();
|
||||
flushList();
|
||||
out.push('<hr class="md-hr">');
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (heading) {
|
||||
flushPara();
|
||||
flushList();
|
||||
const level = heading[1].length;
|
||||
out.push(`<h${level} class="md-h">${inlineMarkdown(heading[2])}</h${level}>`);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const bq = line.match(/^>\s?(.*)$/);
|
||||
if (bq) {
|
||||
flushPara();
|
||||
flushList();
|
||||
const quoteLines = [];
|
||||
while (i < lines.length) {
|
||||
const m = lines[i].match(/^>\s?(.*)$/);
|
||||
if (!m) break;
|
||||
quoteLines.push(m[1]);
|
||||
i += 1;
|
||||
}
|
||||
out.push(
|
||||
`<blockquote class="md-quote">${quoteLines
|
||||
.map((q) => `<p class="md-p">${inlineMarkdown(q)}</p>`)
|
||||
.join("")}</blockquote>`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ul = line.match(/^\s*[-*+]\s+(.+)$/);
|
||||
if (ul) {
|
||||
flushPara();
|
||||
if (listType && listType !== "ul") flushList();
|
||||
listType = "ul";
|
||||
listItems.push(ul[1]);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const ol = line.match(/^\s*\d+\.\s+(.+)$/);
|
||||
if (ol) {
|
||||
flushPara();
|
||||
if (listType && listType !== "ol") flushList();
|
||||
listType = "ol";
|
||||
listItems.push(ol[1]);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.trim()) {
|
||||
flushPara();
|
||||
flushList();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
flushList();
|
||||
para.push(line);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
flushPara();
|
||||
flushList();
|
||||
return out.join("") || esc(text).replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function inlineMarkdown(line) {
|
||||
let s = esc(line);
|
||||
|
||||
const codes = [];
|
||||
s = s.replace(/`([^`]+)`/g, (_, code) => {
|
||||
const i = codes.length;
|
||||
codes.push(`<code class="md-code">${code}</code>`);
|
||||
return `\u0000CODE${i}\u0000`;
|
||||
});
|
||||
|
||||
s = s.replace(
|
||||
/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
|
||||
(_, label, url) =>
|
||||
`<a class="md-a" href="${esc(url)}" target="_blank" rel="noopener noreferrer">${label}</a>`,
|
||||
);
|
||||
|
||||
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||
s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
||||
s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||
s = s.replace(/_([^_]+)_/g, "<em>$1</em>");
|
||||
s = s.replace(/~~([^~]+)~~/g, "<del>$1</del>");
|
||||
|
||||
s = s.replace(/\u0000CODE(\d+)\u0000/g, (_, i) => codes[Number(i)] ?? "");
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Plain body: markdown when enabled, else escaped newlines. */
|
||||
export function formatBodyHtml(text) {
|
||||
if (isMarkdownRenderEnabled()) return renderMarkdownToHtml(text);
|
||||
return esc(text).replace(/\n/g, "<br>");
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
* P3 header / footer / aside — 辅助,紧凑
|
||||
*/
|
||||
|
||||
import { formatBodyHtml, isMarkdownRenderEnabled } from "./markdown.js";
|
||||
|
||||
export const PRESENT_SHELL_IDS = [
|
||||
"prose",
|
||||
"chat_monitor",
|
||||
@@ -132,6 +134,10 @@ function regionInnerHtml(region, rawValue, esc) {
|
||||
if (chips) return chips;
|
||||
}
|
||||
const text = formatBlockContent(rawValue).trim();
|
||||
if (!text) return `<div class="present-region-body"></div>`;
|
||||
if (isMarkdownRenderEnabled()) {
|
||||
return `<div class="present-region-body">${formatBodyHtml(text)}</div>`;
|
||||
}
|
||||
const paras = text.split(/\n\n+/).filter(Boolean);
|
||||
const inner = paras
|
||||
.map((p) => `<p>${esc(p).replace(/\n/g, "<br>")}</p>`)
|
||||
|
||||
Reference in New Issue
Block a user