improve: html highlighting
This commit is contained in:
parent
bdbefad18b
commit
3b096df682
|
|
@ -194,26 +194,130 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightHtml(str) {
|
function highlightHtml(raw) {
|
||||||
const esc = escapeHtml(str);
|
let out = '';
|
||||||
return esc
|
let i = 0;
|
||||||
// Comments: <!-- ... -->
|
const s = raw;
|
||||||
.replace(/<!--[\s\S]*?-->/g, '<span class="text-gray-600">$&</span>')
|
const n = s.length;
|
||||||
// DOCTYPE
|
|
||||||
.replace(/<!(DOCTYPE[^&]*?)>/gi, '<!<span class="text-gray-500">$1</span>>')
|
while (i < n) {
|
||||||
// Tags: <tagname and </tagname
|
// Comment
|
||||||
.replace(/<(\/?)([\w:-]+)/g, '<span class="text-gray-500"><$1</span><span class="text-red-400">$2</span>')
|
if (s.startsWith('<!--', i)) {
|
||||||
// Closing > and />
|
const end = s.indexOf('-->', i + 4);
|
||||||
.replace(/(\/?)\s*>/g, '<span class="text-gray-500">$1></span>')
|
const chunk = end === -1 ? s.slice(i) : s.slice(i, end + 3);
|
||||||
// Attributes: name="value" or name='value'
|
out += '<span class="text-gray-600">' + escapeHtml(chunk) + '</span>';
|
||||||
.replace(/([\w:-]+)(=)("[^&]*?"|'[^&]*?')/g,
|
i += chunk.length;
|
||||||
'<span class="text-yellow-400">$1</span><span class="text-gray-500">$2</span><span class="text-green-400">$3</span>')
|
continue;
|
||||||
// Boolean/valueless attributes (standalone word between tag name and >)
|
}
|
||||||
.replace(/(<\/span>)\s+([\w:-]+)(?=\s|<span class="text-gray-500">)/g,
|
// CDATA
|
||||||
'$1 <span class="text-yellow-400">$2</span>')
|
if (s.startsWith('<![CDATA[', i)) {
|
||||||
// Inline CSS: style content between quotes (already green, make more specific)
|
const end = s.indexOf(']]>', i + 9);
|
||||||
// Entity references: & < etc
|
const chunk = end === -1 ? s.slice(i) : s.slice(i, end + 3);
|
||||||
.replace(/&[\w#]+;/g, '<span class="text-purple-400">$&</span>');
|
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) {
|
function highlightBody(body, contentType) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue