generated at
ScrapJupyter
ScrapJupyter
コードブロックごとにjsを実行できるUserScript


jupyter notebookみたいなので、ScrapJupyterという名前にした
cosenseに名前変わるし、あとjupyter notebook触らなくなったのでやっぱり名前変えようかなmiyamonz

ただ単にevalしてるだけなので自己責任でお願いします
しかし、それはそっくりそのままUserScriptを使う上での危険性と同じなので、わざわざリマインドすることでもないかもしれない

2021/3頃に書いたものからの変更 2024/10/16
log関数を用意して、その場で横に表示できるようにしました
その他いろいろ変更しました
自動実行あたりの設定の仕様をもうちょっと良くしたい

script.js
(()=>{ const title = 'ScrapJupyter' const debug = (...args) => { //console.log(title, ...args) } // 独自log, 右側に表示できる div[id] .cody-bodyに突っ込む const __log = (id) => (arg) => { $(`[id=L${id}] .code-body [data-${title}-log]`).remove() const text = JSON.stringify(arg) const result = $(`<code data-${title}-log>`) .css('background', 'lightyellow') .append(` => ${text} `) const $icon = $('<i class="far fa-copy">').appendTo(result) $icon.click(()=>{ navigator.clipboard.writeText(text) }) $(`[id=L${id}] .code-body`).append(result) //console.log(id, arg) } window.__log = __log; const transpileLog = (line) => { return line.text.replace(/^(\s)*log/, `__log("${line.id}")`) } // 現在のPageからcode blockの配列を作る function getCodeBlocks() { const lines = scrapbox.Page.lines ?? []; return lines.reduce( (acc,line) => { if(line?.codeBlock?.lang !== "js") return acc if(line?.codeBlock?.start) { return [...acc,{id: line.id, line, content: ""}]; } if(line?.codeBlock) { const text = transpileLog(line) acc[acc.length-1].content += text + "\n" } return acc; },[]) // {id:string, line: typeof scrapbox["Page"]["lines"][number] start line , content:string}[] } const createButton = () => { const r = $(`<div data-${title}-button>`) .css({ 'position': 'absolute', 'border': "solid 1px", 'border-radius': '50%', 'text-align': 'center', 'font-size': 'large', }) .css({left: -40, top:0, 'z-index': 900}) .width(30).height(30) .on('mousedown',function(){ $(this).css('color','red') }) .on('mouseup',function(){ $(this).css('color','black') }) .text("▶") return r } //ボタンは div[id] .code-startの下に突っ込む const $button = (id) => $(`[id="L${id}"] .code-start [data-${title}-button]`) const $buttons = () => $(`[id] .code-start [data-${title}-button]`) const getOrCreateButton = (id) => { const found = $button(id) return found.length ? found : createButton() } const removeAllButtons = () => { debug('removeAllButtons') $buttons().remove() } function appendButtons() { getCodeBlocks().forEach( block => { const { id } = block const b = getOrCreateButton(id) b.click(()=>{run(block)}) $(`[id=L${id}] .code-start`).append(b) }) } function run(block) { debug('run', block) const b = getOrCreateButton(block.id) try { (1,eval)(block.content) b.css("background", "lightgreen") return true } catch (e) { console.error(title, "eval error", block, e); b.css("background", "red") return false } } function runAll() { debug('runAll start') for(const block of getCodeBlocks()){ const ok = run(block) if(!ok) break; } debug('runAll end') } // disable when project is changed const currentProject = scrapbox.Project.name; function disableWhenProjectChange(){ if (scrapbox.Project.name !== currentProject) _disable() } function _enable() { appendButtons() scrapbox.on('lines:changed', appendButtons) //scrapbox.on('lines:changed', runAll) scrapbox.on('project:changed', disableWhenProjectChange) } function _disable(remove=true) { if(remove) removeAllButtons() scrapbox.off('lines:changed', appendButtons) scrapbox.off('lines:changed', runAll) scrapbox.off('project:changed', disableWhenProjectChange) } const off = 'https://i.gyazo.com/b12404a0c17e0808af3c8366419073b2.png' const on = 'https://i.gyazo.com/fcaa624a3b739bb3cb35d7f60bf5ae39.png' function loadScript() { scrapbox.PageMenu.addMenu({ title, image: off, onClick: runAll }) _enable() } // debug const cache = (window.miyamonz ??= {})[title] ??= {}; cache.prev?.() loadScript() cache.prev = () => _disable(false) })()


中身の解説
scrapbox.Page.linesの情報から無理やりコードを復元してるだけ
コードブロックのパーツを表すlineにはcodeBlockという変数が入ってる
その下にstart, end, lang, textあたりの情報が入ってる
start →コードブロックの先頭か否か
end →コードブロックの末端か否か
lang →コードの言語(javascriptとか)
text →その行のテキスト
start, endを見ながらtextを結合してるだけ
start見つけたら新規作成して、都度textを結合、endが見えたら終了
面倒だからindentの空白もtrimしてないよ 多分動くっしょ

eval
(1,eval)('hoge = 1')
これでwindow.hogeに入る
なんで?miyamonz


以下考察

evalじゃない方法を考える
普通にapi経由で呼び出しだと、ファイル名が必要 & 同名は結合されて1つのファイルとなる
これはscrapboxの仕様
なので、ファイル名ごとに実行ボタンを配置するとかはたぶん可能 やってないけど
いつの間にか無名ファイルにもapiで取れるようになったので、実はそっちのほうがいいかも
全部に名前はつけたくないけど、個別のブロックごとに実行したいなら現状の方が良い

発展型
他にも、適当にcreateCanvasしたりしてp5.jsとか動かしたら楽しいかも?

js実行するためにevalしたけど、なにか別のlispだとか, js上で処理できる言語を実行するのいいかも?

chrome拡張かなにかで外部に投げてみたい
http postすれば拡張も要らない?
scprapboxをなんかターンテーブルというか素材置場として、外部アプリに情報送信してDJみたいな感じ?をイメージしてる


雑に遊ぶ
js
window.open('https://example.com')
https://example.com 直リンと同じじゃんmiyamonz
jsなので動的に動けるとかはある
js
const n = Date.now() window.open(`${n}`)

Userscriptとして読み込むほどでもないものを直ちに実行したいときとかは便利かもしれない
まず突然コードブロックを書いてバシバシ実行しながらuserscriptを書く
いい感じになってきたらちゃんとimportして動くようにする、みたいな?


userscript自作するときに、書いたら即実行できるのはかなり便利