mirror of
https://codeberg.org/listyantidewi/your-everyday-tools.git
synced 2026-07-02 07:27:39 +08:00
134 lines
5.1 KiB
HTML
134 lines
5.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}HTML Formatter - EveryTools{% endblock %}
|
|
{% block top_title %}HTML Formatter{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="client-tool">
|
|
<div class="tool-header">
|
|
<h1>HTML Formatter</h1>
|
|
<p>Beautify or minify HTML source</p>
|
|
</div>
|
|
<div class="split-pane">
|
|
<div class="pane">
|
|
<div class="pane-header">
|
|
<span>Input</span>
|
|
<div>
|
|
<button class="btn btn-small" onclick="formatHTML()">Beautify</button>
|
|
<button class="btn btn-small" onclick="minifyHTML()">Minify</button>
|
|
</div>
|
|
</div>
|
|
<div class="pane-body">
|
|
<textarea id="html-input" placeholder="<div><p>Hello</p></div>"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="pane">
|
|
<div class="pane-header">
|
|
<span>Output</span>
|
|
<button class="btn btn-small" onclick="copyOutput()"><i class="bi bi-clipboard"></i> Copy</button>
|
|
</div>
|
|
<div class="pane-body">
|
|
<pre id="html-output"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
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","bdo","br","cite","code","em","i","img","input","kbd","label","mark","q","s","small","span","strong","sub","sup","time","u","var"]);
|
|
|
|
function tokenize(src) {
|
|
const tokens = [];
|
|
let i = 0;
|
|
while (i < src.length) {
|
|
if (src[i] === "<") {
|
|
const end = src.indexOf(">", i);
|
|
if (end === -1) { tokens.push({type: "text", value: src.slice(i)}); break; }
|
|
const tag = src.slice(i, end + 1);
|
|
if (tag.startsWith("<!--")) {
|
|
const c = src.indexOf("-->", i);
|
|
tokens.push({type: "comment", value: src.slice(i, c === -1 ? src.length : c + 3)});
|
|
i = c === -1 ? src.length : c + 3;
|
|
} else if (tag.startsWith("<!")) {
|
|
tokens.push({type: "doctype", value: tag});
|
|
i = end + 1;
|
|
} else if (tag.startsWith("</")) {
|
|
tokens.push({type: "close", value: tag, name: tag.slice(2, -1).trim().toLowerCase()});
|
|
i = end + 1;
|
|
} else {
|
|
const name = tag.slice(1).split(/[\s>/]/)[0].toLowerCase();
|
|
const selfClosing = tag.endsWith("/>") || VOID_TAGS.has(name);
|
|
tokens.push({type: selfClosing ? "void" : "open", value: tag, name});
|
|
i = end + 1;
|
|
if (["script","style","pre","textarea"].includes(name) && !selfClosing) {
|
|
const closeRe = new RegExp(`</${name}\\s*>`, "i");
|
|
const m = src.slice(i).match(closeRe);
|
|
if (m) {
|
|
tokens.push({type: "raw", value: src.slice(i, i + m.index)});
|
|
tokens.push({type: "close", value: m[0], name});
|
|
i += m.index + m[0].length;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const next = src.indexOf("<", i);
|
|
const text = src.slice(i, next === -1 ? src.length : next);
|
|
if (text.trim()) tokens.push({type: "text", value: text.trim()});
|
|
i = next === -1 ? src.length : next;
|
|
}
|
|
}
|
|
return tokens;
|
|
}
|
|
|
|
function formatHTML() {
|
|
const src = document.getElementById("html-input").value;
|
|
const tokens = tokenize(src);
|
|
const out = [];
|
|
let depth = 0;
|
|
const pad = () => " ".repeat(depth);
|
|
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
const t = tokens[i];
|
|
const next = tokens[i + 1];
|
|
if (t.type === "open") {
|
|
// Inline tag with just text inside, keep on one line
|
|
if (INLINE_TAGS.has(t.name) && next && next.type === "text" &&
|
|
tokens[i + 2] && tokens[i + 2].type === "close" && tokens[i + 2].name === t.name) {
|
|
out.push(pad() + t.value + next.value + tokens[i + 2].value);
|
|
i += 2;
|
|
} else {
|
|
out.push(pad() + t.value);
|
|
depth++;
|
|
}
|
|
} else if (t.type === "close") {
|
|
depth = Math.max(0, depth - 1);
|
|
out.push(pad() + t.value);
|
|
} else if (t.type === "void" || t.type === "doctype" || t.type === "comment") {
|
|
out.push(pad() + t.value);
|
|
} else if (t.type === "text") {
|
|
out.push(pad() + t.value);
|
|
} else if (t.type === "raw") {
|
|
out.push(t.value);
|
|
}
|
|
}
|
|
document.getElementById("html-output").textContent = out.join("\n");
|
|
}
|
|
|
|
function minifyHTML() {
|
|
const src = document.getElementById("html-input").value;
|
|
const out = src
|
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
.replace(/>\s+</g, "><")
|
|
.replace(/\s{2,}/g, " ")
|
|
.trim();
|
|
document.getElementById("html-output").textContent = out;
|
|
}
|
|
|
|
function copyOutput() {
|
|
navigator.clipboard.writeText(document.getElementById("html-output").textContent);
|
|
}
|
|
</script>
|
|
{% endblock %}
|