Mermaid記法可視化UserScript
実装
.lineの次に直接挿入しているっぽい
CSSは特に当てていない
code blocksはdefaultで非表示
SVG領域をクリックすると、コードブロックの表示状態が切り替わる
https://gyazo.com/d88ea10021d5bab12d37fa0eefffb45a
classを3つくらいつくって実装している
前回から差分のあったmermaidのみ更新している
最近標準機能に取り込まれた
code blockが見えなくなる
カーソルをcode blockに置くと見えるようになる
diagramはその下に表示される
code reading
refactoring
hooksみたいな仕組みがほしいな
code:ts
const makeComponent = () => {
let storage;
return (data) => {
// 同じデータの場合は再描画しない
if (storage === data) return;
storage = data;
draw(storage);
};
};
const renders = new Map<string, Renderer>();
const render = renders.get(key) ?? makeComponent();
renders.set(key, render);
render(data);
});
これだと、いらないcomponentを削除できないか
削除は元コードと同じ方法でやるしかなさそうだな
もしくはPreact並みに本格的な差分アルゴリズムを実装するしかない
必要な処理
code blocksの更新監視
viewerの削除と生成
生成
削除
付随するcodeblockの表示切り替え
言語ごとに作成するviwerを変える
内部でclick eventを使いたいときは、viewerの中の要素でstoppropagationしておく
でないとクリックしたときcodeblockの表示切り替えが発動してしまう
code:example.ts
launch({
renderers: {
when: /\.plantuml$/,
render: ({ blocks }) => {
const url = encode(content(blocks));
const img = document.createElement("img");
img.src = url;
return img;
},
}
});
code:viewer.ts
export const launch = ({ renders }) => {
let codes = new Map<string, Blocks>();
const update = () => {
const newCodes = readCodeBlocks(getInternalLines());
for (const item of diff(codes, newCodes)) {
switch (item.op) {
case "noop":
continue;
case "change":
case "add":
remove(item.key);
const render = renders.find(({ when }) => when.test(item.key))?.render;
if (!render) continue;
// 空文字もdelete扱いする
if (content(item.blocks) === "") continue;
create(item.key, item.blocks, render);
break;
case "delete":
remove(item.key);
break;
}
}
codes = newCodes;
};
const handlePageChanged = () => {
if (scrapbox.layout !== "page") {
scrapbox.off("lines:changed", update);
return;
}
update();
scrapbox.on("lines:changed", update);
};
handlePageChanged();
scrapbox.on("page:changed", handlePageChanged);
return () => {
scrapbox.off("page:changed", handlePageChanged);
};
};
code:viewer.ts
/** 指定したコードブロックにpreviewerがあれば削除する */
export const remove = (filename: string) => {
document.getElementsById(scrap-preview-${filename})?.remove?.();
};
/** 指定したコードブロックのpreviewerを作成 or 更新する */
const create = (filename: string, blocks: Blocks, render) => {
remove(filename);
const viewer = previewArea(filename);
viewer.textContent = "";
viewer.append(...doms);
return dispose;
};
const previewArea = (filename: string) => {
const area = document.createElement("div");
area.classList.add("scrap-preview");
area.dataset.filename = filename;
let visible = false;
area.addEventListener("click", () => {
if (visible) {
for (const { id } of lines) {
hiddenIds.add(#${id});
}
} else {
for (const { id } of lines) {
hiddenIds.delete(#${id});
}
}
visible = !visible;
style.textContent = .scrap-preview{cursor:pointer}${[...hiddenIds].join(",")}{display:none};
});
}
/** 非表示にする要素のIDのリスト */
const hiddenIds = new Set<string>();
click event handlerは、行が変更される度に設定し直す必要がある
viewerと行データをclassにまとめたほうが管理が楽かもしれない
もしくは、update=remove&createとする
こっちのほうが実装が楽かも
immutableにobjectを扱える
code:readCodeBlocks.ts
export type Blocks = Line[][];
}
export const content = (blocks: Blocks):string =>
blocks.flatMap(
(block) => block.map((line) => line.text)
).join("\n").trim();
code:diff.ts
import { Blocks, content } from "./code.ts";
export function* diff(oldMap: Map<string, Blocks>, newMap: Map<string, Blocks>) {
const oldBlocks = oldMap.get(key);
if (!oldBlocks) {
yield { op: "add", key, blocks };
continue;
}
if (content(oldBlocks) !== content(blocks)) yield { op: "change", key, blocks };
yield { op: "noop", key, blocks };
}
if (newMap.has(key)) continue;
yield { op: "delete", key, blocks };
}
}