Has anyone managed to get the APL bookmarklet working for modern Jupyter?
ChatGPT came up with this, but it only works for clicking (poorly):
// APL toolbar for modern Jupyter / CM6
(function () {
if (window.__apl_toolbar_installed) { document.querySelector('.apl_toolbar').toggleAttribute('hidden'); return; }
window.__apl_toolbar_installed = true;
// --- helpers
const he = s => s.replace(/[<&'"]/g, c => ({'<':'<','&':'&',"'":''','"':'"'}[c]));
const insertIntoTextarea = (t, text) => {
const i = t.selectionStart, j = t.selectionEnd;
t.value = t.value.slice(0, i) + text + t.value.slice(j);
t.selectionStart = t.selectionEnd = i + text.length;
t.focus();
t.dispatchEvent(new Event('input', { bubbles: true }));
};
const insertIntoContentEditable = (el, text) => {
const sel = window.getSelection();
if (!sel.rangeCount) {
// focus and place caret at end
el.focus();
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
const range = sel.getRangeAt(0);
range.deleteContents();
// insert text node(s)
const tn = document.createTextNode(text);
range.insertNode(tn);
// move caret after inserted node
range.setStartAfter(tn);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
// notify CM6 by dispatching input events on nearest editable
let node = el;
el.dispatchEvent(new InputEvent('input', { bubbles: true }));
// try to find ancestor CodeMirror textarea/input to trigger internal handlers
const ta = el.closest('.jp-Notebook, .jp-FileEditor, .jp-Editor')?.querySelector('textarea');
if (ta) { ta.dispatchEvent(new Event('input', { bubbles: true })); }
el.focus();
};
const insertSymbol = s => {
const ae = document.activeElement;
if (!ae) return;
if ((ae.tagName === 'TEXTAREA') || (ae.tagName === 'INPUT' && (ae.type === 'text' || ae.type === 'search'))) {
insertIntoTextarea(ae, s);
return;
}
// try to find nearest contenteditable cm element
let ce = ae;
if (!ce || !ce.isContentEditable) {
// sometimes CodeMirror's actual editable is the .cm-content inside the editor
ce = document.querySelector('.cm-content[contenteditable="true"]:focus') || document.querySelector('.cm-content[contenteditable="true"]');
}
if (ce && ce.isContentEditable) {
insertIntoContentEditable(ce, s);
return;
}
// last resort: insert into body at caret
const sel = window.getSelection();
if (sel.rangeCount) {
const r = sel.getRangeAt(0);
r.deleteContents();
r.insertNode(document.createTextNode(s));
r.collapse(false);
sel.removeAllRanges();
}
};
// --- symbol sets (kept close to original)
const lbs = [
'←',' ', '⍟','○','*','!','?',' ',
'⌹','○','!!','??',' ','|','⌈','⌊',
'⊥','⊤','⊣','⊢',' ','==','≠','≤',
'<','>','≥','≡','≢',' ','∨','∧',
'⍲','⍱',' ','↑','↓','⊂','⊃','⊆',
'⌷','⍋','⍒',' ','⍳','⍸','∊','⍷',
'∪','∩','~',' ','/','\\','⌿','⍀',
',','⍪','⍴','⌽','⊖','⍉',' ','¨','⍨',
'⍣','..','∘','⍛','⍤','⍥','@@',' ',
'⍞','⎕','⍠','⌸','⌺','⌶','⍎','⍕','⋄','⍝','→','⍵','⍺','∇','&&',' ',
'¯','⍬','∆','⍙'
];
// optional small labels for tooltip (kept short)
const tt = {
'←':'assign','⌹':'matrx_inv','⍟':'log','⍳':'indices','∊':'member','⍴':'shape','∘':'compose','⍎':'execute','⍕':'format'
};
// --- build bar
const bar = document.createElement('div');
bar.className = 'apl_toolbar';
bar.style.cssText = `
position:fixed;left:0;right:0;top:0;z-index:2147483647;
font-family: "DejaVu Sans Mono", monospace; background:#f3f3f3;
color:#111;border-bottom:1px solid #bbb;padding:3px 6px;display:flex;flex-wrap:wrap;gap:4px;
`;
// close button
const close = document.createElement('button');
close.innerText = '✖';
close.title = 'Close';
close.style.cssText = 'margin-right:8px';
close.onclick = () => { bar.hidden = true; };
bar.appendChild(close);
// build buttons
lbs.forEach(sym => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'apl_sym';
b.title = tt[sym] || sym;
b.textContent = sym;
b.style.cssText = `
padding:2px 6px;border:1px solid #ccc;border-radius:4px;background:white;
font-size:14px;line-height:1;cursor:pointer;min-width:26px;text-align:center;
`;
b.onclick = e => {
insertSymbol(sym);
e.preventDefault(); e.stopPropagation();
};
bar.appendChild(b);
});
// toggle backquote-mode helper info (for plaintext only)
const info = document.createElement('span');
info.style.cssText = 'margin-left:8px;font-size:12px;color:#444';
info.innerHTML = 'Click symbols to insert. For plain textareas: press ` then key for composed APL characters.';
bar.appendChild(info);
document.body.appendChild(bar);
// push page content down so it doesn't overlap editor top
const _updateMargin = () => { document.body.style.marginTop = bar.clientHeight + 'px'; };
_updateMargin();
window.addEventListener('resize', _updateMargin);
// --- backquote mappings (for plaintext use) copied and simplified from original
const bqk = " =1234567890-qwertyuiop\\asdfghjk∙l;\'zxcvbnm,./`[]+!@#$%^&*()_QWERTYUIOP|ASDFGHJKL:\"ZXCVBNM<>?~{}".replace(/\∙/g,'');
const bqv = "`÷¨¯<≤=≥>≠∨∧×?⍵∊⍴~↑↓⍳○*⊢∙⍺⌈⌊_∇∆∘'⎕⍎⍕∙⊂⊃∩∪⊥⊤|⍝⍀⌿⋄←→⌹⌶⍫⍒⍋⌽⍉⊖⍟⍱⍲!⍰W⍷R⍨YU⍸⍥⍣⊣ASD⍛⍢H⍤⌸⌷≡≢⊆⊇CVB¤∥⍪⍙⍠⌺⍞⍬".replace(/\∙/g,'');
const bqc = {};
for (let i=0;i<bqk.length;i++) bqc[bqk[i]] = bqv[i];
// simple backquote mode for textareas/inputs (press ` then next key to insert mapped char)
let backquoteMode = false;
const handleKeydown = e => {
const ae = document.activeElement;
if (!ae) return;
if (!((ae.tagName === 'TEXTAREA') || (ae.tagName === 'INPUT' && (ae.type === 'text' || ae.type === 'search')))) return;
if (!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
if (e.key === '`') {
backquoteMode = true;
document.body.classList.add('apl_bq');
e.preventDefault();
return;
}
if (backquoteMode) {
backquoteMode = false;
document.body.classList.remove('apl_bq');
const mapped = bqc[e.key] || bqc[e.key.toLowerCase()];
if (mapped) {
insertIntoTextarea(ae, mapped);
e.preventDefault();
return;
}
}
// handle tab-based short completions: check previous 2 chars
if (e.key === 'Tab') {
const t = ae;
const i = t.selectionStart;
if (i >= 2) {
const two = t.value.slice(i-2,i);
// naive set: some common pairs -> symbol mapping
const tabMap = {'<-':'←','<-':'←','/\\':'⍀','..':'∘.'};
const sub = tabMap[two];
if (sub) {
t.value = t.value.slice(0,i-2) + sub + t.value.slice(t.selectionEnd);
t.selectionStart = t.selectionEnd = i-2 + sub.length;
e.preventDefault();
t.dispatchEvent(new Event('input',{bubbles:true}));
}
}
}
}
};
document.addEventListener('keydown', handleKeydown, true);
// small style for backquote mode state
const css = document.createElement('style');
css.innerHTML = `
.apl_toolbar .apl_sym:hover{background:#eee}
body.apl_bq{outline: 2px dashed rgba(0,0,0,0.08);}
`;
document.head.appendChild(css);
// expose a small API to remove toolbar if needed
window.__APLToolbar = {
remove: () => {
bar.remove();
document.body.style.marginTop = '';
document.removeEventListener('keydown', handleKeydown, true);
window.__apl_toolbar_installed = false;
if (css.parentNode) css.remove();
}
};
})();