ScrapJupyter
ScrapJupyter
jupyter notebookみたいなので、ScrapJupyterという名前にした
cosenseに名前変わるし、あとjupyter notebook触らなくなったのでやっぱり名前変えようかな
ただ単に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に入る
なんで?
以下考察
evalじゃない方法を考える
普通にapi経由で呼び出しだと、ファイル名が必要 & 同名は結合されて1つのファイルとなる
これはscrapboxの仕様
なので、ファイル名ごとに実行ボタンを配置するとかはたぶん可能 やってないけど
いつの間にか無名ファイルにもapiで取れるようになったので、実はそっちのほうがいいかも
全部に名前はつけたくないけど、個別のブロックごとに実行したいなら現状の方が良い
発展型
他にも、適当にcreateCanvasしたりして
p5.jsとか動かしたら楽しいかも?
js実行するためにevalしたけど、なにか別のlispだとか, js上で処理できる言語を実行するのいいかも?
chrome拡張かなにかで外部に投げてみたい
http postすれば拡張も要らない?
scprapboxをなんかターンテーブルというか素材置場として、外部アプリに情報送信してDJみたいな感じ?をイメージしてる
雑に遊ぶ
jswindow.open('https://example.com')
jsなので動的に動けるとかはある
jsconst n = Date.now()
window.open(`${n}`)
Userscriptとして読み込むほどでもないものを直ちに実行したいときとかは便利かもしれない
まず突然コードブロックを書いてバシバシ実行しながらuserscriptを書く
いい感じになってきたらちゃんとimportして動くようにする、みたいな?
userscript自作するときに、書いたら即実行できるのはかなり便利