tetratto/tools/html_to_lisp.html
2025-06-01 12:25:33 -04:00

138 lines
4.6 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
textarea {
width: 100%;
height: 250px;
}
</style>
</head>
<body>
<form onsubmit="convert(event)">
<fieldset>
<legend>Data</legend>
<label for="input">Input</label>
<br />
<textarea name="input" id="input" required></textarea>
</fieldset>
<fieldset>
<legend>Output</legend>
<label for="output">Output</label>
<br />
<textarea name="output" id="output" disabled></textarea>
<br />
<button
type="button"
onclick="window.navigator.clipboard.writeText(document.getElementById('output').value)"
>
Copy
</button>
</fieldset>
<br />
<button>Submit</button>
</form>
<script>
const INDENT_SPACES = 4;
function get_indent_str(indent) {
return " ".repeat(indent > 0 ? indent * INDENT_SPACES : 0);
}
function gen_lisp(
element,
indent = 0,
add_n_after_elements = false,
) {
const indent_str = get_indent_str(indent);
let output = "";
for (const node of element.childNodes) {
if (node.nodeName === "#text") {
const data = node.data.trim();
if (!data) {
continue;
}
if (data.startsWith("{")) {
// templating
let text = "";
for (const line of data
.replaceAll('"', '\\"')
.split("\n")) {
text += ` ${line.trim()}`;
}
output += `${indent_str}(text "${text.trim().replaceAll('"', '\\"').replaceAll('\\\\"', '\\"').replaceAll("}} {{", "}}\n{{")}")\n`;
} else {
// normal text
output += `${indent_str}(text "${data.replaceAll('"', '\\"')}")\n`;
}
} else if (node.localName) {
output += `${indent_str}(${node.localName}\n`;
// add attributes
for (const attr of node.attributes || []) {
output += `${get_indent_str(indent + 1)}("${attr.name}" "${attr.value.replaceAll('"', '\\\\\\"')}")\n`;
}
// add children
output += gen_lisp(node, indent + 1);
// close block
output = output.slice(0, output.length - 1); // remove last character (new line)
output += add_n_after_elements ? ")\n\n" : ")\n";
} else if (node.nodeName === "#comment") {
let text = "";
for (const line of node.textContent.split("\n")) {
if (line.trim() === "prettier-ignore") {
continue;
}
text += `${indent_str};${line.trimEnd()}\n`;
}
output += text;
} else {
console.warn(
"skipped element with no local name:",
node,
);
}
}
return output;
}
function convert(e) {
e.preventDefault();
const element_document = new DOMParser().parseFromString(
e.target.input.value,
"text/html",
);
const start = performance.now();
e.target.output.value = gen_lisp(
element_document.body,
0,
true,
);
console.log(
`processed ${e.target.input.value.length} characters in ${performance.now() - start}ms`,
);
}
</script>
</body>
</html>