- Replace fixed height:1em on .md-blank-line with margin:0.75em to match
paragraph margins; neutralize load-bearing <br> inside blank-line divs
via CSS so spacing is purely margin-driven
- Unify browser-inserted blank paragraph rules (p:empty, div:not([class]))
to use margin:0.75em instead of height:1em
- Add text-decoration:underline to tags.url highlight style and .cm-link/.cm-url
CSS rule so hyperlinks are always underlined in markdown mode
- Add highlightActiveLine bool setting (default true) wired through
settingsService → api allowedKeys → settings UI → _joplockConfig → app.js
- Fix CM theme fontSize to use var(--font-size-note) instead of hardcoded 14px
- Fix CM lineHeight to 1.7 to match .editor-preview
- Fix mobile CSS: remove hardcoded font-size/line-height from .cm-host .cm-content
- Fix Turndown br option: '' instead of '\n' to prevent double-spacing on
render-mode edits (Turndown appends its own \n, so br:'\n' produced \n\n)
- Guard flushSave() syncPV() call behind _previewDirty to prevent Turndown
corruption during autosave when user hasn't edited in preview mode
- Mark ensureEditableAfterPre injected nodes with data-pv-trail and strip them
in htmlToMarkdown clone to prevent extra blank lines after trailing code blocks
- Fix setEditorMode('markdown') to use content comparison for cmSetVal decision
- Increase .editor-preview p margin to 0.75em for better paragraph spacing
- Add 5 new round-trip tests (426 total passing)
The mobile htmx:afterSettle handler was destroying _cmView right after
the global afterSettle handler created it via initEditorPanel/initCM.
Event flow when opening a note on mobile:
1. htmx:afterSwap fires -> destroys any prior _cmView (correct)
2. Global htmx:afterSettle fires -> initEditorPanel() -> initCM() -> _cmView created
3. Mobile htmx:afterSettle fires -> _cmView.destroy() kills the just-created
CM view -> initEditorPanel() returns early (guard already set) -> blank
The destroy in the mobile afterSettle was redundant (afterSwap already
handles it before afterSettle fires) and destructive in markdown-default
mode where initCM runs during afterSettle phase.
Switching to preview then back to markdown worked because setEditorMode
re-creates _cmView via initCM when _cmView is null.
When the same note is open in multiple browsers, changes made in one
browser are now detected and surfaced in the other.
Server:
- itemService.noteFreshnessByUserIdAndJopId: cheap single-row probe
returning {updatedTime, deletedTime} without decoding the note body
- GET /api/web/notes/:id/freshness: serves the probe, marked
isHeartbeat:true so polling does not reset the session idle timer
- PUT /fragments/editor/:id conflict response now sends X-Note-Conflict:1
header so the client can distinguish it from a real save success
Client:
- checkNoteFreshness() runs on every 30s connectivity ping tick and
immediately on visibilitychange (tab focus)
- Clean editor: silently reloads the note fragment
- Dirty editor: shows a banner (same pattern as search-nav-bar) with
'Use server version' and 'Overwrite with mine' buttons
- Deleted note: shows a 'deleted in another window' banner
- _activeEditorIsDirty() uses durable signals (_previewDirty,
_pvSyncTimer, title contenteditable vs hidden input, formHash vs
_savedHash) not the transient autosave-status text
- Banner is server-rendered inside the form (hidden attribute), reuses
search-nav-bar CSS and buttons — no dynamic DOM creation
- htmx:afterRequest skips snapshotHash/setSaved when X-Note-Conflict:1
is present, keeping the editor dirty and the conflict UI visible
- Service worker bumped to v25
Browser-inserted <p><br></p> and <div><br></div> created by Enter in
contenteditable mode rendered at full line-height (~25px), while the
same markdown re-rendered server-side via /fragments/preview produced
.md-blank-line divs at 1em (~15px). Result: visible spacing drift
between live edits and post-toggle rendering.
Add CSS rules to give empty/<br>-only paragraphs and unclassed divs
the same 1em height as .md-blank-line, so contenteditable rendering
stays visually consistent with the canonical renderer.
- add app/proxy/vaultProxyGuard.js: inspects proxied note writes and
deletes before forwarding to upstream Joplin Server
- covers single PUT /api/items/root:/<id>.md:/content, batch PUT
/api/batch_items, single DELETE, and batch DELETE
- rejects with 403 when a vault note body lacks the encrypted marker,
or when a vault note is deleted via the sync proxy
- bodies over 10 MB stream through without inspection (resource blobs)
- unauthenticated requests stream through (upstream handles 401)
- wire guard into createServer.js proxy entry point; replay buffered
body via Readable.from() on allow
- 34 new unit tests, all 394 tests passing
Register BASIC with highlight.js (preview mode) and add it to the
code modal language picker. CodeMirror falls back to plain text in
the modal editor as no CM6 BASIC parser is available.
Use the shared title sanitizer for both SSR and client-side title editing
so note titles are cleaned by one function. Also sanitize note titles on
create in the fragments route and add a regression test for formatted
titles.
syncResponsiveMode already called redrawMobileUI() when crossing
desktop→mobile, but the reverse path only hid the mobile app. A session
that started narrow (mobile mode) never called initNavPanel/initEditorPanel
for the desktop form, leaving the preview and CM host with undefined
display states and stale scroll position.
Mirror the pattern: save wasMobile before clobbering _lastSyncWasMobile,
then call initNavPanel()+initEditorPanel() when wasMobile===true.
Both functions are idempotent (guarded by dataset flags) so a repeat
call on an already-initialised form is a no-op.
- Install markdown-it@14.1.1 as server-side renderer (renderMarkdown only)
- Preserve all Joplin extensions: underline (++..++), checkboxes,
blank-line markers, softbreak→<br>, resource URIs, spellcheck attrs,
hx-* strip, fence/code/image/link render overrides
- Fix blank-line round-trip: emptyDiv/emptyP Turndown rules now return
the BL sentinel instead of '<br>' (which line 611 inflated to 4 newlines)
or '' (which made blank-line edits never save)
- Disable CSS scroll anchoring in preview editor so images flow down
naturally when text is typed above them
- Narrow the Notebook + button label and padding in the nav header
- Bump static asset version to 20260501a
Custom regex renderer's fence extraction required column-0 anchoring,
so any fence inside a list item (or with leading whitespace) was missed
and the loose backticks were mangled by the inline-code regex. Add a
list-item-nested fence pass that outdents the body and preserves the
surrounding list structure as a placeholder, plus relax the column-0
fence regex to allow up to 3 spaces of indentation per CommonMark and
trailing whitespace on the closing fence.
Eliminates fragile ad-hoc DOM toggling that allowed two mobile screens to
render simultaneously. All transitions now go through setMobileState()
reducer; renderMobile() is the only function that writes
.mobile-screen-active. assertSingleActiveScreen() self-heals violations
and traces them. Test asserts the architectural invariant in source.
Also: DB session lookup fails closed on transient errors instead of
crashing; mobile back-save uses formHash (not UI badge); single-screen
CSS via display:none/flex (no transforms); resize debounced without
reload; aria-hidden warning fix.