CodeMirror 6 editor for the content shell's edit page — adopted from the old-web-server editor (the removed solver/web-content/code-mirror-6/code.js) and fitted to the new site as progressive enhancement: it upgrades…
/* CodeMirror 6 editor for the content shell's edit page — adopted from the * old-web-server editor (the removed solver/web-content/code-mirror-6/code.js) and fitted to * the new site as **progressive enhancement**: it upgrades the edit form's plain * <textarea name="content"> in place, syncing the buffer back to the textarea so * the existing htmx POST → 5c validation gate flow is untouched (no-JS still * edits the plain textarea and submits). A solution file's read-only view upgrades * its <pre data-cm> the same way, so a file reads the same viewed as edited. * Lazy-imported by site.js only on those pages, so its ~630 KB vendored graph never * loads elsewhere. * * ES module, same-origin ('self'); CodeMirror injects its stylesheet at runtime * (covered by the CSP style-src 'unsafe-inline', §4.7). No eval. */import{EditorView,Compartment,basicSetup,indentUnit,keymap,indentWithTab,placeholder,python,cpp,json,oneDark,}from'/vendor/codemirror/cm6.js';//: file language → the CodeMirror language extension (html/others: none, plain text).constLANG={python,c:cpp,json};constMONO='ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';/* The buffer's chrome is the shell's own: every surface comes from the site.css palette tokens, so the editor is the same shade as the panel around it. The selectors carry `&.cm-editor` rather than a bare `&`: in the dark theme this stacks on top of the vendored oneDark (kept for its syntax colours alone), and the extra class is what outranks oneDark's own background and gutters. *dark* tells CodeMirror which ground the chrome is, for the defaults it derives from that. */functionchrome(dark){returnEditorView.theme({'&.cm-editor':{height:'100%',fontSize:'13px',borderRadius:'10px',background:'var(--bg)',color:'var(--text)',},'&.cm-editor.cm-focused':{outline:'2px solid var(--accent)',outlineOffset:'1px'},'&.cm-editor .cm-scroller':{fontFamily:MONO,lineHeight:'1.55'},'&.cm-editor .cm-content':{padding:'10px 0'},'&.cm-editor .cm-gutters':{background:'var(--surface)',color:'var(--muted)',border:'none',},'&.cm-editor .cm-activeLine':{background:'var(--surface-hi)'},'&.cm-editor .cm-activeLineGutter':{background:'var(--surface-hi)',color:'var(--text)'},'&.cm-editor .cm-cursor, &.cm-editor .cm-dropCursor':{borderLeftColor:'var(--text)'},},{dark});}//: The theme follows the site's (site.js § theme): oneDark's syntax palette under the//: chrome on the dark ground; on the light one the chrome alone, which leaves//: basicSetup's default highlight style — drawn for a light page — to colour the code.//: In a Compartment, so a toggle reconfigures every mounted view in place, buffer,//: cursor and undo history intact.constthemeSlot=newCompartment();functionisDark(){returndocument.documentElement.getAttribute('data-theme')!=='light';}functiontheme(){returnisDark()?[oneDark,chrome(true)]:[chrome(false)];}/* Every live view. The Set is a strong reference, so a view stays reachable until `reap` drops it — which is the point: an editor whose pane has been swapped away must be destroyed (releasing CodeMirror's own listeners), not merely forgotten. */constviews=newSet();//: Destroy the views whose DOM has gone. Editors mount into #content, which htmx//: swaps wholesale, and a swap gives no notice — so the next mount is the moment//: we know one happened, and is when we clear up after the last page.functionreap(){views.forEach((view)=>{if(!view.dom.isConnected){views.delete(view);view.destroy();}});}//: The CodeMirror language extension *el*'s data-language names, if it has one.functionlanguage(el){returnLANG[el.dataset.language]?LANG[el.dataset.language]():[];}functionenhance(textarea){if(textarea.dataset.cmMounted){return;}textarea.dataset.cmMounted='1';constlangExt=language(textarea);consthost=document.createElement('div');host.className='cm-host';textarea.after(host);textarea.style.display='none';// keep it in the form as the submit sourceconstsync=(view)=>{textarea.value=view.state.doc.toString();};constview=newEditorView({parent:host,doc:textarea.value,extensions:[basicSetup,// gutter, history, brackets, search, folding…themeSlot.of(theme()),langExt,...(textarea.placeholder?[placeholder(textarea.placeholder)]:[]),indentUnit.of(' '),// 4-space indent (project style)EditorView.lineWrapping,keymap.of([indentWithTab,{key:'Mod-s',preventDefault:true,run:()=>{textarea.form.requestSubmit();returntrue;}},]),EditorView.updateListener.of((u)=>{if(u.docChanged){sync(view);}}),],});views.add(view);// Belt-and-suspenders: guarantee the textarea holds the latest buffer at submit.textarea.form.addEventListener('submit',()=>sync(view));// The editor often mounts during an htmx swap, before the flex container has its// final height — CodeMirror would then lay out against a 0-height box (gutter// stacked above the content). Re-measure once layout settles so the gutter sits// beside the code and the scroller fills the pane.requestAnimationFrame(()=>view.requestMeasure());returnview;}//: A view that takes no input has no line being worked on, so none is marked.constnoActiveLine=EditorView.theme({'&.cm-editor .cm-activeLine':{background:'transparent'},'&.cm-editor .cm-activeLineGutter':{background:'transparent',color:'var(--muted)'},});//: Upgrade a read-only <pre> into the same view, minus everything that edits.functiondisplay(pre){if(pre.dataset.cmMounted){return;}pre.dataset.cmMounted='1';consthost=document.createElement('div');host.className='cm-host';pre.after(host);pre.style.display='none';constview=newEditorView({parent:host,doc:pre.textContent,extensions:[noActiveLine,basicSetup,themeSlot.of(theme()),language(pre),EditorView.editable.of(false),EditorView.lineWrapping,],});views.add(view);requestAnimationFrame(()=>view.requestMeasure());// as enhance: mounted mid-swap, before layout}window.addEventListener('euler:theme',()=>{reap();views.forEach((view)=>view.dispatch({effects:themeSlot.reconfigure(theme())}));});//: (Re)mount CodeMirror on every enhanceable textarea and read-only <pre> under *root*//: (the swapped #content, or the document on first load). Idempotent per element.exportfunctionmount(root=document){reap();root.querySelectorAll('textarea.editor-buffer[data-cm]').forEach(enhance);root.querySelectorAll('pre[data-cm]').forEach(display);}