Skip to content

fix(core): stop the browser spellchecking code blocks and inline code - #253

Merged
PeytonNowlin merged 2 commits into
mainfrom
fix/code-spellcheck
Aug 30, 2026
Merged

fix(core): stop the browser spellchecking code blocks and inline code#253
PeytonNowlin merged 2 commits into
mainfrom
fix/code-spellcheck

Conversation

@PeytonNowlin

@PeytonNowlin PeytonNowlin commented Aug 30, 2026

Copy link
Copy Markdown
Owner

The bug

The canvas offered exactly one answer for the whole document — spellcheck on
<openleaf-editor>, copied onto the editable region. So an author who wanted
their prose checked also got a red squiggle under every getElementById.
Source view's textarea (packages/element/src/index.ts, area.spellcheck = false) was the only place in the editor where code was left alone.

The fix

codeSpellcheckPlugin() decorates code blocks and inline <code> with
spellcheck="false", and does it unconditionally. The host attribute is
the author's answer about their prose; code is not prose whichever way they
answered, so this is not tied to it.

Why a decoration and not toDOM

The obvious fix is ['pre', { spellcheck: 'false' }, …] in the schema. That is
wrong: serializeHtml serializes with DOMSerializer.fromSchema, so the same
toDOM that renders the canvas writes the stored HTML. A schema fix would
put editor chrome into every saved document and into every fidelity comparison.
Two tests pin this — one on serializeHtml, one on the element's value.

Two decoration-bookkeeping bugs fixed along the way

  • DecorationSet.find reports decorations that merely touch the queried
    range.
    The neighbouring block is the common case: a code block ending
    exactly where the rebuilt range starts, or an empty paragraph starting
    exactly where it ends. Either was removed as stale and never rebuilt, because
    nodesBetween does not revisit a block the range only abuts. The attribute —
    or, in visual aids, the empty-block guide — fell off the block next to the
    one being edited. This half was already live on main for visual aids:
    <p></p><p>hi</p>, type in the second paragraph, and the ol-empty-block
    decoration disappears.
  • changedRange could not see a mark step. Adding or removing a mark moves
    no position, so its StepMap is empty and reading the maps alone made
    toggling <code> on a word look like a transaction that had changed nothing
    — the decoration did not appear until the author typed in the same block. It
    now folds in the range of a step whose map is the identity.

Those two interact, and the interaction was a regression in the first commit of
this PR, caught by Codex:
once mark steps reach the rebuild, toggling a mark in the paragraph before an
empty one made that paragraph's aid vanish. Fixed in 5a8cc1c.

Refactor included

The whole map → widen → remove → rebuild dance now lives in rebuildChanged
(packages/core/src/decoration-range.ts), called by both decoration plugins in
core, rather than hand-written once per plugin. changedRange and the
widen-to-top-level helper moved out of visual-aids.ts into the same module.
Hoisting rather than copying the wholly-inside filter is deliberate: a copy
leaves the same hazard waiting for the third plugin.

What this does not fix

WebKit applies spellcheck from the editing host rather than per element, so
Safari and every iOS browser still underline code. The only workaround is a
node view that takes the text out of the editable tree, which costs far more
than it buys. This is stated in the source, in the changelog, and in all four
user-facing docs.

Documentation

Updated in the same branch, per AGENTS.md:

  • README.md — the Translations bullet, which is where the spellcheck
    attribute is described.
  • docs/api-reference.md — the spellcheck attribute row.
  • docs/integrating-openleaf.md — the attributes paragraph.
  • packages/element/README.md — the same paragraph in the package copy.
  • packages/core/README.md — a new section next to the other "install this
    plugin if you build the view yourself" sections.
  • CHANGELOG.md — two entries under ## Unreleased### Fixed, one per bug.
  • demo/index.html — the core bundle badge, 124 KB → 125 KB. The new plugin
    put the measured size far enough past the claim that the docs gate's 1%
    survival band no longer covered it (CI measured 125.3 KB against a 124 KB
    claim, and failed). Budget headroom is unaffected: 125.2/127 KB locally.

node scripts/check-docs.mjs passes.

Verification

  • pnpm verify:quick — everything passed, including the docs gate and the
    bundle budgets.
  • pnpm test — 1762 passed, 92 files.
  • pnpm test:e2e — 1047 passed across all three engines. Two failures,
    both pre-existing on main and unrelated (verified by stashing this
    branch and re-running them against a clean tree; the firefox one is also why
    main's own CI is currently red):
    • [firefox] placeholder.spec.tsgetComputedStyle(el, '::before').content
      returns the unresolved attr(data-placeholder) in Firefox rather than the
      substituted string, so the assertion never matches. Test-side.
    • [webkit] demo.spec.ts › serves a promo video the browser can actually decode — the promo asset is H.264-only and Playwright's WebKit build has
      no proprietary codecs, so loadedmetadata never fires.

Each of the three new regression tests fails if the fix it covers is reverted;
I checked by removing the filter and re-running.

No new dependencies.

🤖 Generated with Claude Code

The canvas offered exactly one answer for the whole document -- `spellcheck`
on `<openleaf-editor>` -- so an author who wanted their prose checked also got
a red squiggle under every identifier. Source view's textarea was the only
place in the editor where code was left alone.

`codeSpellcheckPlugin()` decorates code blocks and inline `<code>` with
`spellcheck="false"`, unconditionally: the host attribute is an answer about
prose, and code is not prose whichever way it was set.

It is a decoration rather than an attribute in the schema's `toDOM`, because
`serializeHtml` serializes with `DOMSerializer.fromSchema` -- the same `toDOM`
that renders the canvas writes the stored HTML, so the obvious one-line fix
would have put editor chrome into every saved document and every fidelity
comparison. A test asserts the attribute never reaches `value`.

`changedRange` and the top-level widening are now shared with the visual-aids
plugin rather than copied, and `changedRange` learned to read a step whose
`StepMap` is empty. Adding or removing a mark moves no position, so toggling
`<code>` on a word looked like a transaction that had changed nothing and the
decoration did not appear until the author typed in the same block.

WebKit reads `spellcheck` from the editing host rather than per element, so
Safari and iOS are unchanged. The docs say so.

Signed-off-by: Peyton Nowlin <peytonn98@googlemail.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T03:26:17.233881Z 070b02b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 070b02b793

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CHANGELOG.md
Comment thread packages/core/src/visual-aids.ts Outdated
Codex caught a regression the first commit introduced and, underneath it, a
defect that was already there.

`changedRange` learning to read mark steps meant visual aids now rebuild on a
transaction that previously returned early. That exposed the same
`DecorationSet.find` hazard the spellcheck plugin already guarded against:
`find` reports a decoration that merely *touches* the queried range, so the
empty-paragraph aid on the block after the widened range was removed as stale
and the rebuild -- which scans only that range -- never put it back. Toggling
a mark in the paragraph before an empty one made its aid disappear.

The pre-existing half is the same shape without the mark step: typing in the
paragraph after an empty one already dropped that aid on `main`.

Rather than copy the wholly-inside filter into the second plugin, the whole
map-widen-remove-rebuild dance moves into `rebuildChanged`, which both
decoration plugins in core now call. Both failure modes have a test; each fails
without the filter.

Also bumps the demo's core bundle claim from 124 KB to 125 KB. The new plugin
put the measured size far enough past the claim that the docs gate's 1%
survival band no longer covered it -- CI measured 125.3 KB against a 124 KB
claim.

Signed-off-by: Peyton Nowlin <peytonn98@googlemail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant