📦 Out of Context リポジトリ

リポジトリ / emssr / independent_common_compiler

r2
r2 emssr1
// Common scope notebook の表側。
r2 emssr2
//
r2 emssr3
// ウィンドウは二形ある。
r2 emssr4
//   full   notebook と scope を並べる
r2 emssr5
//   solo   ?pane=<名前> で scope 一枚だけ。別のディスプレイへ出すため
r2 emssr6
//
r2 emssr7
// どちらのウィンドウも自分の kernel を持つ。同期するのは平文と初期値だけで、
r2 emssr8
// 計算はウィンドウごとに回す。lane の作業領域は交わらない(20 式 1.2)ので、
r2 emssr9
// 同じ平文を何枚実行しても state が混ざらない。
r2 emssr10
r2 emssr11
const CDN = 'https://cdn.jsdelivr.net/pyodide/v314.0.7/full/';
r2 emssr12
const MDCDN = 'https://cdn.jsdelivr.net/npm/common-scope-web@0/';  // vendor/web が無いときの逃げ道
r2 emssr13
r2 emssr14
const PANES = {
r2 emssr15
  chamber: ['層と机', 'S(k,q,r) = Atom     r = 層境界音 ka の数、q = 机の位置(16 §2)\nfr(Λ_{r+1}) = fr(Λ_r) の未消費部 ‖ 層 r の出力(19 §4.2)', s => drawChamber(s)],
r2 emssr16
  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 emssr17
  tsui: ['ツイ数平面', null, s => drawTsui(s)],
r2 emssr18
  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 emssr19
  joint: ['接合秩', 'F[α][β] = f(α,β)      ρ = rank_𝔽₂ F\nf = ⊕_{r<ρ} p_r(x_A) q_r(x_B)      (Common-Lang/1 定理 4.1)', () => drawJoint(lastJoint)],
r2 emssr20
  text: ['音節と箱', null, s => drawText(s)],
r2 emssr21
};
r2 emssr22
r2 emssr23
const solo = new URLSearchParams(location.search).get('pane');
r2 emssr24
let py = null, nb = null, nbFile = null, dirty = false;
r2 emssr25
let lastScope = null, lastJoint = null, cur = -1;
r2 emssr26
r2 emssr27
// ===== 起動 =====
r2 emssr28
r2 emssr29
function log(msg, cls) {
r2 emssr30
  const li = document.createElement('li');
r2 emssr31
  li.textContent = msg;
r2 emssr32
  if (cls) li.className = cls;
r2 emssr33
  document.getElementById('bootlog').appendChild(li);
r2 emssr34
}
r2 emssr35
r2 emssr36
function loadScript(src) {
r2 emssr37
  return new Promise((ok, no) => {
r2 emssr38
    const s = document.createElement('script');
r2 emssr39
    s.src = src; s.onload = ok; s.onerror = () => no(new Error(src));
r2 emssr40
    document.head.appendChild(s);
r2 emssr41
  });
r2 emssr42
}
r2 emssr43
r2 emssr44
async function boot() {
r2 emssr45
  const rt = await (await fetch('/api/runtime')).json();
r2 emssr46
  if (rt.missing.length) {
r2 emssr47
    log('配信できないファイル: ' + rt.missing.join(', '), 'bad');
r2 emssr48
    document.getElementById('bootmsg').textContent =
r2 emssr49
      'setup.py を実行して runtime/ を揃えてください。';
r2 emssr50
    return;
r2 emssr51
  }
r2 emssr52
  try {
r2 emssr53
    await startRender(rt.web || MDCDN);
r2 emssr54
    log('Markdown と KaTeX を読み込みました', 'ok');
r2 emssr55
  } catch (e) {
r2 emssr56
    log('Markdown の library を読めません。説明は素のまま出ます', 'bad');
r2 emssr57
  }
r2 emssr58
  const index = rt.pyodide || CDN;
r2 emssr59
  log(rt.pyodide ? 'Pyodide はこのフォルダのコピーを使います' : 'Pyodide を CDN から取ります', 'ok');
r2 emssr60
  await loadScript(index + 'pyodide.js');
r2 emssr61
  const pyodide = await loadPyodide({ indexURL: index });
r2 emssr62
  log('CPython ' + pyodide.version + ' がブラウザの中で動きました', 'ok');
r2 emssr63
r2 emssr64
  pyodide.FS.mkdir('/common');
r2 emssr65
  for (const f of rt.files) {
r2 emssr66
    const buf = new Uint8Array(await (await fetch('/runtime/' + f)).arrayBuffer());
r2 emssr67
    pyodide.FS.writeFile('/common/' + f, buf);
r2 emssr68
  }
r2 emssr69
  log('lane64 の実行台と commonc の image を読み込みました(' + rt.files.length + ' 個)', 'ok');
r2 emssr70
r2 emssr71
  pyodide.runPython(await (await fetch('boot.py')).text());
r2 emssr72
  pyodide.runPython('ready()');
r2 emssr73
  py = {
r2 emssr74
    cell: s => JSON.parse(pyodide.globals.get('cell')(s)),
r2 emssr75
    joint: s => JSON.parse(pyodide.globals.get('joint')(s)),
r2 emssr76
  };
r2 emssr77
  const k = document.getElementById('kernel');
r2 emssr78
  k.textContent = 'lane64 稼働中';
r2 emssr79
  k.className = 'tag ok';
r2 emssr80
  document.getElementById('boot').classList.add('done');
r2 emssr81
}
r2 emssr82
r2 emssr83
// ===== ウィンドウ間の同期 =====
r2 emssr84
r2 emssr85
function publish(patch) {
r2 emssr86
  fetch('/state', { method: 'POST', body: JSON.stringify(patch) }).catch(() => { });
r2 emssr87
}
r2 emssr88
r2 emssr89
// ===== 本文 =====
r2 emssr90
//
r2 emssr91
// 説明セルは Markdown で書く。数式は $…$ と $$…$$。画像と動画は assets/ へ置いて
r2 emssr92
// ![](assets/x.png) や <video src="assets/x.mp4" controls> で貼る。描くのは render.js。
r2 emssr93
r2 emssr94
function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;'); }
r2 emssr95
r2 emssr96
function markInto(el, text) { return renderMarkdown(el, text); }
r2 emssr97
r2 emssr98
// 説明の編集欄。書いている間はすぐ下に見え、file を落とすと assets/ へ上がる
r2 emssr99
function attachEditor(area, get, set, onDone) {
r2 emssr100
  const live = document.createElement('div');
r2 emssr101
  live.className = 'md livepreview';
r2 emssr102
  let timer = null;
r2 emssr103
  const draw = () => markInto(live, get());
r2 emssr104
  area.addEventListener('input', () => {
r2 emssr105
    set(area.value);
r2 emssr106
    clearTimeout(timer);
r2 emssr107
    timer = setTimeout(draw, 220);
r2 emssr108
  });
r2 emssr109
  area.addEventListener('dragover', e => {
r2 emssr110
    if (!e.dataTransfer.types.includes('Files')) return;
r2 emssr111
    e.preventDefault();
r2 emssr112
    e.stopPropagation();
r2 emssr113
    area.classList.add('over');
r2 emssr114
  });
r2 emssr115
  area.addEventListener('dragleave', () => area.classList.remove('over'));
r2 emssr116
  area.addEventListener('drop', async e => {
r2 emssr117
    if (!e.dataTransfer.files.length) return;
r2 emssr118
    e.preventDefault();
r2 emssr119
    e.stopPropagation();
r2 emssr120
    area.classList.remove('over');
r2 emssr121
    const lines = [];
r2 emssr122
    for (const f of e.dataTransfer.files) {
r2 emssr123
      try { lines.push(assetMarkdown(await uploadAsset(f), f)); }
r2 emssr124
      catch (err) { alert(err.message); }
r2 emssr125
    }
r2 emssr126
    const at = area.selectionStart;
r2 emssr127
    area.value = area.value.slice(0, at) + '\n' + lines.join('\n') + '\n' + area.value.slice(at);
r2 emssr128
    set(area.value);
r2 emssr129
    draw();
r2 emssr130
    touch();
r2 emssr131
  });
r2 emssr132
  area.addEventListener('paste', async e => {
r2 emssr133
    const files = [...(e.clipboardData.files || [])];
r2 emssr134
    if (!files.length) return;
r2 emssr135
    e.preventDefault();
r2 emssr136
    const lines = [];
r2 emssr137
    for (const f of files) {
r2 emssr138
      try { lines.push(assetMarkdown(await uploadAsset(f), f)); }
r2 emssr139
      catch (err) { alert(err.message); }
r2 emssr140
    }
r2 emssr141
    const at = area.selectionStart;
r2 emssr142
    area.value = area.value.slice(0, at) + lines.join('\n') + area.value.slice(at);
r2 emssr143
    set(area.value);
r2 emssr144
    draw();
r2 emssr145
    touch();
r2 emssr146
  });
r2 emssr147
  area.addEventListener('blur', () => {
r2 emssr148
    clearTimeout(timer);
r2 emssr149
    live.remove();
r2 emssr150
    onDone();
r2 emssr151
  });
r2 emssr152
  const show = () => {
r2 emssr153
    area.after(live);
r2 emssr154
    draw();
r2 emssr155
  };
r2 emssr156
  return show;
r2 emssr157
}
r2 emssr158
r2 emssr159
// ===== notebook の出し入れ =====
r2 emssr160
//
r2 emssr161
// 保存先は server の notebooks/ になる。書きかけは localStorage にも置くので、
r2 emssr162
// 保存の前にタブを閉じても戻せる。
r2 emssr163
r2 emssr164
const DRAFT = 'common-scope-draft:';
r2 emssr165
r2 emssr166
function blank() {
r2 emssr167
  return {
r2 emssr168
    title: '名前のない notebook',
r2 emssr169
    lead: '',
r2 emssr170
    dic: '',
r2 emssr171
    cells: [{ kind: 'cmn', text: '', source: 'hu hu ka, no ka.' }],
r2 emssr172
  };
r2 emssr173
}
r2 emssr174
r2 emssr175
function touch() {
r2 emssr176
  dirty = true;
r2 emssr177
  try { localStorage.setItem(DRAFT + nbFile, JSON.stringify(nb)); } catch (e) { /* 保存できなくても動く */ }
r2 emssr178
  const s = document.getElementById('save');
r2 emssr179
  if (s) s.textContent = '保存 *';
r2 emssr180
}
r2 emssr181
r2 emssr182
function clean() {
r2 emssr183
  dirty = false;
r2 emssr184
  try { localStorage.removeItem(DRAFT + nbFile); } catch (e) { /* 同上 */ }
r2 emssr185
  const s = document.getElementById('save');
r2 emssr186
  if (s) s.textContent = '保存';
r2 emssr187
}
r2 emssr188
r2 emssr189
async function listNotebooks() {
r2 emssr190
  window._nbs = await (await fetch('/api/notebooks')).json();
r2 emssr191
  const pick = document.getElementById('pick');
r2 emssr192
  pick.innerHTML = '';
r2 emssr193
  window._nbs.forEach(n => {
r2 emssr194
    const o = document.createElement('option');
r2 emssr195
    o.value = n.file; o.textContent = n.title;
r2 emssr196
    pick.appendChild(o);
r2 emssr197
  });
r2 emssr198
  if (nbFile) pick.value = nbFile;
r2 emssr199
  renderToc();
r2 emssr200
}
r2 emssr201
r2 emssr202
async function openNotebook(file) {
r2 emssr203
  if (dirty && !confirm('保存していない変更があります。破棄して開きますか。')) return;
r2 emssr204
  nbFile = file;
r2 emssr205
  nb = await (await fetch('/api/notebook/' + file)).json();
r2 emssr206
  let draft = null;
r2 emssr207
  try { draft = localStorage.getItem(DRAFT + file); } catch (e) { /* 無ければ保存分を使う */ }
r2 emssr208
  if (draft && confirm('保存していない書きかけがあります。そちらを開きますか。')) {
r2 emssr209
    nb = JSON.parse(draft);
r2 emssr210
    dirty = true;
r2 emssr211
  } else {
r2 emssr212
    try { localStorage.removeItem(DRAFT + file); } catch (e) { /* 同上 */ }
r2 emssr213
    dirty = false;
r2 emssr214
  }
r2 emssr215
  nb.cells.forEach(c => { c.result = null; });
r2 emssr216
  document.getElementById('pick').value = file;
r2 emssr217
  renderSheet();
r2 emssr218
  renderToc();
r2 emssr219
  publish({ notebook: file });
r2 emssr220
}
r2 emssr221
r2 emssr222
function adoptNotebook(file, doc) {
r2 emssr223
  nbFile = file;
r2 emssr224
  nb = doc;
r2 emssr225
  nb.cells = nb.cells || [];
r2 emssr226
  renderSheet();
r2 emssr227
  renderToc();
r2 emssr228
  touch();
r2 emssr229
}
r2 emssr230
r2 emssr231
async function saveNotebook(as) {
r2 emssr232
  let file = nbFile;
r2 emssr233
  if (as || !file) {
r2 emssr234
    const guess = (nb.title || 'notebook').replace(/[^\w一-龠ぁ-んァ-ヶー0-9]+/g, '_');
r2 emssr235
    file = prompt('保存する名前', (file || guess + '.json').replace(/\.json$/, '') + '.json');
r2 emssr236
    if (!file) return;
r2 emssr237
  }
r2 emssr238
  const body = JSON.parse(JSON.stringify(nb));
r2 emssr239
  body.cells.forEach(c => { delete c.result; });
r2 emssr240
  const r = await fetch('/api/notebook/' + encodeURIComponent(file),
r2 emssr241
    { method: 'POST', body: JSON.stringify(body) });
r2 emssr242
  if (!r.ok) { alert('保存できません: ' + r.status); return; }
r2 emssr243
  nbFile = (await r.json()).file;
r2 emssr244
  clean();
r2 emssr245
  await listNotebooks();
r2 emssr246
  document.getElementById('pick').value = nbFile;
r2 emssr247
}
r2 emssr248
r2 emssr249
async function deleteNotebook() {
r2 emssr250
  if (!nbFile || !confirm(nbFile + ' を消しますか。')) return;
r2 emssr251
  await fetch('/api/notebook/' + encodeURIComponent(nbFile), { method: 'DELETE' });
r2 emssr252
  try { localStorage.removeItem(DRAFT + nbFile); } catch (e) { /* 同上 */ }
r2 emssr253
  nbFile = null;
r2 emssr254
  await listNotebooks();
r2 emssr255
  if (window._nbs.length) await openNotebook(window._nbs[0].file);
r2 emssr256
  else { nb = blank(); nbFile = 'notebook.json'; renderSheet(); }
r2 emssr257
}
r2 emssr258
r2 emssr259
function download() {
r2 emssr260
  const body = JSON.parse(JSON.stringify(nb));
r2 emssr261
  body.cells.forEach(c => { delete c.result; });
r2 emssr262
  const a = document.createElement('a');
r2 emssr263
  a.href = URL.createObjectURL(new Blob([JSON.stringify(body, null, 2)], { type: 'application/json' }));
r2 emssr264
  a.download = nbFile || 'notebook.json';
r2 emssr265
  a.click();
r2 emssr266
  URL.revokeObjectURL(a.href);
r2 emssr267
}
r2 emssr268
r2 emssr269
// ===== 画面 =====
r2 emssr270
r2 emssr271
function renderToc() {
r2 emssr272
  const t = document.getElementById('toc');
r2 emssr273
  t.innerHTML = '';
r2 emssr274
  const head = document.createElement('div');
r2 emssr275
  head.className = 'head';
r2 emssr276
  head.textContent = 'NOTEBOOK';
r2 emssr277
  t.appendChild(head);
r2 emssr278
  (window._nbs || []).forEach(n => {
r2 emssr279
    const a = document.createElement('a');
r2 emssr280
    a.textContent = n.title;
r2 emssr281
    a.href = '#';
r2 emssr282
    a.title = n.file;
r2 emssr283
    if (n.file === nbFile) a.className = 'on';
r2 emssr284
    a.onclick = e => { e.preventDefault(); openNotebook(n.file); };
r2 emssr285
    t.appendChild(a);
r2 emssr286
  });
r2 emssr287
  const add = document.createElement('a');
r2 emssr288
  add.textContent = '+ 新しい notebook';
r2 emssr289
  add.href = '#';
r2 emssr290
  add.className = 'add';
r2 emssr291
  add.onclick = e => {
r2 emssr292
    e.preventDefault();
r2 emssr293
    if (dirty && !confirm('保存していない変更があります。破棄しますか。')) return;
r2 emssr294
    nbFile = 'notebook.json';
r2 emssr295
    nb = blank();
r2 emssr296
    dirty = false;
r2 emssr297
    renderSheet();
r2 emssr298
    renderToc();
r2 emssr299
  };
r2 emssr300
  t.appendChild(add);
r2 emssr301
}
r2 emssr302
r2 emssr303
function button(label, title, fn) {
r2 emssr304
  const b = document.createElement('button');
r2 emssr305
  b.textContent = label;
r2 emssr306
  if (title) b.title = title;
r2 emssr307
  b.onclick = fn;
r2 emssr308
  return b;
r2 emssr309
}
r2 emssr310
r2 emssr311
function field(label, value, fn, width) {
r2 emssr312
  const w = document.createElement('label');
r2 emssr313
  w.className = 'field';
r2 emssr314
  w.appendChild(document.createTextNode(label));
r2 emssr315
  const i = document.createElement('input');
r2 emssr316
  i.value = value === undefined || value === null ? '' : value;
r2 emssr317
  if (width) i.style.width = width;
r2 emssr318
  i.oninput = () => { fn(i.value); touch(); };
r2 emssr319
  w.appendChild(i);
r2 emssr320
  return w;
r2 emssr321
}
r2 emssr322
r2 emssr323
function renderSheet() {
r2 emssr324
  const host = document.getElementById('sheet');
r2 emssr325
  host.innerHTML = '';
r2 emssr326
r2 emssr327
  // ---- 道具立て ----
r2 emssr328
  const bar = document.createElement('div');
r2 emssr329
  bar.className = 'toolbar';
r2 emssr330
  const save = button('保存', 'notebooks/ へ書き込みます', () => saveNotebook(false));
r2 emssr331
  save.id = 'save';
r2 emssr332
  if (dirty) save.textContent = '保存 *';
r2 emssr333
  bar.append(
r2 emssr334
    button('すべて実行', '上から順に全ての文を実行します', runAll),
r2 emssr335
    save,
r2 emssr336
    button('名前を付けて保存', null, () => saveNotebook(true)),
r2 emssr337
    button('ダウンロード', '.json として手元へ保存します', download),
r2 emssr338
    button('削除', 'この notebook を消します', deleteNotebook));
r2 emssr339
  host.appendChild(bar);
r2 emssr340
r2 emssr341
  // ---- 表題と説明と採用語彙 ----
r2 emssr342
  const head = document.createElement('div');
r2 emssr343
  head.id = 'intro';
r2 emssr344
  const h1 = document.createElement('input');
r2 emssr345
  h1.className = 'title';
r2 emssr346
  h1.value = nb.title || '';
r2 emssr347
  h1.placeholder = '表題';
r2 emssr348
  h1.oninput = () => { nb.title = h1.value; touch(); };
r2 emssr349
  head.appendChild(h1);
r2 emssr350
r2 emssr351
  const lead = document.createElement('div');
r2 emssr352
  lead.className = 'lead md';
r2 emssr353
  markInto(lead, nb.lead || '');
r2 emssr354
  const leadEdit = document.createElement('textarea');
r2 emssr355
  leadEdit.className = 'leadedit';
r2 emssr356
  leadEdit.value = nb.lead || '';
r2 emssr357
  leadEdit.placeholder = '説明。$$ で数式、| で表、** で強調';
r2 emssr358
  leadEdit.hidden = true;
r2 emssr359
  const showLead = attachEditor(leadEdit, () => leadEdit.value,
r2 emssr360
    v => { nb.lead = v; touch(); },
r2 emssr361
    () => { leadEdit.hidden = true; lead.hidden = false; markInto(lead, nb.lead || ''); });
r2 emssr362
  lead.onclick = () => {
r2 emssr363
    lead.hidden = true;
r2 emssr364
    leadEdit.hidden = false;
r2 emssr365
    leadEdit.style.height = Math.max(140, leadEdit.scrollHeight) + 'px';
r2 emssr366
    leadEdit.focus();
r2 emssr367
    showLead();
r2 emssr368
  };
r2 emssr369
  lead.title = 'クリックすると書き換えられます';
r2 emssr370
  head.append(lead, leadEdit);
r2 emssr371
r2 emssr372
  const dic = document.createElement('details');
r2 emssr373
  dic.className = 'settings';
r2 emssr374
  const sum = document.createElement('summary');
r2 emssr375
  sum.textContent = '採用語彙(.dic)';
r2 emssr376
  const dicText = document.createElement('textarea');
r2 emssr377
  dicText.value = nb.dic || '';
r2 emssr378
  dicText.placeholder = '見出し ⟨tab⟩ 文。=N で数だけの語\n例:  pepe\t=8';
r2 emssr379
  dicText.rows = 6;
r2 emssr380
  dicText.oninput = () => { nb.dic = dicText.value; touch(); };
r2 emssr381
  dic.append(sum, dicText);
r2 emssr382
  head.appendChild(dic);
r2 emssr383
  host.appendChild(head);
r2 emssr384
r2 emssr385
  // ---- セル ----
r2 emssr386
  const cells = document.createElement('div');
r2 emssr387
  cells.id = 'cells';
r2 emssr388
  host.appendChild(cells);
r2 emssr389
  redrawCells();
r2 emssr390
r2 emssr391
  host.appendChild(adder(nb.cells.length));
r2 emssr392
}
r2 emssr393
r2 emssr394
function adder(at) {
r2 emssr395
  const d = document.createElement('div');
r2 emssr396
  d.className = 'adder';
r2 emssr397
  d.append(
r2 emssr398
    button('+ Common', 'Common の文を書くセルを足します', () => {
r2 emssr399
      nb.cells.splice(at, 0, { kind: 'cmn', text: '', source: '' });
r2 emssr400
      touch(); redrawCells();
r2 emssr401
    }),
r2 emssr402
    button('+ 説明', '文章だけのセルを足します', () => {
r2 emssr403
      nb.cells.splice(at, 0, { kind: 'note', text: '' });
r2 emssr404
      touch(); redrawCells();
r2 emssr405
    }));
r2 emssr406
  return d;
r2 emssr407
}
r2 emssr408
r2 emssr409
function redrawCells() {
r2 emssr410
  const host = document.getElementById('cells');
r2 emssr411
  host.innerHTML = '';
r2 emssr412
  nb.cells.forEach((c, i) => {
r2 emssr413
    host.appendChild(renderCell(c, i));
r2 emssr414
    host.appendChild(adder(i + 1));
r2 emssr415
  });
r2 emssr416
}
r2 emssr417
r2 emssr418
function move(i, d) {
r2 emssr419
  const j = i + d;
r2 emssr420
  if (j < 0 || j >= nb.cells.length) return;
r2 emssr421
  const [c] = nb.cells.splice(i, 1);
r2 emssr422
  nb.cells.splice(j, 0, c);
r2 emssr423
  touch(); redrawCells();
r2 emssr424
}
r2 emssr425
r2 emssr426
function renderCell(c, i) {
r2 emssr427
  const d = document.createElement('div');
r2 emssr428
  d.className = 'cell' + (c.kind === 'note' ? ' note' : '');
r2 emssr429
r2 emssr430
  // 説明。クリックで書き換えに変わる
r2 emssr431
  const note = document.createElement('div');
r2 emssr432
  note.className = 'note md';
r2 emssr433
  if (c.text) markInto(note, c.text);
r2 emssr434
  else note.innerHTML = '<p class="empty">クリックすると説明を書けます。Markdown と $数式$ が使えます</p>';
r2 emssr435
  const noteEdit = document.createElement('textarea');
r2 emssr436
  noteEdit.className = 'noteedit';
r2 emssr437
  noteEdit.value = c.text || '';
r2 emssr438
  noteEdit.hidden = true;
r2 emssr439
  const showNote = attachEditor(noteEdit, () => noteEdit.value,
r2 emssr440
    v => { c.text = v; touch(); },
r2 emssr441
    () => {
r2 emssr442
      noteEdit.hidden = true; note.hidden = false;
r2 emssr443
      if (c.text) markInto(note, c.text);
r2 emssr444
      else note.innerHTML = '<p class="empty">クリックすると説明を書けます。Markdown と $数式$ が使えます</p>';
r2 emssr445
    });
r2 emssr446
  note.onclick = () => {
r2 emssr447
    note.hidden = true; noteEdit.hidden = false;
r2 emssr448
    noteEdit.style.height = Math.max(110, noteEdit.scrollHeight) + 'px';
r2 emssr449
    noteEdit.focus();
r2 emssr450
    showNote();
r2 emssr451
  };
r2 emssr452
  d.append(note, noteEdit);
r2 emssr453
r2 emssr454
  const tools = document.createElement('div');
r2 emssr455
  tools.className = 'tools';
r2 emssr456
  tools.append(
r2 emssr457
    button('↑', '上へ', () => move(i, -1)),
r2 emssr458
    button('↓', '下へ', () => move(i, 1)),
r2 emssr459
    button('複製', null, () => {
r2 emssr460
      nb.cells.splice(i + 1, 0, JSON.parse(JSON.stringify({ ...c, result: null })));
r2 emssr461
      touch(); redrawCells();
r2 emssr462
    }),
r2 emssr463
    button('削除', null, () => {
r2 emssr464
      const has = (c.source || '').trim() || (c.text || '').trim();
r2 emssr465
      if (has && !confirm('このセルを消しますか。')) return;
r2 emssr466
      nb.cells.splice(i, 1);
r2 emssr467
      touch(); redrawCells();
r2 emssr468
    }));
r2 emssr469
r2 emssr470
  if (c.kind === 'note') {
r2 emssr471
    d.appendChild(tools);
r2 emssr472
    return d;
r2 emssr473
  }
r2 emssr474
r2 emssr475
  const row = document.createElement('div');
r2 emssr476
  row.className = 'row';
r2 emssr477
  const gut = document.createElement('div');
r2 emssr478
  gut.className = 'gut';
r2 emssr479
  gut.textContent = '[' + (i + 1) + ']';
r2 emssr480
  const ta = document.createElement('textarea');
r2 emssr481
  ta.className = 'src';
r2 emssr482
  ta.value = c.source || '';
r2 emssr483
  ta.spellcheck = false;
r2 emssr484
  ta.placeholder = 'hu hu ka, no ka.';
r2 emssr485
  ta.rows = Math.max(2, (c.source || '').split('\n').length);
r2 emssr486
  ta.oninput = () => { c.source = ta.value; touch(); };
r2 emssr487
  ta.onkeydown = e => {
r2 emssr488
    if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); run(i); }
r2 emssr489
  };
r2 emssr490
  ta.ondragover = e => { e.preventDefault(); ta.classList.add('over'); };
r2 emssr491
  ta.ondragleave = () => ta.classList.remove('over');
r2 emssr492
  ta.ondrop = async e => {
r2 emssr493
    e.preventDefault();
r2 emssr494
    ta.classList.remove('over');
r2 emssr495
    const f = e.dataTransfer.files[0];
r2 emssr496
    if (!f) return;
r2 emssr497
    ta.value = c.source = await f.text();
r2 emssr498
    ta.rows = Math.max(2, c.source.split('\n').length);
r2 emssr499
    touch();
r2 emssr500
    run(i);
r2 emssr501
  };
r2 emssr502
  row.append(gut, ta);
r2 emssr503
  d.appendChild(row);
r2 emssr504
r2 emssr505
  const bar = document.createElement('div');
r2 emssr506
  bar.className = 'bar';
r2 emssr507
  bar.appendChild(button('Run', 'Ctrl+Enter', () => run(i)));
r2 emssr508
  const hint = document.createElement('span');
r2 emssr509
  hint.className = 'hint';
r2 emssr510
  hint.textContent = 'Ctrl+Enter で実行。.cmn をドロップすると差し替えます';
r2 emssr511
  bar.append(hint, tools);
r2 emssr512
  d.appendChild(bar);
r2 emssr513
r2 emssr514
  // ---- セルの設定 ----
r2 emssr515
  const set = document.createElement('details');
r2 emssr516
  set.className = 'settings';
r2 emssr517
  const sum = document.createElement('summary');
r2 emssr518
  sum.textContent = 'このセルの設定';
r2 emssr519
  set.appendChild(sum);
r2 emssr520
  const grid = document.createElement('div');
r2 emssr521
  grid.className = 'fields';
r2 emssr522
  grid.append(
r2 emssr523
    field('初期値(語=値)', seedText(c.seeds), v => { c.seeds = parseSeeds(v); }, '13em'),
r2 emssr524
    field('語数', c.words || '', v => { c.words = v ? Number(v) : undefined; }, '7em'),
r2 emssr525
    field('入力の語(接合秩)', (c.inputs || []).join(','), v => {
r2 emssr526
      c.inputs = v.trim() ? v.split(',').map(x => Number(x.trim())) : undefined;
r2 emssr527
    }, '9em'),
r2 emssr528
    field('カット κ', c.cut === undefined ? '' : c.cut, v => {
r2 emssr529
      c.cut = v === '' ? undefined : Number(v);
r2 emssr530
    }, '4em'),
r2 emssr531
    field('読む語', c.out || '', v => { c.out = v ? Number(v) : undefined; }, '4em'));
r2 emssr532
  set.appendChild(grid);
r2 emssr533
  d.appendChild(set);
r2 emssr534
r2 emssr535
  const out = document.createElement('div');
r2 emssr536
  out.className = 'out';
r2 emssr537
  out.id = 'out' + i;
r2 emssr538
  d.appendChild(out);
r2 emssr539
  if (c.result) showResult(i, c.result);
r2 emssr540
  return d;
r2 emssr541
}
r2 emssr542
r2 emssr543
function seedText(s) {
r2 emssr544
  return s ? Object.entries(s).map(([k, v]) => k + '=' + v).join(' ') : '';
r2 emssr545
}
r2 emssr546
r2 emssr547
function parseSeeds(v) {
r2 emssr548
  const out = {};
r2 emssr549
  for (const t of v.split(/[\s,]+/)) {
r2 emssr550
    const m = t.match(/^(\d+)=(-?\d+)$/);
r2 emssr551
    if (m) out[m[1]] = Number(m[2]);
r2 emssr552
  }
r2 emssr553
  return Object.keys(out).length ? out : undefined;
r2 emssr554
}
r2 emssr555
r2 emssr556
// ===== 実行 =====
r2 emssr557
r2 emssr558
function specOf(c) {
r2 emssr559
  return {
r2 emssr560
    source: c.source, dic: nb.dic || null, seeds: c.seeds || null,
r2 emssr561
    words: c.words || null, budget: c.budget || null,
r2 emssr562
  };
r2 emssr563
}
r2 emssr564
r2 emssr565
function viewOf(c) {
r2 emssr566
  return { inputs: c.inputs || null, cut: c.cut === undefined ? null : c.cut, out: c.out || 0 };
r2 emssr567
}
r2 emssr568
r2 emssr569
function run(i) {
r2 emssr570
  if (!py) return;
r2 emssr571
  const c = nb.cells[i];
r2 emssr572
  const spec = specOf(c);
r2 emssr573
  lastScope = py.cell(JSON.stringify(spec));
r2 emssr574
  lastJoint = null;
r2 emssr575
  cur = i;
r2 emssr576
  c.result = lastScope;
r2 emssr577
  showResult(i, lastScope);
r2 emssr578
  if (usesJoint()) runJoint(c);
r2 emssr579
  renderGrid();
r2 emssr580
  publish({ cell: i, notebook: nbFile, spec: spec, view: viewOf(c) });
r2 emssr581
}
r2 emssr582
r2 emssr583
function runAll() {
r2 emssr584
  nb.cells.forEach((c, i) => { if (c.kind !== 'note' && (c.source || '').trim()) run(i); });
r2 emssr585
}
r2 emssr586
r2 emssr587
function runJoint(c) {
r2 emssr588
  lastJoint = (py && c && c.inputs)
r2 emssr589
    ? py.joint(JSON.stringify(Object.assign(specOf(c), viewOf(c)))) : null;
r2 emssr590
}
r2 emssr591
r2 emssr592
function showResult(i, sc) {
r2 emssr593
  const host = document.getElementById('out' + i);
r2 emssr594
  if (!host) return;
r2 emssr595
  if (!sc.ok) {
r2 emssr596
    const want = nb.cells[i] && nb.cells[i].expect === 'fail';
r2 emssr597
    host.innerHTML = '<div class="' + (want ? 'expected' : 'err') + '">'
r2 emssr598
      + (want ? '読み手が止めた: ' : '') + esc(sc.error) + '</div>';
r2 emssr599
    return;
r2 emssr600
  }
r2 emssr601
  const tag = sc.report === 0 ? 'tu(成功)' : sc.report === 42 ? 'te(失敗)' : '報告なし';
r2 emssr602
  const vals = sc.gamma.length
r2 emssr603
    ? sc.gamma.map(g => `語${g.word} = ${g.den === 1 ? g.num : g.num + '/' + g.den}`).join(' ')
r2 emssr604
    : '出口境界は空';
r2 emssr605
  host.innerHTML =
r2 emssr606
    '<div class="val">' + vals + '</div>' +
r2 emssr607
    (sc.put ? '<div>Put: ' + esc(sc.put) + '</div>' : '') +
r2 emssr608
    '<div class="dim">' + tag + ' 音節 ' + sc.size.syl + ' 箱 ' + sc.size.box +
r2 emssr609
    ' image ' + sc.size.image + ' byte / ' + sc.image.insns + ' 命令 block ' +
r2 emssr610
    sc.image.blocks.length + ' hash ' + sc.mark + '</div>';
r2 emssr611
}
r2 emssr612
r2 emssr613
// ===== scope のパネル配置 =====
r2 emssr614
//
r2 emssr615
// パネルは二分木で持つ。
r2 emssr616
//
r2 emssr617
//   {t:'leaf', pane}
r2 emssr618
//   {t:'split', dir:'row'|'col', a, b, ratio}
r2 emssr619
//
r2 emssr620
// 上のタブをパネルへドロップすると、落とした縁で分かれる。中央なら差し替える。
r2 emssr621
// パネル名を掴んで別のパネルへ落とすと入れ替わる。ヘッダのボタンでも分けられる。
r2 emssr622
r2 emssr623
const KEY = 'common-scope-layout';
r2 emssr624
let layout = null, dragPane = null, dragFrom = null;
r2 emssr625
r2 emssr626
function defaultLayout() {
r2 emssr627
  return {
r2 emssr628
    t: 'split', dir: 'col', ratio: 0.5,
r2 emssr629
    a: { t: 'leaf', pane: 'chamber' },
r2 emssr630
    b: { t: 'leaf', pane: 'dag' },
r2 emssr631
  };
r2 emssr632
}
r2 emssr633
r2 emssr634
function loadLayout() {
r2 emssr635
  try {
r2 emssr636
    const s = localStorage.getItem(KEY);
r2 emssr637
    if (s) return JSON.parse(s);
r2 emssr638
  } catch (e) { /* localStorage が無い環境では既定の配置で始める */ }
r2 emssr639
  return defaultLayout();
r2 emssr640
}
r2 emssr641
r2 emssr642
function saveLayout() {
r2 emssr643
  try { localStorage.setItem(KEY, JSON.stringify(layout)); } catch (e) { /* 保存できなくても動く */ }
r2 emssr644
}
r2 emssr645
r2 emssr646
function usesJoint() {
r2 emssr647
  let hit = false;
r2 emssr648
  (function walk(n) {
r2 emssr649
    if (!n) return;
r2 emssr650
    if (n.t === 'leaf') { if (n.pane === 'joint') hit = true; return; }
r2 emssr651
    walk(n.a); walk(n.b);
r2 emssr652
  })(layout);
r2 emssr653
  return hit || solo === 'joint';
r2 emssr654
}
r2 emssr655
r2 emssr656
function renderPalette() {
r2 emssr657
  const p = document.getElementById('palette');
r2 emssr658
  p.innerHTML = '';
r2 emssr659
  const h = document.createElement('span');
r2 emssr660
  h.className = 'hint';
r2 emssr661
  h.textContent = 'パネルへドロップすると分かれます';
r2 emssr662
  p.appendChild(h);
r2 emssr663
  for (const key in PANES) {
r2 emssr664
    const c = document.createElement('span');
r2 emssr665
    c.className = 'tab';
r2 emssr666
    c.textContent = PANES[key][0];
r2 emssr667
    c.draggable = true;
r2 emssr668
    c.title = 'ドラッグしてパネルへ';
r2 emssr669
    c.ondragstart = e => {
r2 emssr670
      dragPane = key; dragFrom = null;
r2 emssr671
      e.dataTransfer.effectAllowed = 'copy';
r2 emssr672
      e.dataTransfer.setData('text/plain', key);
r2 emssr673
    };
r2 emssr674
    p.appendChild(c);
r2 emssr675
  }
r2 emssr676
  const reset = document.createElement('span');
r2 emssr677
  reset.className = 'tab';
r2 emssr678
  reset.textContent = '既定に戻す';
r2 emssr679
  reset.style.marginLeft = 'auto';
r2 emssr680
  reset.onclick = () => { layout = defaultLayout(); saveLayout(); renderGrid(); };
r2 emssr681
  p.appendChild(reset);
r2 emssr682
}
r2 emssr683
r2 emssr684
function paneBody(key) {
r2 emssr685
  const [, formula, make] = PANES[key];
r2 emssr686
  if (!lastScope) return nothing('セルを実行してください。');
r2 emssr687
  if (!lastScope.ok) return nothing(lastScope.error);
r2 emssr688
  const d = document.createElement('div');
r2 emssr689
  if (formula) {
r2 emssr690
    const f = document.createElement('div');
r2 emssr691
    f.className = 'formula';
r2 emssr692
    f.textContent = formula;
r2 emssr693
    d.appendChild(f);
r2 emssr694
  }
r2 emssr695
  d.appendChild(make(lastScope));
r2 emssr696
  if (key === 'chamber' && !lastScope.align.ok) {
r2 emssr697
    d.appendChild(nothing('音節と箱の対応: ' + lastScope.align.why));
r2 emssr698
  }
r2 emssr699
  return d;
r2 emssr700
}
r2 emssr701
r2 emssr702
function makeLeaf(node, parent) {
r2 emssr703
  const d = document.createElement('div');
r2 emssr704
  d.className = 'leaf';
r2 emssr705
r2 emssr706
  const head = document.createElement('header');
r2 emssr707
  head.draggable = true;
r2 emssr708
  head.ondragstart = e => {
r2 emssr709
    dragPane = node.pane; dragFrom = node;
r2 emssr710
    e.dataTransfer.effectAllowed = 'move';
r2 emssr711
    e.dataTransfer.setData('text/plain', node.pane);
r2 emssr712
  };
r2 emssr713
  const sel = document.createElement('select');
r2 emssr714
  sel.className = 'name';
r2 emssr715
  for (const k in PANES) {
r2 emssr716
    const o = document.createElement('option');
r2 emssr717
    o.value = k; o.textContent = PANES[k][0];
r2 emssr718
    sel.appendChild(o);
r2 emssr719
  }
r2 emssr720
  sel.value = node.pane;
r2 emssr721
  sel.onchange = () => { node.pane = sel.value; renderGrid(); };
r2 emssr722
  const sp = document.createElement('span');
r2 emssr723
  sp.className = 'sp';
r2 emssr724
  head.append(sel, sp,
r2 emssr725
    button('⊟', '横に分ける', () => { dropInto(node, node.pane, 'bottom'); renderGrid(); }),
r2 emssr726
    button('⊞', '縦に分ける', () => { dropInto(node, node.pane, 'right'); renderGrid(); }),
r2 emssr727
    button('⧉', '別ウィンドウで開く', () => window.open('?pane=' + node.pane, '_blank')),
r2 emssr728
    button('✕', 'このパネルを閉じる', () => { removeNode(node); renderGrid(); }));
r2 emssr729
  if (!parent) head.lastChild.disabled = true;
r2 emssr730
r2 emssr731
  const body = document.createElement('div');
r2 emssr732
  body.className = 'body';
r2 emssr733
  body.appendChild(paneBody(node.pane));
r2 emssr734
  d.append(head, body);
r2 emssr735
r2 emssr736
  // ドロップ。落とした縁で分ける
r2 emssr737
  const hint = document.createElement('div');
r2 emssr738
  hint.className = 'dropmark';
r2 emssr739
  hint.hidden = true;
r2 emssr740
  d.appendChild(hint);
r2 emssr741
  const zoneOf = e => {
r2 emssr742
    const r = d.getBoundingClientRect();
r2 emssr743
    const u = (e.clientX - r.left) / r.width, v = (e.clientY - r.top) / r.height;
r2 emssr744
    const m = Math.min(u, 1 - u, v, 1 - v);
r2 emssr745
    if (m > 0.28) return 'center';
r2 emssr746
    if (m === u) return 'left';
r2 emssr747
    if (m === 1 - u) return 'right';
r2 emssr748
    return m === v ? 'top' : 'bottom';
r2 emssr749
  };
r2 emssr750
  d.ondragover = e => {
r2 emssr751
    if (!dragPane) return;
r2 emssr752
    e.preventDefault();
r2 emssr753
    const s = { left: [0, 0, '50%', '100%'], right: ['50%', 0, '50%', '100%'],
r2 emssr754
                top: [0, 0, '100%', '50%'], bottom: [0, '50%', '100%', '50%'],
r2 emssr755
                center: [0, 0, '100%', '100%'] }[zoneOf(e)];
r2 emssr756
    hint.hidden = false;
r2 emssr757
    hint.style.left = typeof s[0] === 'number' ? s[0] + 'px' : s[0];
r2 emssr758
    hint.style.top = typeof s[1] === 'number' ? s[1] + 'px' : s[1];
r2 emssr759
    hint.style.width = s[2];
r2 emssr760
    hint.style.height = s[3];
r2 emssr761
  };
r2 emssr762
  d.ondragleave = () => { hint.hidden = true; };
r2 emssr763
  d.ondrop = e => {
r2 emssr764
    e.preventDefault();
r2 emssr765
    hint.hidden = true;
r2 emssr766
    if (!dragPane) return;
r2 emssr767
    const z = zoneOf(e), moved = dragPane;
r2 emssr768
    if (dragFrom && dragFrom !== node) removeNode(dragFrom);
r2 emssr769
    dropInto(node, moved, z);
r2 emssr770
    dragPane = dragFrom = null;
r2 emssr771
    renderGrid();
r2 emssr772
  };
r2 emssr773
  return d;
r2 emssr774
}
r2 emssr775
r2 emssr776
function dropInto(node, pane, zone) {
r2 emssr777
  if (zone === 'center') { node.pane = pane; return; }
r2 emssr778
  const fresh = { t: 'leaf', pane: pane };
r2 emssr779
  const here = { t: 'leaf', pane: node.pane };
r2 emssr780
  const dir = (zone === 'left' || zone === 'right') ? 'row' : 'col';
r2 emssr781
  const first = (zone === 'left' || zone === 'top') ? fresh : here;
r2 emssr782
  node.t = 'split';
r2 emssr783
  node.dir = dir;
r2 emssr784
  node.ratio = 0.5;
r2 emssr785
  node.a = first;
r2 emssr786
  node.b = first === fresh ? here : fresh;
r2 emssr787
  delete node.pane;
r2 emssr788
}
r2 emssr789
r2 emssr790
function removeNode(target) {
r2 emssr791
  const parent = findParent(layout, target);
r2 emssr792
  if (!parent) return;
r2 emssr793
  const other = parent.a === target ? parent.b : parent.a;
r2 emssr794
  Object.keys(parent).forEach(k => delete parent[k]);
r2 emssr795
  Object.assign(parent, other);
r2 emssr796
}
r2 emssr797
r2 emssr798
function findParent(n, target) {
r2 emssr799
  if (!n || n.t !== 'split') return null;
r2 emssr800
  if (n.a === target || n.b === target) return n;
r2 emssr801
  return findParent(n.a, target) || findParent(n.b, target);
r2 emssr802
}
r2 emssr803
r2 emssr804
function makeNode(node, parent) {
r2 emssr805
  if (node.t === 'leaf') return makeLeaf(node, parent);
r2 emssr806
  const d = document.createElement('div');
r2 emssr807
  d.className = 'node ' + node.dir;
r2 emssr808
  const a = makeNode(node.a, node), b = makeNode(node.b, node);
r2 emssr809
  a.style.flex = node.ratio + ' 1 0';
r2 emssr810
  b.style.flex = (1 - node.ratio) + ' 1 0';
r2 emssr811
  const gap = document.createElement('div');
r2 emssr812
  gap.className = 'gap';
r2 emssr813
  gap.onmousedown = e => {
r2 emssr814
    e.preventDefault();
r2 emssr815
    const rect = d.getBoundingClientRect();
r2 emssr816
    const move = ev => {
r2 emssr817
      const r = node.dir === 'row'
r2 emssr818
        ? (ev.clientX - rect.left) / rect.width
r2 emssr819
        : (ev.clientY - rect.top) / rect.height;
r2 emssr820
      node.ratio = Math.min(0.88, Math.max(0.12, r));
r2 emssr821
      a.style.flex = node.ratio + ' 1 0';
r2 emssr822
      b.style.flex = (1 - node.ratio) + ' 1 0';
r2 emssr823
    };
r2 emssr824
    const up = () => {
r2 emssr825
      window.removeEventListener('mousemove', move);
r2 emssr826
      window.removeEventListener('mouseup', up);
r2 emssr827
      saveLayout();
r2 emssr828
    };
r2 emssr829
    window.addEventListener('mousemove', move);
r2 emssr830
    window.addEventListener('mouseup', up);
r2 emssr831
  };
r2 emssr832
  d.append(a, gap, b);
r2 emssr833
  return d;
r2 emssr834
}
r2 emssr835
r2 emssr836
function renderGrid() {
r2 emssr837
  if (solo) {
r2 emssr838
    const host = document.getElementById('soloScope');
r2 emssr839
    host.innerHTML = '';
r2 emssr840
    const d = document.createElement('div');
r2 emssr841
    d.className = 'solobody';
r2 emssr842
    d.appendChild(paneBody(solo));
r2 emssr843
    host.appendChild(d);
r2 emssr844
    return;
r2 emssr845
  }
r2 emssr846
  const host = document.getElementById('grid');
r2 emssr847
  host.innerHTML = '';
r2 emssr848
  host.appendChild(makeNode(layout, null));
r2 emssr849
  saveLayout();
r2 emssr850
}
r2 emssr851
r2 emssr852
// ===== 入口 =====
r2 emssr853
r2 emssr854
async function main() {
r2 emssr855
  document.body.dataset.mode = solo ? 'solo' : 'full';
r2 emssr856
  layout = solo ? null : loadLayout();
r2 emssr857
  if (solo) {
r2 emssr858
    document.getElementById('solo').hidden = false;
r2 emssr859
    document.getElementById('soloHead').textContent =
r2 emssr860
      (PANES[solo] ? PANES[solo][0] : solo) +
r2 emssr861
      ' — メインウィンドウが送る平文を、このウィンドウの kernel が実行します';
r2 emssr862
  }
r2 emssr863
  await boot();
r2 emssr864
r2 emssr865
  if (!solo) {
r2 emssr866
    await listNotebooks();
r2 emssr867
    document.getElementById('pick').onchange = e => openNotebook(e.target.value);
r2 emssr868
    renderPalette();
r2 emssr869
    renderGrid();
r2 emssr870
    if (window._nbs.length) await openNotebook(window._nbs[0].file);
r2 emssr871
    else { nbFile = 'notebook.json'; nb = blank(); renderSheet(); renderToc(); }
r2 emssr872
r2 emssr873
    // .json を落とすと開く。.cmn を落とすとセルが増える
r2 emssr874
    const sheet = document.getElementById('sheet');
r2 emssr875
    sheet.addEventListener('dragover', e => { if (e.dataTransfer.types.includes('Files')) e.preventDefault(); });
r2 emssr876
    sheet.addEventListener('drop', async e => {
r2 emssr877
      const f = e.dataTransfer.files[0];
r2 emssr878
      if (!f) return;
r2 emssr879
      e.preventDefault();
r2 emssr880
      const text = await f.text();
r2 emssr881
      if (f.name.endsWith('.json')) adoptNotebook(f.name, JSON.parse(text));
r2 emssr882
      else if (f.name.endsWith('.dic')) { nb.dic = text; touch(); renderSheet(); }
r2 emssr883
      else { nb.cells.push({ kind: 'cmn', text: '', source: text }); touch(); renderSheet(); }
r2 emssr884
    });
r2 emssr885
    window.addEventListener('beforeunload', e => { if (dirty) e.preventDefault(); });
r2 emssr886
  } else {
r2 emssr887
    renderGrid();
r2 emssr888
  }
r2 emssr889
r2 emssr890
  new EventSource('/events').onmessage = ev => {
r2 emssr891
    const st = JSON.parse(ev.data);
r2 emssr892
    if (!solo || !py || !st.spec) return;
r2 emssr893
    lastScope = py.cell(JSON.stringify(st.spec));
r2 emssr894
    lastJoint = null;
r2 emssr895
    if (solo === 'joint' && st.view && st.view.inputs) {
r2 emssr896
      lastJoint = py.joint(JSON.stringify(Object.assign({}, st.spec, st.view)));
r2 emssr897
    }
r2 emssr898
    renderGrid();
r2 emssr899
  };
r2 emssr900
}
r2 emssr901
r2 emssr902
main();