diff --git a/apps/web/src/views/detail.ejs b/apps/web/src/views/detail.ejs
index 6742287..9740cdd 100644
--- a/apps/web/src/views/detail.ejs
+++ b/apps/web/src/views/detail.ejs
@@ -194,26 +194,130 @@
);
}
- function highlightHtml(str) {
- const esc = escapeHtml(str);
- return esc
- // Comments:
- .replace(/<!--[\s\S]*?-->/g, '$&')
- // DOCTYPE
- .replace(/<!(DOCTYPE[^&]*?)>/gi, '<!$1>')
- // Tags: <$1$2')
- // Closing > and />
- .replace(/(\/?)\s*>/g, '$1>')
- // Attributes: name="value" or name='value'
- .replace(/([\w:-]+)(=)("[^&]*?"|'[^&]*?')/g,
- '$1$2$3')
- // Boolean/valueless attributes (standalone word between tag name and >)
- .replace(/(<\/span>)\s+([\w:-]+)(?=\s|)/g,
- '$1 $2')
- // Inline CSS: style content between quotes (already green, make more specific)
- // Entity references: & < etc
- .replace(/&[\w#]+;/g, '$&');
+ function highlightHtml(raw) {
+ let out = '';
+ let i = 0;
+ const s = raw;
+ const n = s.length;
+
+ while (i < n) {
+ // Comment
+ if (s.startsWith('', i + 4);
+ const chunk = end === -1 ? s.slice(i) : s.slice(i, end + 3);
+ out += '' + escapeHtml(chunk) + '';
+ i += chunk.length;
+ continue;
+ }
+ // CDATA
+ if (s.startsWith('', i + 9);
+ const chunk = end === -1 ? s.slice(i) : s.slice(i, end + 3);
+ out += '' + escapeHtml(chunk) + '';
+ i += chunk.length;
+ continue;
+ }
+ // DOCTYPE / processing instruction
+ if (s.startsWith('', i);
+ const chunk = end === -1 ? s.slice(i) : s.slice(i, end + 1);
+ out += '' + escapeHtml(chunk) + '';
+ i += chunk.length;
+ continue;
+ }
+ // Script/style: highlight tag but treat content as plain text
+ const scriptMatch = s.slice(i).match(/^<(script|style)([\s>])/i);
+ if (scriptMatch) {
+ const tagName = scriptMatch[1];
+ const closeTag = '' + tagName;
+ const closeIdx = s.toLowerCase().indexOf(closeTag.toLowerCase(), i + 1);
+ const tagEnd = s.indexOf('>', i);
+ if (tagEnd !== -1) {
+ // Opening tag
+ out += highlightTag(s.slice(i, tagEnd + 1));
+ const contentStart = tagEnd + 1;
+ if (closeIdx !== -1) {
+ // Content as plain
+ const content = s.slice(contentStart, closeIdx);
+ if (content) out += '' + escapeHtml(content) + '';
+ // Closing tag
+ const closeEnd = s.indexOf('>', closeIdx);
+ const closeChunk = closeEnd === -1 ? s.slice(closeIdx) : s.slice(closeIdx, closeEnd + 1);
+ out += highlightTag(closeChunk);
+ i = closeIdx + closeChunk.length;
+ } else {
+ i = tagEnd + 1;
+ }
+ continue;
+ }
+ }
+ // Regular tag
+ if (s[i] === '<' && (s[i+1] === '/' || /[a-zA-Z]/.test(s[i+1] || ''))) {
+ const end = s.indexOf('>', i);
+ if (end !== -1) {
+ out += highlightTag(s.slice(i, end + 1));
+ i = end + 1;
+ continue;
+ }
+ }
+ // Entity reference
+ if (s[i] === '&') {
+ const semi = s.indexOf(';', i);
+ if (semi !== -1 && semi - i < 10) {
+ const ent = s.slice(i, semi + 1);
+ out += '' + escapeHtml(ent) + '';
+ i = semi + 1;
+ continue;
+ }
+ }
+ // Plain text
+ const next = s.indexOf('<', i + 1);
+ const ampNext = s.indexOf('&', i + 1);
+ let textEnd = n;
+ if (next !== -1) textEnd = next;
+ if (ampNext !== -1 && ampNext < textEnd) textEnd = ampNext;
+ out += escapeHtml(s.slice(i, textEnd));
+ i = textEnd;
+ }
+ return out;
+ }
+
+ function highlightTag(tag) {
+ // Parse: ?tagname attrs... /?>
+ const m = tag.match(/^(<\/?)([^\s/>]+)([\s\S]*?)(\/?>\s*)$/);
+ if (!m) return '' + escapeHtml(tag) + '';
+ const [, open, name, attrs, close] = m;
+ let result = '' + escapeHtml(open) + '';
+ result += '' + escapeHtml(name) + '';
+ if (attrs.trim()) result += highlightAttrs(attrs);
+ result += '' + escapeHtml(close) + '';
+ return result;
+ }
+
+ function highlightAttrs(str) {
+ let out = '';
+ let i = 0;
+ const n = str.length;
+ while (i < n) {
+ // Whitespace
+ if (/\s/.test(str[i])) { out += str[i]; i++; continue; }
+ // attr=value or attr="value" or attr='value' or boolean attr
+ const am = str.slice(i).match(/^([\w:.@-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/);
+ if (am) {
+ out += '' + escapeHtml(am[1]) + '';
+ if (am[0].includes('=')) {
+ out += '=';
+ const val = am[2] ?? am[3] ?? am[4] ?? '';
+ const q = am[2] != null ? '"' : am[3] != null ? "'" : '';
+ out += '' + escapeHtml(q + val + q) + '';
+ }
+ i += am[0].length;
+ } else {
+ out += escapeHtml(str[i]);
+ i++;
+ }
+ }
+ return out;
}
function highlightBody(body, contentType) {