improve: html highlighting

This commit is contained in:
nate 2026-03-24 18:04:45 +04:00
parent bdbefad18b
commit 3b096df682
1 changed files with 124 additions and 20 deletions

View File

@ -194,26 +194,130 @@
);
}
function highlightHtml(str) {
const esc = escapeHtml(str);
return esc
// Comments: <!-- ... -->
.replace(/&lt;!--[\s\S]*?--&gt;/g, '<span class="text-gray-600">$&</span>')
// DOCTYPE
.replace(/&lt;!(DOCTYPE[^&]*?)&gt;/gi, '&lt;!<span class="text-gray-500">$1</span>&gt;')
// Tags: <tagname and </tagname
.replace(/&lt;(\/?)([\w:-]+)/g, '<span class="text-gray-500">&lt;$1</span><span class="text-red-400">$2</span>')
// Closing > and />
.replace(/(\/?)\s*&gt;/g, '<span class="text-gray-500">$1&gt;</span>')
// Attributes: name="value" or name='value'
.replace(/([\w:-]+)(=)(&quot;[^&]*?&quot;|&#39;[^&]*?&#39;)/g,
'<span class="text-yellow-400">$1</span><span class="text-gray-500">$2</span><span class="text-green-400">$3</span>')
// Boolean/valueless attributes (standalone word between tag name and >)
.replace(/(<\/span>)\s+([\w:-]+)(?=\s|<span class="text-gray-500">)/g,
'$1 <span class="text-yellow-400">$2</span>')
// Inline CSS: style content between quotes (already green, make more specific)
// Entity references: &amp; &lt; etc
.replace(/&amp;[\w#]+;/g, '<span class="text-purple-400">$&</span>');
function highlightHtml(raw) {
let out = '';
let i = 0;
const s = raw;
const n = s.length;
while (i < n) {
// Comment
if (s.startsWith('<!--', i)) {
const end = s.indexOf('-->', i + 4);
const chunk = end === -1 ? s.slice(i) : s.slice(i, end + 3);
out += '<span class="text-gray-600">' + escapeHtml(chunk) + '</span>';
i += chunk.length;
continue;
}
// CDATA
if (s.startsWith('<![CDATA[', i)) {
const end = s.indexOf(']]>', i + 9);
const chunk = end === -1 ? s.slice(i) : s.slice(i, end + 3);
out += '<span class="text-gray-600">' + escapeHtml(chunk) + '</span>';
i += chunk.length;
continue;
}
// DOCTYPE / processing instruction
if (s.startsWith('<!', i) || s.startsWith('<?', i)) {
const end = s.indexOf('>', i);
const chunk = end === -1 ? s.slice(i) : s.slice(i, end + 1);
out += '<span class="text-gray-500">' + escapeHtml(chunk) + '</span>';
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 += '<span class="text-gray-400">' + escapeHtml(content) + '</span>';
// 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 += '<span class="text-purple-400">' + escapeHtml(ent) + '</span>';
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 '<span class="text-gray-500">' + escapeHtml(tag) + '</span>';
const [, open, name, attrs, close] = m;
let result = '<span class="text-gray-500">' + escapeHtml(open) + '</span>';
result += '<span class="text-red-400">' + escapeHtml(name) + '</span>';
if (attrs.trim()) result += highlightAttrs(attrs);
result += '<span class="text-gray-500">' + escapeHtml(close) + '</span>';
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 += '<span class="text-yellow-400">' + escapeHtml(am[1]) + '</span>';
if (am[0].includes('=')) {
out += '<span class="text-gray-500">=</span>';
const val = am[2] ?? am[3] ?? am[4] ?? '';
const q = am[2] != null ? '"' : am[3] != null ? "'" : '';
out += '<span class="text-green-400">' + escapeHtml(q + val + q) + '</span>';
}
i += am[0].length;
} else {
out += escapeHtml(str[i]);
i++;
}
}
return out;
}
function highlightBody(body, contentType) {