AGENT_GUIDE.md was ~33k tokens, loaded in full by every agent that followed AGENTS.md's 'read it first', whatever the task. A third of it was lookups an agent can get from the code (file map, route table, env var table, settings key list, static asset list) and had already gone stale in several places. - AGENTS.md (~1.8k tokens, always loaded): what the project is, a 'before you touch X, read Y' table, the rules that always apply, workflow (including that a push to master publishes the Docker image), and the traps that have each broken real data or a real phone. It keeps the tool-managed graft block as is. - docs/agents/*.md (11 files, read on demand): the hard-won 'why' and 'do not regress' contracts by topic, sliced from the old guide with nothing hand-rewritten. The derivable lookups are dropped; the few non-derivable facts from them (git-ignored dev compose, built bundles, reference.docx) moved to testing-and-dev.md. - pointer comments at the top of the contract-heavy source files, so the rule surfaces at the point of editing - tests/agentDocs.test.js keeps the docs honest: AGENTS.md has a size budget, every topic file is linked and says when to read it, and any file, function or npm script a doc names must still exist - README and two code comments no longer point at AGENT_GUIDE.md
5.7 KiB
Markdown-mode extras (public/cm-extras.js)
Read this before: changing inline image previews, chips, task checkboxes, folding, tables, paste-as-markdown or the CodeMirror bundle. This file records why and what not to regress. It deliberately does not say where code lives (ask the code index, or grep): maps go stale, reasons do not.
Markdown-mode extras (public/cm-extras.js)
initCM() spreads _cmExtraExtensions(C) (try/catch-guarded: a failure must never stop the editor mounting) into the CM6 extension list. The extras need extra symbols from the bundle (Decoration, WidgetType, StateField, ViewPlugin, showPanel, hoverTooltip, fold APIs, syntaxTree, Prec, ...) — they are exported from cm-build/index.js; if you add an extra that needs another CM symbol, export it there and rebuild with npm run build:cm.
User settings (Settings → Markdown editor; app/settingsService.js, whitelisted in app/routes/api.js, exposed through _joplockConfig in pages.js, read in app.js as _mdInlineWidgets / _mdLivePreview / _mdStatusBar / _mdFolding):
mdInlineWidgets(default ON): image previews, attachment/note-link chips, task checkboxes, code-block background + Copy button.mdLivePreview(default OFF): hide#,**,*,~~, backticks and[](url)marks except on the heading line / inside the construct the caret touches. Only when the editor has focus.mdStatusBar(default ON, desktop shell only: never created in the mobile shell): bottomshowPanelwith word count, reading time, Ln/Col (or selection word count) and an Outline popup.mdFolding(default ON): fold gutter (headings, lists, fenced code) — the theme hides.cm-guttersentirely when OFF.- Always on: paste-as-markdown, URL-over-selection → link, table helper, hover info on
:/idlinks, and the file-drop claim below.
How it fits together (details that bit us — keep them):
- One
StateField(modelField) holdsscanDoc(doc)output (images, links, tasks, headings, fences; nothing inside fenced code is ever scanned) and provides the block image widgets (block decorations cannot come from aViewPlugin). AViewPluginbuilds the inline decorations (chips, checkboxes, code lines, live-preview hides) forview.visibleRangesonly. Docs over ~2 MB skip scanning. - Image rows are block widgets under the
/<img src=":/id">line; the source line stays visible and editable. Previews load throughhooks.fetchResourceBlobinto a refcounted blob-URL cache because/resources/:idisCache-Control: no-store. Click selects the source reference (Delete removes the image); double-click opens the lightbox. - Resize writes
<img src=":/id" alt=".." width="N" />— byte-identical to the TurndownjoplinImgrule and understood byrenderMarkdown(), so rendered mode honours the width and round-trips it. Double-clicking the handle resets to plain. Widget positions go stale as text above changes, solocateImage()re-finds the line fromview.posAtDOM()before every edit. - Resize/checkbox edits are ignored when the editor is read-only (
canEdit()checkscontenteditable="false", which is what_applyFormReadonlysets). - CM6 does not repaint its cursor/selection layers when a block widget changes height (image finishing its load).
relayout()re-asserts the selection (addToHistory:false) after each load;requestMeasure()alone leaves the caret painted at a stale y. - CodeMirror's built-in drop handler reads dropped text-like files (.txt, .svg, .json, ...) and inserts their contents. The app's own listener uploads them, so the extras claim file drops with a
domEventHandlers({drop})returningtrue(handlers from extensions run before the built-in). Without it a dropped text file is both pasted AND uploaded. - Paste: the app's capture-phase
pastelistener (initCM) uploads image/file clipboard items; the extras'pastehandler converts rich HTML viahooks.htmlToMarkdown=_pasteHtmlToMarkdown()(the shared Turndown instance) only when the HTML has real structure (shouldConvertHtml), never inside fenced code, never for VS Code clipboards, never on Ctrl/Cmd+Shift+V. (lang-markdownalso wraps URL-over-selection natively; the extras' version just guarantees it and escapes brackets.) - Download: image previews get a top-right ⬇ button and attachment chips an inline ⬇ (shown once the target resolves to a resource). Both call
hooks.downloadResource=downloadResource(), so behaviour matches rendered mode (action sheet with View/Save on desktop/PWA, direct download on mobile web). The buttons stop propagation so they neither open the lightbox nor select the image source. - Chips replace
[text](:/id); the kind (note vs attachment) is resolved lazily via_resolveInternalLink()(note headers cache, thenHEAD /resources/:id). Notes open through the note-list row (_openNoteById()), attachments through_openResourceLightbox(). Rendered mode still treats every:/idlink as an attachment — note-link navigation exists only in markdown mode. - Table helper (
Prec.highkeymap): Tab/Shift-Tab align the whole pipe table and move between cells (Tab at the last cell appends a row). Tables inside fences are ignored. Do not bind Enter: people type pipe tables row by row, and an "Enter adds a formatted row" handler corrupts what they type next (it broketable-regression.spec.js). - No regex lookbehind anywhere in
cm-extras.js(a parse-time SyntaxError on iOS < 16.4 would take the whole file down) — pinned by a test. - Coverage:
tests/cmExtras.test.js(pure helpers + real CM6 in jsdom),playwright-tests/md-extras.spec.js(real browser: preview/resize/round-trip, chips + hover, checkboxes, tables, paste, status bar/outline/folding, Copy button, live preview).