Created
December 3, 2022 05:24
-
-
Save sonota88/47ec37ef761ac4290ac88ee28e3c1621 to your computer and use it in GitHub Desktop.
HTML + JavaScript で Emacs の align-regexp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title>align-regexp</title> | |
<style> | |
* { | |
font-family: Firge, monospace; | |
} | |
body { | |
padding: 1rem 4%; | |
} | |
input, textarea { | |
margin: 0.2rem 0; | |
} | |
textarea { | |
width: 90%; | |
height: 40vh; | |
} | |
</style> | |
<script> | |
function puts(...args) { | |
// console.log(...args); | |
} | |
function getEl(sel) { | |
return document.querySelector(sel); | |
} | |
function handleEv(el, ev, fn) { | |
let els; | |
if (typeof el === "string") { | |
els = document.querySelectorAll(el); | |
} else { | |
els = [el]; | |
} | |
els.forEach(el => el.addEventListener(ev, fn)); | |
} | |
function debounce(fn, msec) { | |
const self = debounce; | |
if (self.map[fn] != null) { | |
clearTimeout(self.map[fn]); | |
} | |
self.map[fn] = setTimeout( | |
() => { | |
fn(); | |
self.map[fn] = null; | |
}, | |
msec | |
); | |
} | |
debounce.map = {}; | |
// -------------------------------- | |
function getPattern() { | |
return getEl("#pattern").value; | |
} | |
function getIndex() { | |
return Math.trunc(getEl("#index").value); | |
} | |
function getInput() { | |
return getEl("#input").value; | |
} | |
function setInput(text) { | |
getEl("#input").value = text; | |
} | |
function getPreview() { | |
return getEl("#preview").value; | |
} | |
function setPreview(text) { | |
getEl("#preview").value = text; | |
} | |
// -------------------------------- | |
function isHankaku(c) { | |
const cc = c.charCodeAt(0); | |
return ( | |
(32 <= cc && cc <= 126) // (SPC) ... '~' | |
|| (65377 <= cc && cc <= 65439) // '。' ... '゚' | |
); | |
} | |
function charWidth(c) { | |
return isHankaku(c) ? 1 : 2; | |
} | |
function lengthInHankaku(s) { | |
let len = 0; | |
for (let i = 0; i < s.length; i++) { | |
len += charWidth(s.charAt(i)); | |
} | |
return len; | |
} | |
/* | |
* left sep right | |
* ^m.index == left length | |
* ^^^m[0] | |
*/ | |
function matchLine(line, re, index) { | |
let rest = line; | |
let found = false; | |
let pos = 0; | |
let i = -1; | |
while (true) { | |
i++; | |
if (1000 < i) { | |
throw new Error("too many iteration"); | |
} | |
const m = rest.match(re); | |
if (m == null) { | |
break; | |
} | |
pos += m.index; // add left | |
if (index <= i) { | |
found = true; | |
break; | |
} | |
pos += m[0].length; // add separator | |
rest = rest.substring(m.index + m[0].length); | |
} | |
return found ? pos : null; | |
} | |
function getAlignPos(pairs) { | |
const headLenList = | |
pairs | |
.map(pair => pair[0]) | |
.map(lengthInHankaku); | |
return Math.max(...headLenList); | |
} | |
function toPair(line, re, index) { | |
const pos = matchLine(line, re, index); | |
if (pos == null) { | |
return [line, ""]; | |
} else { | |
const head = line.substring(0, pos); | |
const rest = line.substring(pos); | |
return [head, rest]; | |
} | |
} | |
function alignRegexp(input, re, index) { | |
const lines = input.split("\n"); | |
const pairs = lines.map(line => toPair(line, re, index)); | |
const alignPos = getAlignPos(pairs); | |
const newLines = | |
pairs.map(pair => { | |
const [head, rest] = pair; | |
if (rest === "") { | |
return head; | |
} else { | |
const numSpaces = alignPos - lengthInHankaku(head); | |
return head + " ".repeat(numSpaces) + " " + rest; | |
} | |
}); | |
return newLines.join("\n"); | |
} | |
function _refreshPreview() { | |
const pattern = getPattern(); | |
if (pattern === "") { | |
setPreview(""); | |
return; | |
} | |
const re = new RegExp(pattern); | |
const input = getInput(); | |
const index = getIndex(); | |
const aligned = alignRegexp(input, re, index); | |
setPreview(aligned); | |
} | |
function refreshPreview() { | |
try { | |
_refreshPreview(); | |
} catch(e) { | |
setPreview(e.message); | |
throw e; | |
} | |
} | |
function doApply() { | |
const text = getPreview(); | |
setInput(text); | |
} | |
function oninput_text() { | |
debounce(refreshPreview, 100); | |
} | |
function onclick_apply() { | |
doApply(); | |
} | |
function onkeydown(ev) { | |
if (ev.ctrlKey && ev.key === "Enter") { | |
doApply(); | |
} | |
} | |
function init() { | |
refreshPreview(); | |
["#pattern", "#input", "#index"] | |
.forEach(sel => | |
handleEv(sel, "input", oninput_text) | |
); | |
handleEv("#btn_apply", "click", onclick_apply) | |
handleEv(window, "keydown", (ev) => onkeydown(ev)); | |
} | |
window.addEventListener("pageshow", init); | |
</script> | |
</head> | |
<body> | |
pattern: <input id="pattern" style="width: 50%;" /> | |
<br /> | |
position: <input id="index" type="number" value="0" min="0" /> | |
<br /> | |
<textarea id="input"></textarea> | |
<hr /> | |
preview | |
<button id="btn_apply">apply (C-RET)</button> | |
<br /> | |
<textarea id="preview" style="color: #444;"></textarea> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment