複数行で数式を書くためのUserScript
概要
複数行で数式をかけるようにします
マクロに登録し数式表記で参照することで表示
コードブロック直下の行で逐次レンダリング
数式のコードブロックの折りたたみ
how to use
jsimport { setCodeFormula, setup } from "../MultiLineFormula/script.js"
function mullineformula() {
if (scrapbox.Layout === "page") {
setCodeFormula(macros).then(() => {
forceUpdate();
setup(macros);
})
}
}
window.scrapbox.addListener("page:changed", mullineformula);
まとめて動作するようにしたもの : import "/api/code/RrMg4Kn7Pgk-HQwximSddmt9/katex-patch/patch.js"
ファイル名をxxx.mathとしてコードブロックに数式を書く。
直下の行に [$ \mullinedefxxx ]
で参照する
参照がコードブロックの直下にあればコードブロックの編集で逐次に再レンダリングされる
コードブロックを隠したいときは▼を押す
アップデート
2022/10/07 : マクロ参照の行にインデントがある場合にクリックできない問題を修正
script.jsfunction getReactFiber(obj) {
return obj[Object.keys(obj).find((key) => key.startsWith("__reactFiber"))]
}
const lines = document.getElementsByClassName("lines")[0]
const linesReactObj = getReactFiber(lines)
const lineObjs = linesReactObj.pendingProps.children;
const textarea = document.getElementById('text-input')
const textareaReactObj = getReactFiber(textarea)
function getFormula(lines, lineObjs, index){
let content = ""
let seekFirst = index
while (true) {
if (lineObjs[seekFirst].props.codeBlock.start) break;
content = lines.children[seekFirst].textContent.trim() + content
seekFirst -= 1
}
let seekEnd = index
while (true) {
if (lineObjs[seekEnd].props.codeBlock.end) break;
seekEnd += 1;
content += lines.children[seekEnd].textContent.trim()
}
return [content, [seekFirst, seekEnd]]
}
const closure = (macros) => function keyupevent(e) {
const position = textareaReactObj.return.return.stateNode.props.position;
const lineObjs = linesReactObj.pendingProps.children;
const cursorindex = position.line;
const currentline = lineObjs[cursorindex].props
if (currentline.codeBlock !== undefined && currentline.codeBlock.lang === "math"){
//console.log(currentline)
const name = currentline.codeBlock.filename.slice(0, -5)
const [formula, [firstLine, lastLine]] = getFormula(lines, lineObjs, cursorindex)
macros["\\mullinedef"+name] = formula
const firstline = lines.children[firstLine]
const nextline = lines.children[lastLine+1]
const nextlineObj = getReactFiber(nextline).child
.return.return.stateNode;
nextlineObj.updater.enqueueForceUpdate(nextlineObj)
if (isSetFoldBtns(lines.children[firstLine])) {
if (isFolded(nextline)) nextline.children[0].click()
} else {
setFoldBtns(firstline.getElementsByClassName("indent")[0] ,nextline)
nextline.children[0].style.display = "none"
}
}
}
function fold(line) {
if (Array.from(line.children).some(e=>e.classList.contains("code-block"))) {
line.style.display = "none"
return fold(line.nextSibling)
}
return line
}
function unfold(line) {
if (Array.from(line.children).some(e=>e.classList.contains("code-block"))) {
line.style.display = ""
return unfold(line.nextSibling)
}
return line
}
function isFolded(line) {
return line.children[0].style.display === ""
}
function isSetFoldBtns(codefirstline) {
return codefirstline.getElementsByClassName("indent")[0].children[0].textContent === '▼'
}
function setFoldBtns(codeindentelem, codesiblingline) {
const codefirstline = codeindentelem.parentElement.parentElement
const foldbtn = document.createElement("span")
foldbtn.style.cursor = "pointer"
foldbtn.textContent = "▼"
const expnbtn = document.createElement("span")
expnbtn.textContent = "▶"
const indent_style = codesiblingline.querySelector(
"span.text > span > span > span.indent")?.getAttribute("style") ?? ""
const exp_style = "cursor: pointer; padding-right: 10px;" + indent_style
expnbtn.setAttribute("style", exp_style)
foldbtn.onclick = (e)=>{
unfoldbtn.style.display = ""
fold(codefirstline)
}
unfoldbtn.onclick = (e)=>{
unfoldbtn.style.display = "none"
unfold(codefirstline)
}
codeindentelem.prepend(foldbtn)
codesiblingline.prepend(unfoldbtn)
}
function setFold(codefirstelem) {
const codesiblingline = fold(codefirstline)
setFoldBtns(codefirstelem, codesiblingline)
}
export function setCodeFormula(macros, callback) {
const tasks = []
for (const elem of lines.getElementsByClassName("code-block-start")) {
const titleelem = elem.children[0]
if (titleelem.title !== "math") continue
const codeurl = titleelem.children[0].href
const name = codeurl.slice(codeurl.lastIndexOf("/")+1, -5)
tasks.push(fetch(codeurl).then(
result => result.text()
).then(
formula => {
macros["\\mullinedef"+name] = formula
}
))
const codeindentelem = elem.parentElement.parentElement
const codesiblingline = fold(codeindentelem.parentElement.parentElement)
setFoldBtns(codeindentelem, codesiblingline)
}
return Promise.all(tasks)
}
export function setup(macros) {
textarea.addEventListener('keyup', closure(macros));
}
備考
マクロ名はどうにかうまい命名に変えたい
同じファイル名でコードブロックが書かれることは気にしていない実装なのでバグる
実装メモ
lineのオブジェクトの updater.enqueueForceUpdate
を呼ぶことで再レンダリングを実現している
入力の反映タイミングがよくわからなかったのでtextareaのkeyupで呼ぶことで誤魔化している
ロード時のコードブロックの判定が面倒だったのでurlから取得することにした