Add a 'note shows Edited on open' triage section to Debugging Guidance: the
three post-load reconcile log lines and what each means, how to round-trip a
stored body offline to find the cause, and the list of known-lossy constructs
that are by design and must not be 'fixed' with more markers.
Correct the blank-line marker entry, which still described the pre-fix
behaviour of rewriting markers 'empty or not' — that is exactly what silently
destroyed text typed into a marker. Also document the two Turndown traps that
caused real corruption: flanking-whitespace expulsion next to atomic children
(NBSP-encoded sentinel, do not simplify) and blockquote soft breaks needing the
'>' prefix carried across the restore.
Rendered mode (TinyMCE) briefly showed "Edited" and fired a useless autosave
every time certain notes were opened, and text typed between two blocks could
be lost entirely.
Data loss: <p class="md-blank-line"> is a real, focusable paragraph in the
iframe, so clicking the gap between blocks puts the caret inside it. The
blank-line normalisation in tinymceToMarkdown() rewrote such paragraphs
unconditionally ("empty or not"), replacing whatever the user had just typed
with the <br> sentinel before Turndown ever saw it. Now only markers that are
still genuinely blank collapse to the sentinel; a marker holding real content
is left verbatim and converts as an ordinary paragraph.
Unbounded whitespace growth: Turndown derives an element's flanking whitespace
from node.textContent, which skips atomic children. For <a><img/> Label</a>
it reported leading whitespace that is actually interior to the output, so
replacementForNode()'s content.trim() could not remove it yet it was prepended
anyway. The run grew by one character on every open (12 -> 13 -> 14 ...),
slowly corrupting the stored body. Interior whitespace next to <img>/<br> is
now hidden behind a sentinel that encodes the exact character (these runs are
frequently NBSP, so restoring a plain space would itself dirty the note).
Blockquotes: a soft-wrapped quote line lost its "> " prefix because the <br>
sentinel is restored after Turndown has prefixed its own lines, so "> b\n> c"
came back as "> b\nc" and the second line escaped the quote. The prefix is now
carried across the break. Bare "> " lines also no longer keep a trailing space.
Phantom edit: _savedHash is snapshotted before the editor loads, so the 820ms
post-load reconcile compared normalised content against the raw server body and
treated any cosmetic rewrite as a user edit. Some normalisations cannot be
represented in HTML at all (an indented ``` fence loses its indent, a blank
line after an ATX heading is collapsed by design). The reconcile now
distinguishes the two cases via a _tinymceUserTypedSinceLoad flag, wired to
keydown/paste/cut/drop -- deliberately not input/SetContent, which setContent()
itself fires. A real edit during the quiet window still saves; pure load
normalisation just re-baselines the hash.
Bump ASSET_VERSION and the service-worker cache name so browsers refetch app.js.
Tests: 790 pass (was 776).
A bare <pre> (no <code> child, no language-* class) from pasted external
HTML fell through to Turndown's default fenced-code rule, which does not
widen the fence when the content contains a backtick run and instead
inline-escapes the backticks — producing broken markdown. Match every
<pre>; the replacement already reads textContent when there is no <code>
child, so bare <pre> is fenced (and fence-widened) safely.
Add regression tests for the bare-<pre> backtick-run case and the normal
language-tagged <pre><code> path.
- TinyMCE Enter at end of a non-empty checkbox now always inserts a new
checkbox item, regardless of what follows (paragraph, another checkbox,
or nothing). Exiting the list is done via Enter on an empty item. This
fixes being unable to add items to an existing checklist that is
followed by other text; only bottom-of-note appends worked before.
- Restore rendered-mode title auto-fill broken by ffb7bd0: onEdit now
calls a cheap DOM-only autoTitleFromTinyMCE() instead of relying on a
per-edit synthetic textarea 'input' event.
- Tests: rewrite checkbox-enter-behavior.spec.js (end-of-note,
followed-by-text, empty-item exit); remove checkbox-exit.spec.js
(encoded old exit-on-following-paragraph behavior); add
autotitle-rendered.spec.js and tinymceOnEditSync auto-title guards.
Bug: inserting a row on top of a 4-column table in rendered mode, then
switching notes and coming back, corrupted the table (header row
duplicated). Root cause: the turndown table rule always used all
<tbody> <tr> elements as body rows even when the first was already
used as the header row due to the absence of <thead>.
Fix: when <thead> is absent and <tbody> is present, skip the first
<tr> (used as headerRow) from bodyRows. Harden the fallback for
single-row tables.
Tests: 20 new unit tests (html-to-markdown conversion, full round-trip,
edge cases) and 2 new Playwright E2E tests covering the exact bug
scenario.
BeforeExecCommand handler splits <br>-separated <p> blocks before insertUnorderedList/insertOrderedList runs, same pattern used for FormatBlock. Also sets margin:0 on split blocks to preserve linebreak-mode spacing.
- Extend range to end of <li> before extractContents() (collapsed ranges return empty)
- Same fix for both TinyMCE editor and preview contenteditable handlers
- Add <li> Enter handler to preview mode (was missing entirely)
Remove confirmMoveOutOfVault mechanism and vault→plain / vault→vault
move paths. Vault notes now have immutable parentId — the server rejects
any PUT that changes a vault note's folder (400). The folder select
stays disabled for vault notes even after unlock.
Server:
- _helpers.js: remove confirmMoveOutOfVault from assertVaultNoteBodyEncrypted,
add nextFolderId guard to ciphertext-egress check
- fragments.js: add parentId immutability guard before every write path
(autosave PUT + createCopy), enrich folders with isVault in editor GET
- api.js: add parentId immutability guard for web API PUT
Client:
- app.js: remove dead isEnc branches from folder-change handler, keep
only plain→vault encrypt flow; remove confirmMoveOutOfVault hidden
input injection; remove fs.disabled=false from _completeUnlock;
gate _applyFormReadonly on vault state so select stays disabled;
add data-is-vault option-attribute as primary vault detection
Templates:
- fragments.js: add data-is-vault='1' to folder select <option> elements
Tests:
- createServer.test.js: update vault move tests for new rejection behavior
- vaultRichModeSync.test.js: remove vault→plain behavioral test (dead path)
- proxyVaultGuard.test.js: add ciphertext-egress-to-non-vault test
- vault-move.spec.js: replace vault→plain/vault→vault tests with disabled-select
+ server-rejection tests; add plain-note folder-move tests (6, 7)
Versions: 20260717vaultsimplify1 / shell-v74
When clicking a note title, TinyMCE fires async events after
_tinymceSuppressEdits is cleared (setTimeout 0), reaching onEdit()
and triggering markEdited() + scheduleSave(). The markdown round-trip
(original body -> render -> HTML -> tinymceToMarkdown) normalises
whitespace, so the save actually fires with a different body hash.
Add _tinymcePostLoad flag set in _setTinyMCEContent's setTimeout
callback (after suppress clears). On the first onEdit() that fires:
- skip the bubbling input dispatch so the form listener stays quiet
- recapture _savedHash against the round-tripped markdown instead of
calling markEdited/scheduleSave, then clear the flag
Subsequent real edits compare against the normalised baseline and
behave correctly.
- New POST /api/export/html: single self-contained HTML file with the
active theme's CSS inlined, images/attachments as base64 data URIs,
and heading/link colors matching the TinyMCE rendered-mode view
- CSS extraction helper (extractCssBlocks) pulls theme + .editor-preview
rules out of styles.css via a brace-depth scanner
- New inlineResourceLinks helper: attachment links become data: URIs
with a download attribute instead of dead /resources/ hrefs
- PDF table borders bumped to visible 1px/2px lines; fixed pandoc's
default HTML template CSS (50px body padding) leaking into PDF output
via --include-in-header
- Regenerated public/reference.docx with a patched Table style (visible
gridlines) via new scripts/build-reference-docx.sh
- Client: exportNoteAsHtml now posts to the server instead of building
an unstyled client-side doc; sends current theme slug
- Bumped service worker cache to v54-20260715htmlexport
- Added tests: extractCssBlocks, inlineResourceLinks, handleExportHtml
Both themes had --border set to solid accent color instead of
transparent rgba() like every other theme. Changed to low-opacity
rgba() borders (12-14% for --border, 40% for --border-focus).
Export:
- Export button in TinyMCE toolbar (rendered mode) and mobile hamburger menu
- Format dropdown: Markdown, HTML, Word (.docx), PDF (print window)
- html-docx.js vendored UMD bundle for DOCX export
- PDF opens clean print window: white bg, black text, @page margins, no header/footer
- Esc closes export menu; Cancel button in dropdown
- Export menu hidden in markdown mode; positioned via fixed+JS to avoid overflow clipping
FormatBlock fix:
- blocks dropdown was converting the whole note when content used linebreak mode
(single <p> with <br> separators). NodeChange handler now splits <br>-joined
blocks into individual <p> elements on caret entry so FormatBlock targets only
the selected line.
- 6 Playwright tests in playwright-tests/format-block.spec.js covering caret,
triple-click, keyboard selection, existing-heading, revert-to-paragraph,
and the linebreak-mode regression case.
Table borders:
- 3px solid borders in markdown preview (styles.css), TinyMCE content_style
(with !important to override skin defaults), and PDF print window.
- Table header bg + bold in all three contexts.
- Headers (h1-h6) in rendered mode now use var(--accent), matching
folder/notebook color in the nav panel (was var(--text-heading)).
- Fix native right-click spelling suggestions: TinyMCE's link plugin
context-menu item resolved to a non-empty value for ANY editable
text, not just real hyperlinks, so it always intercepted right-clicks
and blocked the native browser context menu (and its spellcheck
suggestions). Set contextmenu:'image table' to exclude link; image
and table right-clicks are unaffected since those only match on
actual <img>/table elements. Link insertion/editing remains
available via the toolbar and click-to-copy/Ctrl-click-to-open.
TinyMCE runs in an iframe (separate browsing context), so keydown events
fired inside it never bubble to the parent document's keydown listener.
This made Esc silently do nothing while focus was inside the note body
where search highlights live, even though the same key worked once focus
left the note. Wired the same two-stage Esc logic (dismiss highlight,
then clear nav-search) directly on editor.on('keydown'), following the
existing pattern used for the AI-completion shortcut and resource-viewer
overlay, both of which have the same iframe limitation.
- Add rounded + humanist font families with expanded fallback stacks
- Refactor flushSave() from minified one-liner: watchdog timeout, mutex
for encrypted autosave coordination, proper cleanup on all paths
- Fix openNavFolderAndFirstNote so already-expanded folders can collapse
- Service worker cache v46 -> v47