generated at
コードブロックのJSをその場で実行するUserScript
コードブロックのJSをその場で実行するUserScript


evalしてるので自己責任でお願いします
しかし、それはそっくりそのままUserScriptを使う上での危険性と同じなので、わざわざリマインドすることでもないかもしれない
複数人プロジェクトで使ったことないけど、どういう危険がある?

P5.jsも実行できるよ


script.js
{ const title = 'コードブロックのJSをその場で実行するUserScript' // eval function evalCode(text) { if (text.match(/\sawait\s/)) { evalCodeAsync(text) } else { // "a = 5"などで、window.aへ保存できる (0, eval)(text); } } // awaitができる。代わりに、windowを暗黙に使えない async function evalCodeAsync(text) { return await Object.getPrototypeOf(async function () { }).constructor(text)(); } // log用 ---------------- function toText(arg) { if (typeof arg === 'string') return arg if (typeof arg === 'number') return arg.toString() if (arg instanceof Set) return `Set(${arg.size}){${Array.from(arg).map(item => toText(item)).join(',')}}}` //if('toString' in arg) return arg.toString() return JSON.stringify(arg) } // 独自log, 右側に表示できる div[id] .code-bodyに突っ込む function findOrCreateLog(id) { const found = $(`[id=L${id}] [data-${title}-log]`) if (found?.length) return found return $(`<code data-${title}-log>`) .css('background', 'lightyellow') .append(`<span>`) .append($('<i class="far fa-copy">').css({ cursor: 'pointer' })) .appendTo(`[id=L${id}] .code-body`) } const __log = (id) => (arg) => { const text = toText(arg) const $log = findOrCreateLog(id) $log.find('span').text(` => ${text} `) $log.find('i') .off() .on('click', () => { navigator.clipboard.writeText(text) }) } window.__log = __log; function removeLog() { console.log($(`[data-${title}-log]`)[0]) $(`[data-${title}-log]`).remove() } const transpileLog = (line) => { return line.text.replace(/^(\s*)log/, `$1__log("${line.id}")`); } // 現在のPageからcode blockの配列を作る function getCodeBlocks() { const lines = scrapbox.Page.lines ?? []; const codeBlocks = [] for (const line of lines) { if (line?.codeBlock?.lang !== "js") continue //console.log(line?.codeBlock) if (line?.codeBlock?.start) { const block = { id: line.id, line, filename: line.codeBlock.filename, content: '', } codeBlocks.push(block) continue } if (line?.codeBlock) { const text = transpileLog(line) codeBlocks[codeBlocks.length - 1].content += text.slice(line.codeBlock.indent) + "\n" } } return codeBlocks } 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]`) function getOrCreateButton(id) { const found = $button(id) return found.length ? found : createButton() } function removeAllButtons() { $buttons().remove() } function appendButtons() { getCodeBlocks().forEach(block => { const { id } = block const b = getOrCreateButton(id) b.off() b.click(() => run(block)) $(`[id=L${id}] .code-start`).append(b) }) } function run(block) { //console.log('run', block) const b = getOrCreateButton(block.id) const captured = block.filename const blocks = getCodeBlocks() try { evalCode(block.content) } catch (e) { console.error(title, "eval error", block, e); console.log(e.toString()) console.log(e.stack.toString()) const ee = e.stack.toString().split('\n')[1] const errorPos = parseErrorPosition(ee) console.log(errorPos) b.css("background", "red") return false } try { // p5の特別処理 if (isP5(block)) onRunP5() } catch (e) { console.error('error on p5', e) } b.css("background", "lightgreen") return true } function parseErrorPosition(errorMessage) { const matches = errorMessage.match(/:(\d+:\d+)/g); console.log(errorMessage, matches) // 最後のマッチを取り出す const lastMatch = matches[matches.length - 1]; // `:`を除去して行:列だけを取得 const lineAndColumn = lastMatch.slice(1); // 先頭の ":" を削除 console.log(lineAndColumn); // 出力: 5:2 const [line, col] = lineAndColumn.split(':') return { line: Number(line), col: Number(col) } } // auto--------------------------------- let prevBlocks = [] function runChangedBlock() { const blocks = getCodeBlocks() for (const prevBlock of prevBlocks) { const next = blocks.find(b => b.id === prevBlock.id) const changed = next.content !== prevBlock.content if (changed) { removeLog() run(next) } } prevBlocks = blocks } let autoRun = false; const runChangedBlock_ = _.debounce(runChangedBlock, 100) function enableAutoRun() { autoRun = true; runChangedBlock() scrapbox.on('lines:changed', runChangedBlock_) $(`.page-menu-extension #${title} img`).attr('src', on) } function disableAutoRun() { autoRun = false; scrapbox.off('lines:changed', runChangedBlock_) $(`.page-menu-extension #${title} img`).attr('src', off) } function toggleAutoRun() { if (!autoRun) enableAutoRun(); else disableAutoRun(); } // // 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('project:changed', disableWhenProjectChange) } function disable() { removeAllButtons() removeLog() scrapbox.off('lines:changed', appendButtons) scrapbox.off('project:changed', disableWhenProjectChange) disableAutoRun() } const off = 'https://i.gyazo.com/b12404a0c17e0808af3c8366419073b2.png' const on = 'https://i.gyazo.com/fcaa624a3b739bb3cb35d7f60bf5ae39.png' function loadUserScript() { scrapbox.PageMenu.addMenu({ title, image: autoRun ? on : off, onClick: toggleAutoRun, }) enable() } // debug const cache = (window.miyamonz ??= {})[title] ??= {}; cache.prev?.() loadUserScript() cache.prev = () => { disable() } // p5 const loadP5 = () => loadScript("https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.0/p5.js") loadP5() async function loadScript(src) { if ($(`script[src="${src}"]`).length) return const script = document.createElement('script'); script.src = src; const promise = new Promise(resolve => { script.addEventListener('load', resolve); }); document.body.appendChild(script); return promise; } function createCanvasContainer() { const id = 'canvasContainer' if ($(`#${id}`).length === 0) $(`<div id="${id}">`).appendTo('#app-container') return $(`#${id}`) .css({ //border: "solid 1px", position: "fixed", bottom: 20, right: 0, zIndex: 1000 }) //.attr('draggable', true) .empty() //.append($('<div>').css({background:'lightblue'}).height(20)) //.append($('#defaultCanvas0')) } function isP5(block) { // グローバルにdrawがあれば、P5をやっているっぽい return typeof draw !== undefined //return block.content.match(/function\s+draw\(.*\)\s*{/) } async function onRunP5() { // p5が準備できてから await loadP5() const container = createCanvasContainer() window.remove?.() new p5(); window.setup?.(); window._renderer.parent(container.attr('id')); } }


中身の解説
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

特殊なlog関数を用意してる
実行前に行idを注入してる
いわばトランスパイル。単に文字列としていじってるだけだけど
引数に渡した中身をcosenseのエディタ上に表示してる


過去経緯
2024/12/7
cosenseに名前変わるし、あとjupyter notebook触らなくなったのでやっぱり名前変えようかなmiyamonz
木星の要素もないし、思い入れもない
やっぱり、名で体を表すほうがいい
2024/10/16
2021/3頃に書いたものからの変更 
log関数を用意して、その場で横に表示できるようにしました
その他いろいろ変更しました
自動実行あたりの設定の仕様をもうちょっと良くしたい
2021/3くらい
固有名詞にすると覚えやすいかなと思って、ScrapJupyterと名前をつけた


以下考察

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

evalじゃないとwindowに変数や関数が保存できない


発展型
他にも、適当に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して動くようにする、みたいな?


行単位で、コード上でそのままフィードバックを与えるやつ、なんていうんだっけ
sparklineもその一種
VS Codeでjsで、テストなどでそれをやる拡張があった気がする
基本無料の
あと音楽系の独自インタプリタでも例を見た
ビートに合わせて光るのは面白い


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