リポジトリ / emssr / independent_common_compiler
r2
| r2 emssr | 1 | // Common scope notebook の表側。 |
| r2 emssr | 2 | // |
| r2 emssr | 3 | // ウィンドウは二形ある。 |
| r2 emssr | 4 | // full notebook と scope を並べる |
| r2 emssr | 5 | // solo ?pane=<名前> で scope 一枚だけ。別のディスプレイへ出すため |
| r2 emssr | 6 | // |
| r2 emssr | 7 | // どちらのウィンドウも自分の kernel を持つ。同期するのは平文と初期値だけで、 |
| r2 emssr | 8 | // 計算はウィンドウごとに回す。lane の作業領域は交わらない(20 式 1.2)ので、 |
| r2 emssr | 9 | // 同じ平文を何枚実行しても state が混ざらない。 |
| r2 emssr | 10 | |
| r2 emssr | 11 | const CDN = 'https://cdn.jsdelivr.net/pyodide/v314.0.7/full/'; |
| r2 emssr | 12 | const MDCDN = 'https://cdn.jsdelivr.net/npm/common-scope-web@0/'; // vendor/web が無いときの逃げ道 |
| r2 emssr | 13 | |
| r2 emssr | 14 | const PANES = { |
| r2 emssr | 15 | chamber: ['層と机', 'S(k,q,r) = Atom r = 層境界音 ka の数、q = 机の位置(16 §2)\nfr(Λ_{r+1}) = fr(Λ_r) の未消費部 ‖ 層 r の出力(19 §4.2)', s => drawChamber(s)], |
| r2 emssr | 16 | dag: ['箱の DAG', 'commonc の OPK / OPA / OPB / OPD をそのまま(19 §3.2)\nδ(Cst(x,d)) = d, δ(u⊕v) = max, δ(u⋆v) = δu + δv(19 式 4.2)', s => drawDag(s)], |
| r2 emssr | 17 | tsui: ['ツイ数平面', null, s => drawTsui(s)], |
| r2 emssr | 18 | lane: ['lane64', 'Σ_lane64 = ℤ^(W×L) × ℤ^(W×S) × ℕ × {0,1}^W × ℰ* × Σ_out*(20 式 1.1)\nblock の入口 ℬ = {0} ∪ {i+d_i} ∪ {i+1}(式 7.4)、層 Λ_r = {v | h(v) = r}(式 7.2)', s => drawLane(s)], |
| r2 emssr | 19 | joint: ['接合秩', 'F[α][β] = f(α,β) ρ = rank_𝔽₂ F\nf = ⊕_{r<ρ} p_r(x_A) q_r(x_B) (Common-Lang/1 定理 4.1)', () => drawJoint(lastJoint)], |
| r2 emssr | 20 | text: ['音節と箱', null, s => drawText(s)], |
| r2 emssr | 21 | }; |
| r2 emssr | 22 | |
| r2 emssr | 23 | const solo = new URLSearchParams(location.search).get('pane'); |
| r2 emssr | 24 | let py = null, nb = null, nbFile = null, dirty = false; |
| r2 emssr | 25 | let lastScope = null, lastJoint = null, cur = -1; |
| r2 emssr | 26 | |
| r2 emssr | 27 | // ===== 起動 ===== |
| r2 emssr | 28 | |
| r2 emssr | 29 | function log(msg, cls) { |
| r2 emssr | 30 | const li = document.createElement('li'); |
| r2 emssr | 31 | li.textContent = msg; |
| r2 emssr | 32 | if (cls) li.className = cls; |
| r2 emssr | 33 | document.getElementById('bootlog').appendChild(li); |
| r2 emssr | 34 | } |
| r2 emssr | 35 | |
| r2 emssr | 36 | function loadScript(src) { |
| r2 emssr | 37 | return new Promise((ok, no) => { |
| r2 emssr | 38 | const s = document.createElement('script'); |
| r2 emssr | 39 | s.src = src; s.onload = ok; s.onerror = () => no(new Error(src)); |
| r2 emssr | 40 | document.head.appendChild(s); |
| r2 emssr | 41 | }); |
| r2 emssr | 42 | } |
| r2 emssr | 43 | |
| r2 emssr | 44 | async function boot() { |
| r2 emssr | 45 | const rt = await (await fetch('/api/runtime')).json(); |
| r2 emssr | 46 | if (rt.missing.length) { |
| r2 emssr | 47 | log('配信できないファイル: ' + rt.missing.join(', '), 'bad'); |
| r2 emssr | 48 | document.getElementById('bootmsg').textContent = |
| r2 emssr | 49 | 'setup.py を実行して runtime/ を揃えてください。'; |
| r2 emssr | 50 | return; |
| r2 emssr | 51 | } |
| r2 emssr | 52 | try { |
| r2 emssr | 53 | await startRender(rt.web || MDCDN); |
| r2 emssr | 54 | log('Markdown と KaTeX を読み込みました', 'ok'); |
| r2 emssr | 55 | } catch (e) { |
| r2 emssr | 56 | log('Markdown の library を読めません。説明は素のまま出ます', 'bad'); |
| r2 emssr | 57 | } |
| r2 emssr | 58 | const index = rt.pyodide || CDN; |
| r2 emssr | 59 | log(rt.pyodide ? 'Pyodide はこのフォルダのコピーを使います' : 'Pyodide を CDN から取ります', 'ok'); |
| r2 emssr | 60 | await loadScript(index + 'pyodide.js'); |
| r2 emssr | 61 | const pyodide = await loadPyodide({ indexURL: index }); |
| r2 emssr | 62 | log('CPython ' + pyodide.version + ' がブラウザの中で動きました', 'ok'); |
| r2 emssr | 63 | |
| r2 emssr | 64 | pyodide.FS.mkdir('/common'); |
| r2 emssr | 65 | for (const f of rt.files) { |
| r2 emssr | 66 | const buf = new Uint8Array(await (await fetch('/runtime/' + f)).arrayBuffer()); |
| r2 emssr | 67 | pyodide.FS.writeFile('/common/' + f, buf); |
| r2 emssr | 68 | } |
| r2 emssr | 69 | log('lane64 の実行台と commonc の image を読み込みました(' + rt.files.length + ' 個)', 'ok'); |
| r2 emssr | 70 | |
| r2 emssr | 71 | pyodide.runPython(await (await fetch('boot.py')).text()); |
| r2 emssr | 72 | pyodide.runPython('ready()'); |
| r2 emssr | 73 | py = { |
| r2 emssr | 74 | cell: s => JSON.parse(pyodide.globals.get('cell')(s)), |
| r2 emssr | 75 | joint: s => JSON.parse(pyodide.globals.get('joint')(s)), |
| r2 emssr | 76 | }; |
| r2 emssr | 77 | const k = document.getElementById('kernel'); |
| r2 emssr | 78 | k.textContent = 'lane64 稼働中'; |
| r2 emssr | 79 | k.className = 'tag ok'; |
| r2 emssr | 80 | document.getElementById('boot').classList.add('done'); |
| r2 emssr | 81 | } |
| r2 emssr | 82 | |
| r2 emssr | 83 | // ===== ウィンドウ間の同期 ===== |
| r2 emssr | 84 | |
| r2 emssr | 85 | function publish(patch) { |
| r2 emssr | 86 | fetch('/state', { method: 'POST', body: JSON.stringify(patch) }).catch(() => { }); |
| r2 emssr | 87 | } |
| r2 emssr | 88 | |
| r2 emssr | 89 | // ===== 本文 ===== |
| r2 emssr | 90 | // |
| r2 emssr | 91 | // 説明セルは Markdown で書く。数式は $…$ と $$…$$。画像と動画は assets/ へ置いて |
| r2 emssr | 92 | //  や <video src="assets/x.mp4" controls> で貼る。描くのは render.js。 |
| r2 emssr | 93 | |
| r2 emssr | 94 | function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<'); } |
| r2 emssr | 95 | |
| r2 emssr | 96 | function markInto(el, text) { return renderMarkdown(el, text); } |
| r2 emssr | 97 | |
| r2 emssr | 98 | // 説明の編集欄。書いている間はすぐ下に見え、file を落とすと assets/ へ上がる |
| r2 emssr | 99 | function attachEditor(area, get, set, onDone) { |
| r2 emssr | 100 | const live = document.createElement('div'); |
| r2 emssr | 101 | live.className = 'md livepreview'; |
| r2 emssr | 102 | let timer = null; |
| r2 emssr | 103 | const draw = () => markInto(live, get()); |
| r2 emssr | 104 | area.addEventListener('input', () => { |
| r2 emssr | 105 | set(area.value); |
| r2 emssr | 106 | clearTimeout(timer); |
| r2 emssr | 107 | timer = setTimeout(draw, 220); |
| r2 emssr | 108 | }); |
| r2 emssr | 109 | area.addEventListener('dragover', e => { |
| r2 emssr | 110 | if (!e.dataTransfer.types.includes('Files')) return; |
| r2 emssr | 111 | e.preventDefault(); |
| r2 emssr | 112 | e.stopPropagation(); |
| r2 emssr | 113 | area.classList.add('over'); |
| r2 emssr | 114 | }); |
| r2 emssr | 115 | area.addEventListener('dragleave', () => area.classList.remove('over')); |
| r2 emssr | 116 | area.addEventListener('drop', async e => { |
| r2 emssr | 117 | if (!e.dataTransfer.files.length) return; |
| r2 emssr | 118 | e.preventDefault(); |
| r2 emssr | 119 | e.stopPropagation(); |
| r2 emssr | 120 | area.classList.remove('over'); |
| r2 emssr | 121 | const lines = []; |
| r2 emssr | 122 | for (const f of e.dataTransfer.files) { |
| r2 emssr | 123 | try { lines.push(assetMarkdown(await uploadAsset(f), f)); } |
| r2 emssr | 124 | catch (err) { alert(err.message); } |
| r2 emssr | 125 | } |
| r2 emssr | 126 | const at = area.selectionStart; |
| r2 emssr | 127 | area.value = area.value.slice(0, at) + '\n' + lines.join('\n') + '\n' + area.value.slice(at); |
| r2 emssr | 128 | set(area.value); |
| r2 emssr | 129 | draw(); |
| r2 emssr | 130 | touch(); |
| r2 emssr | 131 | }); |
| r2 emssr | 132 | area.addEventListener('paste', async e => { |
| r2 emssr | 133 | const files = [...(e.clipboardData.files || [])]; |
| r2 emssr | 134 | if (!files.length) return; |
| r2 emssr | 135 | e.preventDefault(); |
| r2 emssr | 136 | const lines = []; |
| r2 emssr | 137 | for (const f of files) { |
| r2 emssr | 138 | try { lines.push(assetMarkdown(await uploadAsset(f), f)); } |
| r2 emssr | 139 | catch (err) { alert(err.message); } |
| r2 emssr | 140 | } |
| r2 emssr | 141 | const at = area.selectionStart; |
| r2 emssr | 142 | area.value = area.value.slice(0, at) + lines.join('\n') + area.value.slice(at); |
| r2 emssr | 143 | set(area.value); |
| r2 emssr | 144 | draw(); |
| r2 emssr | 145 | touch(); |
| r2 emssr | 146 | }); |
| r2 emssr | 147 | area.addEventListener('blur', () => { |
| r2 emssr | 148 | clearTimeout(timer); |
| r2 emssr | 149 | live.remove(); |
| r2 emssr | 150 | onDone(); |
| r2 emssr | 151 | }); |
| r2 emssr | 152 | const show = () => { |
| r2 emssr | 153 | area.after(live); |
| r2 emssr | 154 | draw(); |
| r2 emssr | 155 | }; |
| r2 emssr | 156 | return show; |
| r2 emssr | 157 | } |
| r2 emssr | 158 | |
| r2 emssr | 159 | // ===== notebook の出し入れ ===== |
| r2 emssr | 160 | // |
| r2 emssr | 161 | // 保存先は server の notebooks/ になる。書きかけは localStorage にも置くので、 |
| r2 emssr | 162 | // 保存の前にタブを閉じても戻せる。 |
| r2 emssr | 163 | |
| r2 emssr | 164 | const DRAFT = 'common-scope-draft:'; |
| r2 emssr | 165 | |
| r2 emssr | 166 | function blank() { |
| r2 emssr | 167 | return { |
| r2 emssr | 168 | title: '名前のない notebook', |
| r2 emssr | 169 | lead: '', |
| r2 emssr | 170 | dic: '', |
| r2 emssr | 171 | cells: [{ kind: 'cmn', text: '', source: 'hu hu ka, no ka.' }], |
| r2 emssr | 172 | }; |
| r2 emssr | 173 | } |
| r2 emssr | 174 | |
| r2 emssr | 175 | function touch() { |
| r2 emssr | 176 | dirty = true; |
| r2 emssr | 177 | try { localStorage.setItem(DRAFT + nbFile, JSON.stringify(nb)); } catch (e) { /* 保存できなくても動く */ } |
| r2 emssr | 178 | const s = document.getElementById('save'); |
| r2 emssr | 179 | if (s) s.textContent = '保存 *'; |
| r2 emssr | 180 | } |
| r2 emssr | 181 | |
| r2 emssr | 182 | function clean() { |
| r2 emssr | 183 | dirty = false; |
| r2 emssr | 184 | try { localStorage.removeItem(DRAFT + nbFile); } catch (e) { /* 同上 */ } |
| r2 emssr | 185 | const s = document.getElementById('save'); |
| r2 emssr | 186 | if (s) s.textContent = '保存'; |
| r2 emssr | 187 | } |
| r2 emssr | 188 | |
| r2 emssr | 189 | async function listNotebooks() { |
| r2 emssr | 190 | window._nbs = await (await fetch('/api/notebooks')).json(); |
| r2 emssr | 191 | const pick = document.getElementById('pick'); |
| r2 emssr | 192 | pick.innerHTML = ''; |
| r2 emssr | 193 | window._nbs.forEach(n => { |
| r2 emssr | 194 | const o = document.createElement('option'); |
| r2 emssr | 195 | o.value = n.file; o.textContent = n.title; |
| r2 emssr | 196 | pick.appendChild(o); |
| r2 emssr | 197 | }); |
| r2 emssr | 198 | if (nbFile) pick.value = nbFile; |
| r2 emssr | 199 | renderToc(); |
| r2 emssr | 200 | } |
| r2 emssr | 201 | |
| r2 emssr | 202 | async function openNotebook(file) { |
| r2 emssr | 203 | if (dirty && !confirm('保存していない変更があります。破棄して開きますか。')) return; |
| r2 emssr | 204 | nbFile = file; |
| r2 emssr | 205 | nb = await (await fetch('/api/notebook/' + file)).json(); |
| r2 emssr | 206 | let draft = null; |
| r2 emssr | 207 | try { draft = localStorage.getItem(DRAFT + file); } catch (e) { /* 無ければ保存分を使う */ } |
| r2 emssr | 208 | if (draft && confirm('保存していない書きかけがあります。そちらを開きますか。')) { |
| r2 emssr | 209 | nb = JSON.parse(draft); |
| r2 emssr | 210 | dirty = true; |
| r2 emssr | 211 | } else { |
| r2 emssr | 212 | try { localStorage.removeItem(DRAFT + file); } catch (e) { /* 同上 */ } |
| r2 emssr | 213 | dirty = false; |
| r2 emssr | 214 | } |
| r2 emssr | 215 | nb.cells.forEach(c => { c.result = null; }); |
| r2 emssr | 216 | document.getElementById('pick').value = file; |
| r2 emssr | 217 | renderSheet(); |
| r2 emssr | 218 | renderToc(); |
| r2 emssr | 219 | publish({ notebook: file }); |
| r2 emssr | 220 | } |
| r2 emssr | 221 | |
| r2 emssr | 222 | function adoptNotebook(file, doc) { |
| r2 emssr | 223 | nbFile = file; |
| r2 emssr | 224 | nb = doc; |
| r2 emssr | 225 | nb.cells = nb.cells || []; |
| r2 emssr | 226 | renderSheet(); |
| r2 emssr | 227 | renderToc(); |
| r2 emssr | 228 | touch(); |
| r2 emssr | 229 | } |
| r2 emssr | 230 | |
| r2 emssr | 231 | async function saveNotebook(as) { |
| r2 emssr | 232 | let file = nbFile; |
| r2 emssr | 233 | if (as || !file) { |
| r2 emssr | 234 | const guess = (nb.title || 'notebook').replace(/[^\w一-龠ぁ-んァ-ヶー0-9]+/g, '_'); |
| r2 emssr | 235 | file = prompt('保存する名前', (file || guess + '.json').replace(/\.json$/, '') + '.json'); |
| r2 emssr | 236 | if (!file) return; |
| r2 emssr | 237 | } |
| r2 emssr | 238 | const body = JSON.parse(JSON.stringify(nb)); |
| r2 emssr | 239 | body.cells.forEach(c => { delete c.result; }); |
| r2 emssr | 240 | const r = await fetch('/api/notebook/' + encodeURIComponent(file), |
| r2 emssr | 241 | { method: 'POST', body: JSON.stringify(body) }); |
| r2 emssr | 242 | if (!r.ok) { alert('保存できません: ' + r.status); return; } |
| r2 emssr | 243 | nbFile = (await r.json()).file; |
| r2 emssr | 244 | clean(); |
| r2 emssr | 245 | await listNotebooks(); |
| r2 emssr | 246 | document.getElementById('pick').value = nbFile; |
| r2 emssr | 247 | } |
| r2 emssr | 248 | |
| r2 emssr | 249 | async function deleteNotebook() { |
| r2 emssr | 250 | if (!nbFile || !confirm(nbFile + ' を消しますか。')) return; |
| r2 emssr | 251 | await fetch('/api/notebook/' + encodeURIComponent(nbFile), { method: 'DELETE' }); |
| r2 emssr | 252 | try { localStorage.removeItem(DRAFT + nbFile); } catch (e) { /* 同上 */ } |
| r2 emssr | 253 | nbFile = null; |
| r2 emssr | 254 | await listNotebooks(); |
| r2 emssr | 255 | if (window._nbs.length) await openNotebook(window._nbs[0].file); |
| r2 emssr | 256 | else { nb = blank(); nbFile = 'notebook.json'; renderSheet(); } |
| r2 emssr | 257 | } |
| r2 emssr | 258 | |
| r2 emssr | 259 | function download() { |
| r2 emssr | 260 | const body = JSON.parse(JSON.stringify(nb)); |
| r2 emssr | 261 | body.cells.forEach(c => { delete c.result; }); |
| r2 emssr | 262 | const a = document.createElement('a'); |
| r2 emssr | 263 | a.href = URL.createObjectURL(new Blob([JSON.stringify(body, null, 2)], { type: 'application/json' })); |
| r2 emssr | 264 | a.download = nbFile || 'notebook.json'; |
| r2 emssr | 265 | a.click(); |
| r2 emssr | 266 | URL.revokeObjectURL(a.href); |
| r2 emssr | 267 | } |
| r2 emssr | 268 | |
| r2 emssr | 269 | // ===== 画面 ===== |
| r2 emssr | 270 | |
| r2 emssr | 271 | function renderToc() { |
| r2 emssr | 272 | const t = document.getElementById('toc'); |
| r2 emssr | 273 | t.innerHTML = ''; |
| r2 emssr | 274 | const head = document.createElement('div'); |
| r2 emssr | 275 | head.className = 'head'; |
| r2 emssr | 276 | head.textContent = 'NOTEBOOK'; |
| r2 emssr | 277 | t.appendChild(head); |
| r2 emssr | 278 | (window._nbs || []).forEach(n => { |
| r2 emssr | 279 | const a = document.createElement('a'); |
| r2 emssr | 280 | a.textContent = n.title; |
| r2 emssr | 281 | a.href = '#'; |
| r2 emssr | 282 | a.title = n.file; |
| r2 emssr | 283 | if (n.file === nbFile) a.className = 'on'; |
| r2 emssr | 284 | a.onclick = e => { e.preventDefault(); openNotebook(n.file); }; |
| r2 emssr | 285 | t.appendChild(a); |
| r2 emssr | 286 | }); |
| r2 emssr | 287 | const add = document.createElement('a'); |
| r2 emssr | 288 | add.textContent = '+ 新しい notebook'; |
| r2 emssr | 289 | add.href = '#'; |
| r2 emssr | 290 | add.className = 'add'; |
| r2 emssr | 291 | add.onclick = e => { |
| r2 emssr | 292 | e.preventDefault(); |
| r2 emssr | 293 | if (dirty && !confirm('保存していない変更があります。破棄しますか。')) return; |
| r2 emssr | 294 | nbFile = 'notebook.json'; |
| r2 emssr | 295 | nb = blank(); |
| r2 emssr | 296 | dirty = false; |
| r2 emssr | 297 | renderSheet(); |
| r2 emssr | 298 | renderToc(); |
| r2 emssr | 299 | }; |
| r2 emssr | 300 | t.appendChild(add); |
| r2 emssr | 301 | } |
| r2 emssr | 302 | |
| r2 emssr | 303 | function button(label, title, fn) { |
| r2 emssr | 304 | const b = document.createElement('button'); |
| r2 emssr | 305 | b.textContent = label; |
| r2 emssr | 306 | if (title) b.title = title; |
| r2 emssr | 307 | b.onclick = fn; |
| r2 emssr | 308 | return b; |
| r2 emssr | 309 | } |
| r2 emssr | 310 | |
| r2 emssr | 311 | function field(label, value, fn, width) { |
| r2 emssr | 312 | const w = document.createElement('label'); |
| r2 emssr | 313 | w.className = 'field'; |
| r2 emssr | 314 | w.appendChild(document.createTextNode(label)); |
| r2 emssr | 315 | const i = document.createElement('input'); |
| r2 emssr | 316 | i.value = value === undefined || value === null ? '' : value; |
| r2 emssr | 317 | if (width) i.style.width = width; |
| r2 emssr | 318 | i.oninput = () => { fn(i.value); touch(); }; |
| r2 emssr | 319 | w.appendChild(i); |
| r2 emssr | 320 | return w; |
| r2 emssr | 321 | } |
| r2 emssr | 322 | |
| r2 emssr | 323 | function renderSheet() { |
| r2 emssr | 324 | const host = document.getElementById('sheet'); |
| r2 emssr | 325 | host.innerHTML = ''; |
| r2 emssr | 326 | |
| r2 emssr | 327 | // ---- 道具立て ---- |
| r2 emssr | 328 | const bar = document.createElement('div'); |
| r2 emssr | 329 | bar.className = 'toolbar'; |
| r2 emssr | 330 | const save = button('保存', 'notebooks/ へ書き込みます', () => saveNotebook(false)); |
| r2 emssr | 331 | save.id = 'save'; |
| r2 emssr | 332 | if (dirty) save.textContent = '保存 *'; |
| r2 emssr | 333 | bar.append( |
| r2 emssr | 334 | button('すべて実行', '上から順に全ての文を実行します', runAll), |
| r2 emssr | 335 | save, |
| r2 emssr | 336 | button('名前を付けて保存', null, () => saveNotebook(true)), |
| r2 emssr | 337 | button('ダウンロード', '.json として手元へ保存します', download), |
| r2 emssr | 338 | button('削除', 'この notebook を消します', deleteNotebook)); |
| r2 emssr | 339 | host.appendChild(bar); |
| r2 emssr | 340 | |
| r2 emssr | 341 | // ---- 表題と説明と採用語彙 ---- |
| r2 emssr | 342 | const head = document.createElement('div'); |
| r2 emssr | 343 | head.id = 'intro'; |
| r2 emssr | 344 | const h1 = document.createElement('input'); |
| r2 emssr | 345 | h1.className = 'title'; |
| r2 emssr | 346 | h1.value = nb.title || ''; |
| r2 emssr | 347 | h1.placeholder = '表題'; |
| r2 emssr | 348 | h1.oninput = () => { nb.title = h1.value; touch(); }; |
| r2 emssr | 349 | head.appendChild(h1); |
| r2 emssr | 350 | |
| r2 emssr | 351 | const lead = document.createElement('div'); |
| r2 emssr | 352 | lead.className = 'lead md'; |
| r2 emssr | 353 | markInto(lead, nb.lead || ''); |
| r2 emssr | 354 | const leadEdit = document.createElement('textarea'); |
| r2 emssr | 355 | leadEdit.className = 'leadedit'; |
| r2 emssr | 356 | leadEdit.value = nb.lead || ''; |
| r2 emssr | 357 | leadEdit.placeholder = '説明。$$ で数式、| で表、** で強調'; |
| r2 emssr | 358 | leadEdit.hidden = true; |
| r2 emssr | 359 | const showLead = attachEditor(leadEdit, () => leadEdit.value, |
| r2 emssr | 360 | v => { nb.lead = v; touch(); }, |
| r2 emssr | 361 | () => { leadEdit.hidden = true; lead.hidden = false; markInto(lead, nb.lead || ''); }); |
| r2 emssr | 362 | lead.onclick = () => { |
| r2 emssr | 363 | lead.hidden = true; |
| r2 emssr | 364 | leadEdit.hidden = false; |
| r2 emssr | 365 | leadEdit.style.height = Math.max(140, leadEdit.scrollHeight) + 'px'; |
| r2 emssr | 366 | leadEdit.focus(); |
| r2 emssr | 367 | showLead(); |
| r2 emssr | 368 | }; |
| r2 emssr | 369 | lead.title = 'クリックすると書き換えられます'; |
| r2 emssr | 370 | head.append(lead, leadEdit); |
| r2 emssr | 371 | |
| r2 emssr | 372 | const dic = document.createElement('details'); |
| r2 emssr | 373 | dic.className = 'settings'; |
| r2 emssr | 374 | const sum = document.createElement('summary'); |
| r2 emssr | 375 | sum.textContent = '採用語彙(.dic)'; |
| r2 emssr | 376 | const dicText = document.createElement('textarea'); |
| r2 emssr | 377 | dicText.value = nb.dic || ''; |
| r2 emssr | 378 | dicText.placeholder = '見出し ⟨tab⟩ 文。=N で数だけの語\n例: pepe\t=8'; |
| r2 emssr | 379 | dicText.rows = 6; |
| r2 emssr | 380 | dicText.oninput = () => { nb.dic = dicText.value; touch(); }; |
| r2 emssr | 381 | dic.append(sum, dicText); |
| r2 emssr | 382 | head.appendChild(dic); |
| r2 emssr | 383 | host.appendChild(head); |
| r2 emssr | 384 | |
| r2 emssr | 385 | // ---- セル ---- |
| r2 emssr | 386 | const cells = document.createElement('div'); |
| r2 emssr | 387 | cells.id = 'cells'; |
| r2 emssr | 388 | host.appendChild(cells); |
| r2 emssr | 389 | redrawCells(); |
| r2 emssr | 390 | |
| r2 emssr | 391 | host.appendChild(adder(nb.cells.length)); |
| r2 emssr | 392 | } |
| r2 emssr | 393 | |
| r2 emssr | 394 | function adder(at) { |
| r2 emssr | 395 | const d = document.createElement('div'); |
| r2 emssr | 396 | d.className = 'adder'; |
| r2 emssr | 397 | d.append( |
| r2 emssr | 398 | button('+ Common', 'Common の文を書くセルを足します', () => { |
| r2 emssr | 399 | nb.cells.splice(at, 0, { kind: 'cmn', text: '', source: '' }); |
| r2 emssr | 400 | touch(); redrawCells(); |
| r2 emssr | 401 | }), |
| r2 emssr | 402 | button('+ 説明', '文章だけのセルを足します', () => { |
| r2 emssr | 403 | nb.cells.splice(at, 0, { kind: 'note', text: '' }); |
| r2 emssr | 404 | touch(); redrawCells(); |
| r2 emssr | 405 | })); |
| r2 emssr | 406 | return d; |
| r2 emssr | 407 | } |
| r2 emssr | 408 | |
| r2 emssr | 409 | function redrawCells() { |
| r2 emssr | 410 | const host = document.getElementById('cells'); |
| r2 emssr | 411 | host.innerHTML = ''; |
| r2 emssr | 412 | nb.cells.forEach((c, i) => { |
| r2 emssr | 413 | host.appendChild(renderCell(c, i)); |
| r2 emssr | 414 | host.appendChild(adder(i + 1)); |
| r2 emssr | 415 | }); |
| r2 emssr | 416 | } |
| r2 emssr | 417 | |
| r2 emssr | 418 | function move(i, d) { |
| r2 emssr | 419 | const j = i + d; |
| r2 emssr | 420 | if (j < 0 || j >= nb.cells.length) return; |
| r2 emssr | 421 | const [c] = nb.cells.splice(i, 1); |
| r2 emssr | 422 | nb.cells.splice(j, 0, c); |
| r2 emssr | 423 | touch(); redrawCells(); |
| r2 emssr | 424 | } |
| r2 emssr | 425 | |
| r2 emssr | 426 | function renderCell(c, i) { |
| r2 emssr | 427 | const d = document.createElement('div'); |
| r2 emssr | 428 | d.className = 'cell' + (c.kind === 'note' ? ' note' : ''); |
| r2 emssr | 429 | |
| r2 emssr | 430 | // 説明。クリックで書き換えに変わる |
| r2 emssr | 431 | const note = document.createElement('div'); |
| r2 emssr | 432 | note.className = 'note md'; |
| r2 emssr | 433 | if (c.text) markInto(note, c.text); |
| r2 emssr | 434 | else note.innerHTML = '<p class="empty">クリックすると説明を書けます。Markdown と $数式$ が使えます</p>'; |
| r2 emssr | 435 | const noteEdit = document.createElement('textarea'); |
| r2 emssr | 436 | noteEdit.className = 'noteedit'; |
| r2 emssr | 437 | noteEdit.value = c.text || ''; |
| r2 emssr | 438 | noteEdit.hidden = true; |
| r2 emssr | 439 | const showNote = attachEditor(noteEdit, () => noteEdit.value, |
| r2 emssr | 440 | v => { c.text = v; touch(); }, |
| r2 emssr | 441 | () => { |
| r2 emssr | 442 | noteEdit.hidden = true; note.hidden = false; |
| r2 emssr | 443 | if (c.text) markInto(note, c.text); |
| r2 emssr | 444 | else note.innerHTML = '<p class="empty">クリックすると説明を書けます。Markdown と $数式$ が使えます</p>'; |
| r2 emssr | 445 | }); |
| r2 emssr | 446 | note.onclick = () => { |
| r2 emssr | 447 | note.hidden = true; noteEdit.hidden = false; |
| r2 emssr | 448 | noteEdit.style.height = Math.max(110, noteEdit.scrollHeight) + 'px'; |
| r2 emssr | 449 | noteEdit.focus(); |
| r2 emssr | 450 | showNote(); |
| r2 emssr | 451 | }; |
| r2 emssr | 452 | d.append(note, noteEdit); |
| r2 emssr | 453 | |
| r2 emssr | 454 | const tools = document.createElement('div'); |
| r2 emssr | 455 | tools.className = 'tools'; |
| r2 emssr | 456 | tools.append( |
| r2 emssr | 457 | button('↑', '上へ', () => move(i, -1)), |
| r2 emssr | 458 | button('↓', '下へ', () => move(i, 1)), |
| r2 emssr | 459 | button('複製', null, () => { |
| r2 emssr | 460 | nb.cells.splice(i + 1, 0, JSON.parse(JSON.stringify({ ...c, result: null }))); |
| r2 emssr | 461 | touch(); redrawCells(); |
| r2 emssr | 462 | }), |
| r2 emssr | 463 | button('削除', null, () => { |
| r2 emssr | 464 | const has = (c.source || '').trim() || (c.text || '').trim(); |
| r2 emssr | 465 | if (has && !confirm('このセルを消しますか。')) return; |
| r2 emssr | 466 | nb.cells.splice(i, 1); |
| r2 emssr | 467 | touch(); redrawCells(); |
| r2 emssr | 468 | })); |
| r2 emssr | 469 | |
| r2 emssr | 470 | if (c.kind === 'note') { |
| r2 emssr | 471 | d.appendChild(tools); |
| r2 emssr | 472 | return d; |
| r2 emssr | 473 | } |
| r2 emssr | 474 | |
| r2 emssr | 475 | const row = document.createElement('div'); |
| r2 emssr | 476 | row.className = 'row'; |
| r2 emssr | 477 | const gut = document.createElement('div'); |
| r2 emssr | 478 | gut.className = 'gut'; |
| r2 emssr | 479 | gut.textContent = '[' + (i + 1) + ']'; |
| r2 emssr | 480 | const ta = document.createElement('textarea'); |
| r2 emssr | 481 | ta.className = 'src'; |
| r2 emssr | 482 | ta.value = c.source || ''; |
| r2 emssr | 483 | ta.spellcheck = false; |
| r2 emssr | 484 | ta.placeholder = 'hu hu ka, no ka.'; |
| r2 emssr | 485 | ta.rows = Math.max(2, (c.source || '').split('\n').length); |
| r2 emssr | 486 | ta.oninput = () => { c.source = ta.value; touch(); }; |
| r2 emssr | 487 | ta.onkeydown = e => { |
| r2 emssr | 488 | if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); run(i); } |
| r2 emssr | 489 | }; |
| r2 emssr | 490 | ta.ondragover = e => { e.preventDefault(); ta.classList.add('over'); }; |
| r2 emssr | 491 | ta.ondragleave = () => ta.classList.remove('over'); |
| r2 emssr | 492 | ta.ondrop = async e => { |
| r2 emssr | 493 | e.preventDefault(); |
| r2 emssr | 494 | ta.classList.remove('over'); |
| r2 emssr | 495 | const f = e.dataTransfer.files[0]; |
| r2 emssr | 496 | if (!f) return; |
| r2 emssr | 497 | ta.value = c.source = await f.text(); |
| r2 emssr | 498 | ta.rows = Math.max(2, c.source.split('\n').length); |
| r2 emssr | 499 | touch(); |
| r2 emssr | 500 | run(i); |
| r2 emssr | 501 | }; |
| r2 emssr | 502 | row.append(gut, ta); |
| r2 emssr | 503 | d.appendChild(row); |
| r2 emssr | 504 | |
| r2 emssr | 505 | const bar = document.createElement('div'); |
| r2 emssr | 506 | bar.className = 'bar'; |
| r2 emssr | 507 | bar.appendChild(button('Run', 'Ctrl+Enter', () => run(i))); |
| r2 emssr | 508 | const hint = document.createElement('span'); |
| r2 emssr | 509 | hint.className = 'hint'; |
| r2 emssr | 510 | hint.textContent = 'Ctrl+Enter で実行。.cmn をドロップすると差し替えます'; |
| r2 emssr | 511 | bar.append(hint, tools); |
| r2 emssr | 512 | d.appendChild(bar); |
| r2 emssr | 513 | |
| r2 emssr | 514 | // ---- セルの設定 ---- |
| r2 emssr | 515 | const set = document.createElement('details'); |
| r2 emssr | 516 | set.className = 'settings'; |
| r2 emssr | 517 | const sum = document.createElement('summary'); |
| r2 emssr | 518 | sum.textContent = 'このセルの設定'; |
| r2 emssr | 519 | set.appendChild(sum); |
| r2 emssr | 520 | const grid = document.createElement('div'); |
| r2 emssr | 521 | grid.className = 'fields'; |
| r2 emssr | 522 | grid.append( |
| r2 emssr | 523 | field('初期値(語=値)', seedText(c.seeds), v => { c.seeds = parseSeeds(v); }, '13em'), |
| r2 emssr | 524 | field('語数', c.words || '', v => { c.words = v ? Number(v) : undefined; }, '7em'), |
| r2 emssr | 525 | field('入力の語(接合秩)', (c.inputs || []).join(','), v => { |
| r2 emssr | 526 | c.inputs = v.trim() ? v.split(',').map(x => Number(x.trim())) : undefined; |
| r2 emssr | 527 | }, '9em'), |
| r2 emssr | 528 | field('カット κ', c.cut === undefined ? '' : c.cut, v => { |
| r2 emssr | 529 | c.cut = v === '' ? undefined : Number(v); |
| r2 emssr | 530 | }, '4em'), |
| r2 emssr | 531 | field('読む語', c.out || '', v => { c.out = v ? Number(v) : undefined; }, '4em')); |
| r2 emssr | 532 | set.appendChild(grid); |
| r2 emssr | 533 | d.appendChild(set); |
| r2 emssr | 534 | |
| r2 emssr | 535 | const out = document.createElement('div'); |
| r2 emssr | 536 | out.className = 'out'; |
| r2 emssr | 537 | out.id = 'out' + i; |
| r2 emssr | 538 | d.appendChild(out); |
| r2 emssr | 539 | if (c.result) showResult(i, c.result); |
| r2 emssr | 540 | return d; |
| r2 emssr | 541 | } |
| r2 emssr | 542 | |
| r2 emssr | 543 | function seedText(s) { |
| r2 emssr | 544 | return s ? Object.entries(s).map(([k, v]) => k + '=' + v).join(' ') : ''; |
| r2 emssr | 545 | } |
| r2 emssr | 546 | |
| r2 emssr | 547 | function parseSeeds(v) { |
| r2 emssr | 548 | const out = {}; |
| r2 emssr | 549 | for (const t of v.split(/[\s,]+/)) { |
| r2 emssr | 550 | const m = t.match(/^(\d+)=(-?\d+)$/); |
| r2 emssr | 551 | if (m) out[m[1]] = Number(m[2]); |
| r2 emssr | 552 | } |
| r2 emssr | 553 | return Object.keys(out).length ? out : undefined; |
| r2 emssr | 554 | } |
| r2 emssr | 555 | |
| r2 emssr | 556 | // ===== 実行 ===== |
| r2 emssr | 557 | |
| r2 emssr | 558 | function specOf(c) { |
| r2 emssr | 559 | return { |
| r2 emssr | 560 | source: c.source, dic: nb.dic || null, seeds: c.seeds || null, |
| r2 emssr | 561 | words: c.words || null, budget: c.budget || null, |
| r2 emssr | 562 | }; |
| r2 emssr | 563 | } |
| r2 emssr | 564 | |
| r2 emssr | 565 | function viewOf(c) { |
| r2 emssr | 566 | return { inputs: c.inputs || null, cut: c.cut === undefined ? null : c.cut, out: c.out || 0 }; |
| r2 emssr | 567 | } |
| r2 emssr | 568 | |
| r2 emssr | 569 | function run(i) { |
| r2 emssr | 570 | if (!py) return; |
| r2 emssr | 571 | const c = nb.cells[i]; |
| r2 emssr | 572 | const spec = specOf(c); |
| r2 emssr | 573 | lastScope = py.cell(JSON.stringify(spec)); |
| r2 emssr | 574 | lastJoint = null; |
| r2 emssr | 575 | cur = i; |
| r2 emssr | 576 | c.result = lastScope; |
| r2 emssr | 577 | showResult(i, lastScope); |
| r2 emssr | 578 | if (usesJoint()) runJoint(c); |
| r2 emssr | 579 | renderGrid(); |
| r2 emssr | 580 | publish({ cell: i, notebook: nbFile, spec: spec, view: viewOf(c) }); |
| r2 emssr | 581 | } |
| r2 emssr | 582 | |
| r2 emssr | 583 | function runAll() { |
| r2 emssr | 584 | nb.cells.forEach((c, i) => { if (c.kind !== 'note' && (c.source || '').trim()) run(i); }); |
| r2 emssr | 585 | } |
| r2 emssr | 586 | |
| r2 emssr | 587 | function runJoint(c) { |
| r2 emssr | 588 | lastJoint = (py && c && c.inputs) |
| r2 emssr | 589 | ? py.joint(JSON.stringify(Object.assign(specOf(c), viewOf(c)))) : null; |
| r2 emssr | 590 | } |
| r2 emssr | 591 | |
| r2 emssr | 592 | function showResult(i, sc) { |
| r2 emssr | 593 | const host = document.getElementById('out' + i); |
| r2 emssr | 594 | if (!host) return; |
| r2 emssr | 595 | if (!sc.ok) { |
| r2 emssr | 596 | const want = nb.cells[i] && nb.cells[i].expect === 'fail'; |
| r2 emssr | 597 | host.innerHTML = '<div class="' + (want ? 'expected' : 'err') + '">' |
| r2 emssr | 598 | + (want ? '読み手が止めた: ' : '') + esc(sc.error) + '</div>'; |
| r2 emssr | 599 | return; |
| r2 emssr | 600 | } |
| r2 emssr | 601 | const tag = sc.report === 0 ? 'tu(成功)' : sc.report === 42 ? 'te(失敗)' : '報告なし'; |
| r2 emssr | 602 | const vals = sc.gamma.length |
| r2 emssr | 603 | ? sc.gamma.map(g => `語${g.word} = ${g.den === 1 ? g.num : g.num + '/' + g.den}`).join(' ') |
| r2 emssr | 604 | : '出口境界は空'; |
| r2 emssr | 605 | host.innerHTML = |
| r2 emssr | 606 | '<div class="val">' + vals + '</div>' + |
| r2 emssr | 607 | (sc.put ? '<div>Put: ' + esc(sc.put) + '</div>' : '') + |
| r2 emssr | 608 | '<div class="dim">' + tag + ' 音節 ' + sc.size.syl + ' 箱 ' + sc.size.box + |
| r2 emssr | 609 | ' image ' + sc.size.image + ' byte / ' + sc.image.insns + ' 命令 block ' + |
| r2 emssr | 610 | sc.image.blocks.length + ' hash ' + sc.mark + '</div>'; |
| r2 emssr | 611 | } |
| r2 emssr | 612 | |
| r2 emssr | 613 | // ===== scope のパネル配置 ===== |
| r2 emssr | 614 | // |
| r2 emssr | 615 | // パネルは二分木で持つ。 |
| r2 emssr | 616 | // |
| r2 emssr | 617 | // {t:'leaf', pane} |
| r2 emssr | 618 | // {t:'split', dir:'row'|'col', a, b, ratio} |
| r2 emssr | 619 | // |
| r2 emssr | 620 | // 上のタブをパネルへドロップすると、落とした縁で分かれる。中央なら差し替える。 |
| r2 emssr | 621 | // パネル名を掴んで別のパネルへ落とすと入れ替わる。ヘッダのボタンでも分けられる。 |
| r2 emssr | 622 | |
| r2 emssr | 623 | const KEY = 'common-scope-layout'; |
| r2 emssr | 624 | let layout = null, dragPane = null, dragFrom = null; |
| r2 emssr | 625 | |
| r2 emssr | 626 | function defaultLayout() { |
| r2 emssr | 627 | return { |
| r2 emssr | 628 | t: 'split', dir: 'col', ratio: 0.5, |
| r2 emssr | 629 | a: { t: 'leaf', pane: 'chamber' }, |
| r2 emssr | 630 | b: { t: 'leaf', pane: 'dag' }, |
| r2 emssr | 631 | }; |
| r2 emssr | 632 | } |
| r2 emssr | 633 | |
| r2 emssr | 634 | function loadLayout() { |
| r2 emssr | 635 | try { |
| r2 emssr | 636 | const s = localStorage.getItem(KEY); |
| r2 emssr | 637 | if (s) return JSON.parse(s); |
| r2 emssr | 638 | } catch (e) { /* localStorage が無い環境では既定の配置で始める */ } |
| r2 emssr | 639 | return defaultLayout(); |
| r2 emssr | 640 | } |
| r2 emssr | 641 | |
| r2 emssr | 642 | function saveLayout() { |
| r2 emssr | 643 | try { localStorage.setItem(KEY, JSON.stringify(layout)); } catch (e) { /* 保存できなくても動く */ } |
| r2 emssr | 644 | } |
| r2 emssr | 645 | |
| r2 emssr | 646 | function usesJoint() { |
| r2 emssr | 647 | let hit = false; |
| r2 emssr | 648 | (function walk(n) { |
| r2 emssr | 649 | if (!n) return; |
| r2 emssr | 650 | if (n.t === 'leaf') { if (n.pane === 'joint') hit = true; return; } |
| r2 emssr | 651 | walk(n.a); walk(n.b); |
| r2 emssr | 652 | })(layout); |
| r2 emssr | 653 | return hit || solo === 'joint'; |
| r2 emssr | 654 | } |
| r2 emssr | 655 | |
| r2 emssr | 656 | function renderPalette() { |
| r2 emssr | 657 | const p = document.getElementById('palette'); |
| r2 emssr | 658 | p.innerHTML = ''; |
| r2 emssr | 659 | const h = document.createElement('span'); |
| r2 emssr | 660 | h.className = 'hint'; |
| r2 emssr | 661 | h.textContent = 'パネルへドロップすると分かれます'; |
| r2 emssr | 662 | p.appendChild(h); |
| r2 emssr | 663 | for (const key in PANES) { |
| r2 emssr | 664 | const c = document.createElement('span'); |
| r2 emssr | 665 | c.className = 'tab'; |
| r2 emssr | 666 | c.textContent = PANES[key][0]; |
| r2 emssr | 667 | c.draggable = true; |
| r2 emssr | 668 | c.title = 'ドラッグしてパネルへ'; |
| r2 emssr | 669 | c.ondragstart = e => { |
| r2 emssr | 670 | dragPane = key; dragFrom = null; |
| r2 emssr | 671 | e.dataTransfer.effectAllowed = 'copy'; |
| r2 emssr | 672 | e.dataTransfer.setData('text/plain', key); |
| r2 emssr | 673 | }; |
| r2 emssr | 674 | p.appendChild(c); |
| r2 emssr | 675 | } |
| r2 emssr | 676 | const reset = document.createElement('span'); |
| r2 emssr | 677 | reset.className = 'tab'; |
| r2 emssr | 678 | reset.textContent = '既定に戻す'; |
| r2 emssr | 679 | reset.style.marginLeft = 'auto'; |
| r2 emssr | 680 | reset.onclick = () => { layout = defaultLayout(); saveLayout(); renderGrid(); }; |
| r2 emssr | 681 | p.appendChild(reset); |
| r2 emssr | 682 | } |
| r2 emssr | 683 | |
| r2 emssr | 684 | function paneBody(key) { |
| r2 emssr | 685 | const [, formula, make] = PANES[key]; |
| r2 emssr | 686 | if (!lastScope) return nothing('セルを実行してください。'); |
| r2 emssr | 687 | if (!lastScope.ok) return nothing(lastScope.error); |
| r2 emssr | 688 | const d = document.createElement('div'); |
| r2 emssr | 689 | if (formula) { |
| r2 emssr | 690 | const f = document.createElement('div'); |
| r2 emssr | 691 | f.className = 'formula'; |
| r2 emssr | 692 | f.textContent = formula; |
| r2 emssr | 693 | d.appendChild(f); |
| r2 emssr | 694 | } |
| r2 emssr | 695 | d.appendChild(make(lastScope)); |
| r2 emssr | 696 | if (key === 'chamber' && !lastScope.align.ok) { |
| r2 emssr | 697 | d.appendChild(nothing('音節と箱の対応: ' + lastScope.align.why)); |
| r2 emssr | 698 | } |
| r2 emssr | 699 | return d; |
| r2 emssr | 700 | } |
| r2 emssr | 701 | |
| r2 emssr | 702 | function makeLeaf(node, parent) { |
| r2 emssr | 703 | const d = document.createElement('div'); |
| r2 emssr | 704 | d.className = 'leaf'; |
| r2 emssr | 705 | |
| r2 emssr | 706 | const head = document.createElement('header'); |
| r2 emssr | 707 | head.draggable = true; |
| r2 emssr | 708 | head.ondragstart = e => { |
| r2 emssr | 709 | dragPane = node.pane; dragFrom = node; |
| r2 emssr | 710 | e.dataTransfer.effectAllowed = 'move'; |
| r2 emssr | 711 | e.dataTransfer.setData('text/plain', node.pane); |
| r2 emssr | 712 | }; |
| r2 emssr | 713 | const sel = document.createElement('select'); |
| r2 emssr | 714 | sel.className = 'name'; |
| r2 emssr | 715 | for (const k in PANES) { |
| r2 emssr | 716 | const o = document.createElement('option'); |
| r2 emssr | 717 | o.value = k; o.textContent = PANES[k][0]; |
| r2 emssr | 718 | sel.appendChild(o); |
| r2 emssr | 719 | } |
| r2 emssr | 720 | sel.value = node.pane; |
| r2 emssr | 721 | sel.onchange = () => { node.pane = sel.value; renderGrid(); }; |
| r2 emssr | 722 | const sp = document.createElement('span'); |
| r2 emssr | 723 | sp.className = 'sp'; |
| r2 emssr | 724 | head.append(sel, sp, |
| r2 emssr | 725 | button('⊟', '横に分ける', () => { dropInto(node, node.pane, 'bottom'); renderGrid(); }), |
| r2 emssr | 726 | button('⊞', '縦に分ける', () => { dropInto(node, node.pane, 'right'); renderGrid(); }), |
| r2 emssr | 727 | button('⧉', '別ウィンドウで開く', () => window.open('?pane=' + node.pane, '_blank')), |
| r2 emssr | 728 | button('✕', 'このパネルを閉じる', () => { removeNode(node); renderGrid(); })); |
| r2 emssr | 729 | if (!parent) head.lastChild.disabled = true; |
| r2 emssr | 730 | |
| r2 emssr | 731 | const body = document.createElement('div'); |
| r2 emssr | 732 | body.className = 'body'; |
| r2 emssr | 733 | body.appendChild(paneBody(node.pane)); |
| r2 emssr | 734 | d.append(head, body); |
| r2 emssr | 735 | |
| r2 emssr | 736 | // ドロップ。落とした縁で分ける |
| r2 emssr | 737 | const hint = document.createElement('div'); |
| r2 emssr | 738 | hint.className = 'dropmark'; |
| r2 emssr | 739 | hint.hidden = true; |
| r2 emssr | 740 | d.appendChild(hint); |
| r2 emssr | 741 | const zoneOf = e => { |
| r2 emssr | 742 | const r = d.getBoundingClientRect(); |
| r2 emssr | 743 | const u = (e.clientX - r.left) / r.width, v = (e.clientY - r.top) / r.height; |
| r2 emssr | 744 | const m = Math.min(u, 1 - u, v, 1 - v); |
| r2 emssr | 745 | if (m > 0.28) return 'center'; |
| r2 emssr | 746 | if (m === u) return 'left'; |
| r2 emssr | 747 | if (m === 1 - u) return 'right'; |
| r2 emssr | 748 | return m === v ? 'top' : 'bottom'; |
| r2 emssr | 749 | }; |
| r2 emssr | 750 | d.ondragover = e => { |
| r2 emssr | 751 | if (!dragPane) return; |
| r2 emssr | 752 | e.preventDefault(); |
| r2 emssr | 753 | const s = { left: [0, 0, '50%', '100%'], right: ['50%', 0, '50%', '100%'], |
| r2 emssr | 754 | top: [0, 0, '100%', '50%'], bottom: [0, '50%', '100%', '50%'], |
| r2 emssr | 755 | center: [0, 0, '100%', '100%'] }[zoneOf(e)]; |
| r2 emssr | 756 | hint.hidden = false; |
| r2 emssr | 757 | hint.style.left = typeof s[0] === 'number' ? s[0] + 'px' : s[0]; |
| r2 emssr | 758 | hint.style.top = typeof s[1] === 'number' ? s[1] + 'px' : s[1]; |
| r2 emssr | 759 | hint.style.width = s[2]; |
| r2 emssr | 760 | hint.style.height = s[3]; |
| r2 emssr | 761 | }; |
| r2 emssr | 762 | d.ondragleave = () => { hint.hidden = true; }; |
| r2 emssr | 763 | d.ondrop = e => { |
| r2 emssr | 764 | e.preventDefault(); |
| r2 emssr | 765 | hint.hidden = true; |
| r2 emssr | 766 | if (!dragPane) return; |
| r2 emssr | 767 | const z = zoneOf(e), moved = dragPane; |
| r2 emssr | 768 | if (dragFrom && dragFrom !== node) removeNode(dragFrom); |
| r2 emssr | 769 | dropInto(node, moved, z); |
| r2 emssr | 770 | dragPane = dragFrom = null; |
| r2 emssr | 771 | renderGrid(); |
| r2 emssr | 772 | }; |
| r2 emssr | 773 | return d; |
| r2 emssr | 774 | } |
| r2 emssr | 775 | |
| r2 emssr | 776 | function dropInto(node, pane, zone) { |
| r2 emssr | 777 | if (zone === 'center') { node.pane = pane; return; } |
| r2 emssr | 778 | const fresh = { t: 'leaf', pane: pane }; |
| r2 emssr | 779 | const here = { t: 'leaf', pane: node.pane }; |
| r2 emssr | 780 | const dir = (zone === 'left' || zone === 'right') ? 'row' : 'col'; |
| r2 emssr | 781 | const first = (zone === 'left' || zone === 'top') ? fresh : here; |
| r2 emssr | 782 | node.t = 'split'; |
| r2 emssr | 783 | node.dir = dir; |
| r2 emssr | 784 | node.ratio = 0.5; |
| r2 emssr | 785 | node.a = first; |
| r2 emssr | 786 | node.b = first === fresh ? here : fresh; |
| r2 emssr | 787 | delete node.pane; |
| r2 emssr | 788 | } |
| r2 emssr | 789 | |
| r2 emssr | 790 | function removeNode(target) { |
| r2 emssr | 791 | const parent = findParent(layout, target); |
| r2 emssr | 792 | if (!parent) return; |
| r2 emssr | 793 | const other = parent.a === target ? parent.b : parent.a; |
| r2 emssr | 794 | Object.keys(parent).forEach(k => delete parent[k]); |
| r2 emssr | 795 | Object.assign(parent, other); |
| r2 emssr | 796 | } |
| r2 emssr | 797 | |
| r2 emssr | 798 | function findParent(n, target) { |
| r2 emssr | 799 | if (!n || n.t !== 'split') return null; |
| r2 emssr | 800 | if (n.a === target || n.b === target) return n; |
| r2 emssr | 801 | return findParent(n.a, target) || findParent(n.b, target); |
| r2 emssr | 802 | } |
| r2 emssr | 803 | |
| r2 emssr | 804 | function makeNode(node, parent) { |
| r2 emssr | 805 | if (node.t === 'leaf') return makeLeaf(node, parent); |
| r2 emssr | 806 | const d = document.createElement('div'); |
| r2 emssr | 807 | d.className = 'node ' + node.dir; |
| r2 emssr | 808 | const a = makeNode(node.a, node), b = makeNode(node.b, node); |
| r2 emssr | 809 | a.style.flex = node.ratio + ' 1 0'; |
| r2 emssr | 810 | b.style.flex = (1 - node.ratio) + ' 1 0'; |
| r2 emssr | 811 | const gap = document.createElement('div'); |
| r2 emssr | 812 | gap.className = 'gap'; |
| r2 emssr | 813 | gap.onmousedown = e => { |
| r2 emssr | 814 | e.preventDefault(); |
| r2 emssr | 815 | const rect = d.getBoundingClientRect(); |
| r2 emssr | 816 | const move = ev => { |
| r2 emssr | 817 | const r = node.dir === 'row' |
| r2 emssr | 818 | ? (ev.clientX - rect.left) / rect.width |
| r2 emssr | 819 | : (ev.clientY - rect.top) / rect.height; |
| r2 emssr | 820 | node.ratio = Math.min(0.88, Math.max(0.12, r)); |
| r2 emssr | 821 | a.style.flex = node.ratio + ' 1 0'; |
| r2 emssr | 822 | b.style.flex = (1 - node.ratio) + ' 1 0'; |
| r2 emssr | 823 | }; |
| r2 emssr | 824 | const up = () => { |
| r2 emssr | 825 | window.removeEventListener('mousemove', move); |
| r2 emssr | 826 | window.removeEventListener('mouseup', up); |
| r2 emssr | 827 | saveLayout(); |
| r2 emssr | 828 | }; |
| r2 emssr | 829 | window.addEventListener('mousemove', move); |
| r2 emssr | 830 | window.addEventListener('mouseup', up); |
| r2 emssr | 831 | }; |
| r2 emssr | 832 | d.append(a, gap, b); |
| r2 emssr | 833 | return d; |
| r2 emssr | 834 | } |
| r2 emssr | 835 | |
| r2 emssr | 836 | function renderGrid() { |
| r2 emssr | 837 | if (solo) { |
| r2 emssr | 838 | const host = document.getElementById('soloScope'); |
| r2 emssr | 839 | host.innerHTML = ''; |
| r2 emssr | 840 | const d = document.createElement('div'); |
| r2 emssr | 841 | d.className = 'solobody'; |
| r2 emssr | 842 | d.appendChild(paneBody(solo)); |
| r2 emssr | 843 | host.appendChild(d); |
| r2 emssr | 844 | return; |
| r2 emssr | 845 | } |
| r2 emssr | 846 | const host = document.getElementById('grid'); |
| r2 emssr | 847 | host.innerHTML = ''; |
| r2 emssr | 848 | host.appendChild(makeNode(layout, null)); |
| r2 emssr | 849 | saveLayout(); |
| r2 emssr | 850 | } |
| r2 emssr | 851 | |
| r2 emssr | 852 | // ===== 入口 ===== |
| r2 emssr | 853 | |
| r2 emssr | 854 | async function main() { |
| r2 emssr | 855 | document.body.dataset.mode = solo ? 'solo' : 'full'; |
| r2 emssr | 856 | layout = solo ? null : loadLayout(); |
| r2 emssr | 857 | if (solo) { |
| r2 emssr | 858 | document.getElementById('solo').hidden = false; |
| r2 emssr | 859 | document.getElementById('soloHead').textContent = |
| r2 emssr | 860 | (PANES[solo] ? PANES[solo][0] : solo) + |
| r2 emssr | 861 | ' — メインウィンドウが送る平文を、このウィンドウの kernel が実行します'; |
| r2 emssr | 862 | } |
| r2 emssr | 863 | await boot(); |
| r2 emssr | 864 | |
| r2 emssr | 865 | if (!solo) { |
| r2 emssr | 866 | await listNotebooks(); |
| r2 emssr | 867 | document.getElementById('pick').onchange = e => openNotebook(e.target.value); |
| r2 emssr | 868 | renderPalette(); |
| r2 emssr | 869 | renderGrid(); |
| r2 emssr | 870 | if (window._nbs.length) await openNotebook(window._nbs[0].file); |
| r2 emssr | 871 | else { nbFile = 'notebook.json'; nb = blank(); renderSheet(); renderToc(); } |
| r2 emssr | 872 | |
| r2 emssr | 873 | // .json を落とすと開く。.cmn を落とすとセルが増える |
| r2 emssr | 874 | const sheet = document.getElementById('sheet'); |
| r2 emssr | 875 | sheet.addEventListener('dragover', e => { if (e.dataTransfer.types.includes('Files')) e.preventDefault(); }); |
| r2 emssr | 876 | sheet.addEventListener('drop', async e => { |
| r2 emssr | 877 | const f = e.dataTransfer.files[0]; |
| r2 emssr | 878 | if (!f) return; |
| r2 emssr | 879 | e.preventDefault(); |
| r2 emssr | 880 | const text = await f.text(); |
| r2 emssr | 881 | if (f.name.endsWith('.json')) adoptNotebook(f.name, JSON.parse(text)); |
| r2 emssr | 882 | else if (f.name.endsWith('.dic')) { nb.dic = text; touch(); renderSheet(); } |
| r2 emssr | 883 | else { nb.cells.push({ kind: 'cmn', text: '', source: text }); touch(); renderSheet(); } |
| r2 emssr | 884 | }); |
| r2 emssr | 885 | window.addEventListener('beforeunload', e => { if (dirty) e.preventDefault(); }); |
| r2 emssr | 886 | } else { |
| r2 emssr | 887 | renderGrid(); |
| r2 emssr | 888 | } |
| r2 emssr | 889 | |
| r2 emssr | 890 | new EventSource('/events').onmessage = ev => { |
| r2 emssr | 891 | const st = JSON.parse(ev.data); |
| r2 emssr | 892 | if (!solo || !py || !st.spec) return; |
| r2 emssr | 893 | lastScope = py.cell(JSON.stringify(st.spec)); |
| r2 emssr | 894 | lastJoint = null; |
| r2 emssr | 895 | if (solo === 'joint' && st.view && st.view.inputs) { |
| r2 emssr | 896 | lastJoint = py.joint(JSON.stringify(Object.assign({}, st.spec, st.view))); |
| r2 emssr | 897 | } |
| r2 emssr | 898 | renderGrid(); |
| r2 emssr | 899 | }; |
| r2 emssr | 900 | } |
| r2 emssr | 901 | |
| r2 emssr | 902 | main(); |