mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-24 12:54:48 -06:00
fix: restore safe HTML element rendering and suppress plantuml warning (#70)
* fix: restore safe HTML element rendering and suppress plantuml warning - Add safe HTML tag allowlist in inlineMarkdown: br, hr, kbd, mark, sub, sup, ins, wbr, details, summary, abbr, small, u, s (attribute-free only — XSS safe, tags with attributes stay escaped) - Add <details>/<summary> block-level protection pass with recursive markdown rendering of inner content - Add plantuml to _NO_HIGHLIGHT_LANGS (suppresses highlight.js warning for unsupported language) - CSS for details (collapsible, overflow hidden), kbd (mono font, key style), mark (yellow-glow token for theme adaptation) * fix: restrict safe tags to inline-only, broaden details regex - Remove hr, details, summary from inline _SAFE_TAGS allowlist (they are block-level and produce invalid HTML inside <p> wrappers) - Make <details> regex newline-optional so same-line <details><summary>Title</summary> patterns are captured
This commit is contained in:
@@ -3,9 +3,18 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inline formatting
|
||||
// ---------------------------------------------------------------------------
|
||||
// Safe inline HTML tags allowed through escapeHtml (no attributes — XSS safe)
|
||||
// Block-level tags (details, summary, hr) handled by their own protection passes
|
||||
var _SAFE_TAGS =
|
||||
/<(\/?(?:br|kbd|mark|sub|sup|ins|wbr|abbr|small|u|s))(?:\s*\/?)>/gi;
|
||||
|
||||
function inlineMarkdown(text) {
|
||||
// Escape HTML first so only tags we generate are real
|
||||
text = escapeHtml(text);
|
||||
// Restore safe HTML tags (attribute-free only — already escaped so no XSS)
|
||||
text = text.replace(_SAFE_TAGS, function (m, tag) {
|
||||
return "<" + tag + ">";
|
||||
});
|
||||
// Bold (asterisks only — underscores cause false positives on snake_case)
|
||||
text = text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
// Italic (asterisks only)
|
||||
@@ -304,6 +313,30 @@ function renderMarkdown(text) {
|
||||
return "\x00CB" + (codeBlocks.length - 1) + "\x00";
|
||||
});
|
||||
|
||||
// Protect <details> blocks (safe HTML — attribute-free only)
|
||||
var detailsBlocks = [];
|
||||
text = text.replace(
|
||||
/<details>\s*\n?([\s\S]*?)<\/details>/gi,
|
||||
function (m, inner) {
|
||||
var sumMatch = inner.match(
|
||||
/^\s*<summary>([\s\S]*?)<\/summary>\s*\n?([\s\S]*)/i,
|
||||
);
|
||||
var html;
|
||||
if (sumMatch) {
|
||||
html =
|
||||
"<details><summary>" +
|
||||
inlineMarkdown(sumMatch[1].trim()) +
|
||||
"</summary>" +
|
||||
renderMarkdown(sumMatch[2]) +
|
||||
"</details>";
|
||||
} else {
|
||||
html = "<details>" + renderMarkdown(inner) + "</details>";
|
||||
}
|
||||
detailsBlocks.push(html);
|
||||
return "\x00DT" + (detailsBlocks.length - 1) + "\x00";
|
||||
},
|
||||
);
|
||||
|
||||
// Protect display math ($$...$$) — must come before inline code/math
|
||||
var mathBlocks = [];
|
||||
text = text.replace(/\$\$([\s\S]+?)\$\$/g, function (m, tex) {
|
||||
@@ -579,6 +612,12 @@ function renderMarkdown(text) {
|
||||
result = result.replace(/\x00CB(\d+)\x00/g, function (m, idx) {
|
||||
return codeBlocks[parseInt(idx)];
|
||||
});
|
||||
result = result.replace(/<p>\x00DT(\d+)\x00<\/p>/g, function (m, idx) {
|
||||
return detailsBlocks[parseInt(idx)];
|
||||
});
|
||||
result = result.replace(/\x00DT(\d+)\x00/g, function (m, idx) {
|
||||
return detailsBlocks[parseInt(idx)];
|
||||
});
|
||||
result = result.replace(/<p>\x00BQ(\d+)\x00<\/p>/g, function (m, idx) {
|
||||
return bqBlocks[parseInt(idx)];
|
||||
});
|
||||
@@ -618,6 +657,7 @@ var _NO_HIGHLIGHT_LANGS = {
|
||||
plain: true,
|
||||
nohighlight: true,
|
||||
mermaid: true,
|
||||
plantuml: true,
|
||||
};
|
||||
var _TERMINAL_LANGS = {
|
||||
bash: true,
|
||||
|
||||
@@ -358,6 +358,35 @@ body { position: static; }
|
||||
.msg-assistant .mermaid-container svg * { animation: none !important; }
|
||||
}
|
||||
|
||||
/* Safe HTML elements */
|
||||
.msg-assistant details {
|
||||
margin: 8px 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
.msg-assistant details summary {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--fg-bright);
|
||||
user-select: none;
|
||||
}
|
||||
.msg-assistant details summary:hover { color: var(--accent); }
|
||||
.msg-assistant details[open] > summary { border-bottom: 1px solid var(--border); }
|
||||
.msg-assistant details > :not(summary) { padding: 0 12px; }
|
||||
.msg-assistant kbd {
|
||||
background: var(--bg-highlight);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 1px 5px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
box-shadow: 0 1px 0 var(--border-strong);
|
||||
}
|
||||
.msg-assistant mark { background: var(--yellow-glow); color: var(--fg-bright); border-radius: 2px; padding: 0 2px; }
|
||||
|
||||
/* GFM Callouts / Alerts */
|
||||
.msg-assistant .callout {
|
||||
border-left: 3px solid var(--border-strong);
|
||||
|
||||
Reference in New Issue
Block a user