improve: html highlighting
This commit is contained in:
parent
bdbefad18b
commit
3b096df682
|
|
@ -194,26 +194,130 @@
|
|||
);
|
||||
}
|
||||
|
||||
function highlightHtml(str) {
|
||||
const esc = escapeHtml(str);
|
||||
return esc
|
||||
// Comments: <!-- ... -->
|
||||
.replace(/<!--[\s\S]*?-->/g, '<span class="text-gray-600">$&</span>')
|
||||
// DOCTYPE
|
||||
.replace(/<!(DOCTYPE[^&]*?)>/gi, '<!<span class="text-gray-500">$1</span>>')
|
||||
// Tags: <tagname and </tagname
|
||||
.replace(/<(\/?)([\w:-]+)/g, '<span class="text-gray-500"><$1</span><span class="text-red-400">$2</span>')
|
||||
// Closing > and />
|
||||
.replace(/(\/?)\s*>/g, '<span class="text-gray-500">$1></span>')
|
||||
// Attributes: name="value" or name='value'
|
||||
.replace(/([\w:-]+)(=)("[^&]*?"|'[^&]*?')/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: & < etc
|
||||
.replace(/&[\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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue