コードブロックのJSをその場で実行するUserScript
https://gyazo.com/eae2584e928296f4be6c024a820476d7
evalしてるので自己責任でお願いします
しかし、それはそっくりそのままUserScriptを使う上での危険性と同じなので、わざわざリマインドすることでもないかもしれない
複数人プロジェクトで使ったことないけど、どういう危険がある?
https://gyazo.com/a1c65c0adf0d12e7d08b58e3d5a5ce48
code: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, 右側に表示できる divid .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)
}
}
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
}
//ボタンは divid .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 lineAndColumn = lastMatch.slice(1); // 先頭の ":" を削除
console.log(lineAndColumn); // 出力: 5:2
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()
}
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
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.icon
特殊なlog関数を用意してる
実行前に行idを注入してる
いわばトランスパイル。単に文字列としていじってるだけだけど
引数に渡した中身をcosenseのエディタ上に表示してる
https://gyazo.com/1ca2eb1194ab5b90ef7666fe7a5a0996
過去経緯
2024/12/7
cosenseに名前変わるし、あとjupyter notebook触らなくなったのでやっぱり名前変えようかなmiyamonz.icon
木星の要素もないし、思い入れもない
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みたいな感じ?をイメージしてる
雑に遊ぶ
code:js
jsなので動的に動けるとかはある
code:js
const n = Date.now()
window.open(${n})
Userscriptとして読み込むほどでもないものを直ちに実行したいときとかは便利かもしれない
まず突然コードブロックを書いてバシバシ実行しながらuserscriptを書く
いい感じになってきたらちゃんとimportして動くようにする、みたいな?
行単位で、コード上でそのままフィードバックを与えるやつ、なんていうんだっけ
VS Codeでjsで、テストなどでそれをやる拡張があった気がする
基本無料の
あと音楽系の独自インタプリタでも例を見た
ビートに合わせて光るのは面白い
userscript自作するときに、書いたら即実行できるのはかなり便利