generated at
PlantUML live editor on Scrapbox
即席コード

pu
Bob->Alice: Hello Alice->Bob: 誰ダお前 Bob->Alice: お前こそ誰だよ Alice->Bob: そっちが先に答えろ

tsx
const { mount } = await import("./App.tsx"); mount();
App.tsx
/** @jsx h */ /** @jsxFrag Fragment */ import { h, Fragment, render, } from "../preact@10.5.14/mod.js"; import { useState, useCallback, useEffect, } from "../preact@10.5.14/hooks.js"; import { useCodeBlock } from "./useCodeBlock.ts"; import { toPlantUML } from "./toPlantUML.ts"; interface AppProps { close: () => void; } function App({ close }: AppProps) { const files = useCodeBlock("pu"); const [url ,setUrl] = useState<URL | undefined>(undefined); // URLの更新 useEffect(() => setUrl( files[0] ? toPlantUML(files[0].lines.join("\n")) : undefined ), [files[0]]);

エラーの場合はpng画像を代わりに出す
png画像だとエラー内容も表示してくれるようだ
2022-01-26 07:20:17 Hooksを経由せずにsrcを書き換えるようにした
App.tsx
const onError = useCallback( (e: h.JSX.GenericEventHandler<HTMLImageElement>) => e.currentTarget.src = e.currentTarget.src.replace("svg", "png"), [] ); const onClose = useCallback(() => close(), [close]); return ( <> <style>{` .container { background-color: var(--page-bg); border: 1px solid hsl(72, 64%, 57%); border-radius: 3px; } .pin { position: fixed; top: 10px; left: 10%; width: 80%; max-height: 50%; overflow-y: auto; z-index: 9999; } #close { position: absolute; top: 0; right: 0; } `}</style> <div id="preview" className="container pin"> <button id="close" onClick={onClose}> x </button> {url && (<img src={url} crossorigin="anonymous" onError={onError} />)} </div> </> ); } export function mount() { const app = document.createElement("div"); const shadowRoot = app.attachShadow({ mode: "open" }); document.body.append(app); render(<App close={() => app.remove()} />, shadowRoot); }

特定のcodeBlockを取得する
useCodeBlock.ts
import { useState, useEffect, } from "../preact@10.5.14/hooks.js"; import { throttle } from "./throttle.ts"; export function useCodeBlock(lang: string) { const [files, setFiles] = useState<File[]>([]); useEffect(() => { const callback = throttle(() => { const files = getCodeFiles(); setFiles(files.filter((file) => file.lang === lang)); }, {trailing: true, interval: 300}); scrapbox.addListener("lines:changed", callback); callback(); return () => scrapbox.removeListener("lines:changed", callback); },[]); return files; } export interface File { filename?: string; lang: string; /** コードブロックの開始行のid */ startIds: string[]; lines: string[]; } function getCodeFiles() { const codeBlocks = scrapbox.Page.lines?.flatMap((line) => "codeBlock" in line ? [line] : []) ?? []; return codeBlocks.reduce((acc: File[], { codeBlock, text, id }) => { const sameFileIndex = acc.findIndex(({ filename }) => filename !== undefined && filename === codeBlock.filename ); // code blockの先頭かつ新しいコードブロックのときのみ新しいfileを追加する if (codeBlock.start && sameFileIndex < 0) { return [...acc, { filename: codeBlock.filename, lang: codeBlock.lang, startIds: [id], lines: [] as string[], }]; } if (codeBlock.start) { acc.at(sameFileIndex)?.startIds?.push?.(id); } else { // 既存のコードブロックもしくは末尾のコードブロックに追記する acc.at(sameFileIndex)?.lines?.push?.(text); } return acc; }, []); }

PlantUMLのURLを作る
toPlantUML.ts
import { deflate } from "../denoflate-min/mod.js"; import { encode64, textToBuffer } from "./encode.ts"; export function toPlantUML(uml: string, type: "svg" | "png" = "svg") { return `https://www.plantuml.com/plantuml/${type}/${ encode64(deflate(textToBuffer(uml), 9)) }`; }

PlantUMLのencodeをする
encode.ts
// from http://plantuml.sourceforge.net/codejavascript2.html export function encode64(data: Uint8Array) { let r = ""; for (let i = 0; i < data.length; i += 3) { if (i + 2 === data.length) { r += append3bytes(data[i], data[i + 1], 0); } else if (i + 1 === data.length) { r += append3bytes(data[i], 0, 0); } else { r += append3bytes(data[i], data[i + 1], data[i + 2]); } } return r; } function encode6bit(b: number) { if (b < 10) return String.fromCharCode(48 + b); b -= 10; if (b < 26) return String.fromCharCode(65 + b); b -= 26; if (b < 26) return String.fromCharCode(97 + b); b -= 26; if (b === 0) return '-'; if (b === 1) return '_'; return '?'; } function append3bytes(b1: number, b2: number, b3: number) { const c1 = b1 >> 2; const c2 = ((b1 & 0x3) << 4) | (b2 >> 4); const c3 = ((b2 & 0xF) << 2) | (b3 >> 6); const c4 = b3 & 0x3F; return encode6bit(c1 & 0x3F) + encode6bit(c2 & 0x3F) + encode6bit(c3 & 0x3F) + encode6bit(c4 & 0x3F); } export function textToBuffer(text: string) { const ascii_string = unescape(encodeURIComponent(text)); // 間にこれを噛まさないと文字化けする let buffer = new Uint8Array(ascii_string.length); for (let i = 0; i < ascii_string.length; i++) { buffer[i] = ascii_string.charCodeAt(i); } return buffer; }

Throttle
throttle.ts
function p(n){return new Promise(s=>setTimeout(s,n))}function v(n,s){let{trailing:f=!1,interval:i=0}=s??{},t,r=!1,l=e=>{t?.resolve?.({executed:!1}),t=e},m=()=>{let{...e}=t;return t=void 0,e},c=async()=>{if(r||!t)return;r=!0,i>0&&await p(i);let{parameters:e,resolve:o,reject:u}=m();try{let a=await n(...e);r=!1,o({result:a,executed:!0})}catch(a){r=!1,u(a)}finally{f?await c():(l(),await Promise.resolve())}};return(...e)=>new Promise((o,u)=>{l({parameters:e,resolve:o,reject:u}),c()})}export{v as throttle};

#2022-01-31 15:45:02 useCodeBlock で取得する言語を外部から指定するように変えた
#2022-01-26 07:31:03 エラー周りのflowを修正
#2022-01-09 22:53:51 動いた
#2022-01-07 19:05:46