🔖 HTML 格式化

HTML 格式化:基于 DOMParser 解析树重排,规范化缩进、保留 pre/textarea/script/style 内容。

等待输入...
已复制
const SAMPLE = '\n\n\n\n示例页面\n\n\n

Hello World

\n
\n

这是一个段落。

\n\n
\n\n'; const IND = () => { const v = document.getElementById('indent').value; return v === 'tab' ? '\t' : ' '.repeat(parseInt(v)); }; const VOID_TAGS = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']); const INLINE_TAGS = new Set(['a','abbr','b','bdi','bdo','br','cite','code','data','dfn','em','i','kbd','mark','q','rp','rt','ruby','s','samp','small','span','strong','sub','sup','time','u','var','wbr']); const PRESERVE_TAGS = new Set(['pre','textarea','script','style']); function escapeHtmlAttr(s){ return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>'); } function escapeText(s){ return String(s).replace(/&/g,'&').replace(//g,'>'); } function serializeNode(node, depth, lines){ const pad = IND().repeat(depth); if(node.nodeType === 1){ const tag = node.tagName.toLowerCase(); let attrs = ''; for(const attr of node.attributes){ attrs += ' ' + attr.name + '="' + escapeHtmlAttr(attr.value) + '"'; } const isVoid = VOID_TAGS.has(tag); const isInline = INLINE_TAGS.has(tag); const isPreserve = PRESERVE_TAGS.has(tag); if(isVoid){ lines.push(pad + '<' + tag + attrs + '>'); } else if(isPreserve){ const content = node.textContent || ''; const firstLine = pad + '<' + tag + attrs + '>'; const innerLines = content.split('\n'); lines.push(firstLine); for(const l of innerLines) lines.push(pad + IND() + l); lines.push(pad + ''); } else { lines.push(pad + '<' + tag + attrs + '>'); const childLines = []; for(const child of node.childNodes){ if(child.nodeType === 3){ const text = child.nodeValue.replace(/^\s+|\s+$/g, ''); if(text) childLines.push(pad + IND() + escapeText(text)); } else if(child.nodeType === 1){ serializeNode(child, depth + 1, childLines); } else if(child.nodeType === 8){ childLines.push(pad + IND() + ''); } } if(childLines.length > 0){ for(const l of childLines) lines.push(l); } lines.push(pad + ''); } } else if(node.nodeType === 3){ const text = node.nodeValue.replace(/^\s+|\s+$/g, ''); if(text) lines.push(pad + escapeText(text)); } else if(node.nodeType === 8){ lines.push(pad + ''); } } function formatHtml(){ setErr(''); try{ const src = document.getElementById('input').value; const doc = new DOMParser().parseFromString(src, 'text/html'); if(doc.getElementsByTagName('parsererror').length) throw new Error('HTML 解析错误'); const lines = []; if(doc.doctype) lines.push(''); const html = doc.documentElement; if(html){ serializeNode(html, 0, lines); } else { for(const child of doc.childNodes) serializeNode(child, 0, lines); } setOut(lines.join('\n')); setErr('✅ 格式化成功'); }catch(e){ setErr('❌ ' + e.message); } } function minifyHtml(){ setErr(''); try{ let src = document.getElementById('input').value; src = src.replace(/)[\s\S]*?-->/g, m => m.includes('[if') || m.includes('[endif') ? m : ''); src = src.replace(/>(\s)+<'); src = src.replace(/\s{2,}/g, ' '); src = src.replace(/\s+\/?>/g, '>'); src = src.replace(/\s+>/g, '>'); src = src.replace(/\n/g, ''); src = src.trim(); setOut(src); setErr('✅ 压缩成功'); }catch(e){ setErr('❌ ' + e.message); } } function doBeautify(){ formatHtml(); } function doMinify(){ minifyHtml(); } function doValidate(){ setErr(''); try{ const src = document.getElementById('input').value; const doc = new DOMParser().parseFromString(src, 'text/html'); if(doc.getElementsByTagName('parsererror').length){ setErr('❌ HTML 解析错误'); setOut('❌ HTML 有语法错误,请检查标签是否闭合'); return; } const allTags = src.match(/<[A-Za-z][^>]*>/g) || []; const openTags = allTags.filter(t => !t.startsWith('')).map(t => t.match(/<([A-Za-z][A-Za-z0-9]*)/)[1].toLowerCase()); const closeTags = (src.match(/<\/[A-Za-z][^>]*>/g) || []).map(t => t.match(/<\/([A-Za-z][A-Za-z0-9]*)/)[1].toLowerCase()); const nonVoidOpen = openTags.filter(t => !VOID_TAGS.has(t)); const stack = []; const unclosed = []; for(const t of allTags){ if(t.startsWith('') || VOID_TAGS.has(name)) continue; stack.push(name); } for(const t of closeTags){ if(stack.length) stack.pop(); } const stats = '✅ HTML 分析\n' + '总标签: ' + allTags.length + '\n' + '开标签: ' + openTags.length + '\n' + '闭标签: ' + closeTags.length + '\n' + 'void 元素: ' + (openTags.length - nonVoidOpen.length) + '\n' + 'DOM 子节点: ' + doc.body ? doc.body.childNodes.length : 'N/A' + '\n' + '未闭合标签: ' + (stack.length ? stack.join(', ') : '无 ✅'); setOut(stats); if(stack.length) setErr('❌ 未闭合:' + stack.join(', ')); else setErr('✅ 校验通过'); }catch(e){ setErr('❌ ' + e.message); } } function getIndent(){const v=document.getElementById('indent').value;return v==='tab'?'\t':' '.repeat(parseInt(v));} function setOut(s){document.getElementById('output').textContent=s;} function setErr(s){const e=document.getElementById('err');e.textContent=s||'';e.className='err'+(s?'':' ok');} function clearAll(){document.getElementById('input').value='';setOut('等待输入...');setErr('');} function loadSample(){document.getElementById('input').value=SAMPLE;doBeautify();}