Skip to content

Upgrade ckeditor5-math to CKEditor v47#10

Open
tony wants to merge 11 commits intomainfrom
ckeditor5-spring-2026
Open

Upgrade ckeditor5-math to CKEditor v47#10
tony wants to merge 11 commits intomainfrom
ckeditor5-spring-2026

Conversation

@tony
Copy link
Copy Markdown
Collaborator

@tony tony commented Mar 25, 2026

Resolves LOB-1950

Summary

Upgrades @multiverse-io/ckeditor5-math from CKEditor 5 v43.2.0 → v47.6.1, fixes three equation rendering bugs (two pre-existing, one introduced by the upgrade), adds an outputType toggle to the dev sample, and adds comprehensive drag-drop and output-type test coverage.

What changed

1. CKEditor 5 v43 → v47 core upgrade

Breaking API renames — CKEditor 5 v46 introduced a unified exports system that renamed engine types to carry explicit Model/View prefixes:

Old name (v43) New name (v47) File
DowncastWriter ViewDowncastWriter mathediting.ts
LivePosition ModelLivePosition automath.ts
LiveRange ModelLiveRange automath.ts
Element ModelElement mathediting.ts, utils.ts
DocumentSelection ModelDocumentSelection utils.ts
icons.check / icons.cancel IconCheck / IconCancel mainformview.ts

See: Update to v46.x · Update to v47.x · Migrating imports (v46+)

Dev tooling bumps:

Package From To
@ckeditor/ckeditor5-dev-build-tools 43.0.0 55.2.0
@ckeditor/ckeditor5-package-tools ^2.1.0 ^5.1.0
@ckeditor/ckeditor5-inspector >=4.1.0 >=5.0.0
eslint-config-ckeditor5 >=7.1.0 9.1.0
stylelint-config-ckeditor5 >=7.1.0 10.0.0
typescript 5.0.4 ~5.5
ckeditor5 latest ^47.6.1

TypeScript / build config: tsconfig.dist.json updated with outDir, include, and corrected types path for dev-build-tools v55 + TypeScript 5.5. Added typings/types/index.d.ts SVG module shim.

CI: Node.js 20.x24.x (@ckeditor/ckeditor5-dev-build-tools v55 requires Node ≥24).


2. KaTeX rendering fixes (src/utils.ts, sample/index.html)

Null-prototype macros — CKEditor v47's config.get() returns objects created via Object.create(null). KaTeX's internal Namespace class calls this.current.hasOwnProperty() directly on the user macros object, which throws TypeError: this.current.hasOwnProperty is not a function on null-prototype objects. Fix: shallow-copy katexRenderOptions.macros before passing to katex.render() to restore the Object.prototype chain.

See: CKEditor Config API

Auto-render DOM timing — KaTeX 0.16.x added an explicit null guard in renderMathInElement that throws "No element provided to render" when called with a null element. The <script onload> in <head> fires before document.body exists. Fix: keep katex.min.js in <head> (must be global before CKEditor init), move only auto-render.min.js to end of <body>.

KaTeX CDN bump0.13.50.16.43. SRI hashes dropped (unnecessary for dev sample). See: KaTeX releases · KaTeX CHANGELOG


3. Balloon form CSS padding fix (theme/mathform.css)

CKEditor v47's .ck-reset_all :not(.ck-reset_all-excluded *) CSS reset (specificity 0,2,0) zeroes padding on all descendants inside the balloon panel. Our .ck.ck-math-form selector has the same specificity and loses by source order.

Fix:

  • Prefix selector with form element type → form.ck.ck-math-form (specificity 0,2,1, beats the reset)
  • Add display: flex; flex-direction: column; gap to .ck-math-view for vertical spacing between stacked children
  • Add explicit padding to .ck-math-preview for the equation preview area

4. Drag-and-drop equation fix (src/mathediting.ts)

Two independent bugs cause equations to disappear after drag-and-drop:

Bug A — UIElement render callback not re-invoked: CKEditor's Differ optimizes model moves by repositioning existing view elements. The UIElement's render callback (which calls katex.render()) only fires once. After move, the KaTeX HTML is lost.

Fix: Add a change:data listener that calls editor.editing.reconvertItem() on inserted mathtex-inline/mathtex-display elements. This follows CKEditor's own pattern used in ckeditor5-image, ckeditor5-table, and ckeditor5-list.

Bug B — Browser sanitizes <script> from DataTransfer: When outputType is 'script' (the default), equations serialize as <script type="math/tex">. Browsers strip <script> tags from DataTransfer HTML for security. On drop, the upcast finds no math element and the equation data is entirely lost. This is version-independent — it affects v43 too.

This is why ariel's production integration (which uses outputType: 'span' + forceOutputType: true) never exhibited this bug — <span class="math-tex"> survives DataTransfer sanitization.

See: HTML Sanitizer API (MDN) · DataTransfer API (web.dev)


5. Dev sample outputType toggle (sample/index.html, sample/ckeditor.ts)

Added a radio toggle above the editor to switch between:

  • span (default) — outputType: 'span', forceOutputType: true, HTML templates use <span class="math-tex">
  • scriptoutputType: 'script', forceOutputType: false, HTML templates use <script type="math/tex">

Toggling destroys and recreates the editor with the matching config and HTML template. Default is span to match ariel's production config and avoid the DataTransfer drag-drop bug.


6. Dev sample license key (sample/ckeditor.ts)

CKEditor v47 requires a licenseKey — without one, ClassicEditor.create() rejects with license-key-missing. The key in this PR is a CKEditor development license (non-secret, non-billable):

licenseType: "development"
exp: 2026-03-11T23:59:59Z
distributionChannel: ["sh", "drupal"]
whiteLabel: true

Development keys are free to obtain, restricted to localhost / *.test / *.local / private IPs, and do not consume editor loads. They are explicitly designed for local development, CI, and E2E tests. The key carries no commercial entitlements and cannot be used in production.

See: License key and activation · Obtaining the license key


7. Tests (tests/mathdragdrop.ts)

Added 460 lines of test coverage:

  • Drag-drop DOM rendering — Asserts that the UIElement's rendered HTML (not just model data) survives a simulated drag-drop for both inline and display math
  • outputType: 'span' + forceOutputType: true — Downcast format, forced type on upcast from <script> tags, clipboard roundtrip, drag-drop simulation
  • outputType: 'script' + forceOutputType: false — Downcast format for inline/display, type preservation from both <span> and <script> source HTML

Note: ckeditor5-package-tools v5 dropped the test task. Tests are written against mocha/chai but need a runner configuration to execute (tracked separately).

Screenshots

image

Files changed

File Lines Summary
src/mathediting.ts +33 v47 type renames + reconvertItem() listener for drag-drop
src/automath.ts +30 LivePosition/LiveRangeModelLivePosition/ModelLiveRange
src/utils.ts +15 Shallow-copy macros to restore Object.prototype
src/ui/mainformview.ts +6 icons.check/cancelIconCheck/IconCancel
theme/mathform.css +12 Specificity fix + .ck-math-view flex column + preview padding
sample/ckeditor.ts +163 outputType toggle, destroy/recreate, license key
sample/index.html +107 Radio controls, <template> blocks for span/script
tests/mathdragdrop.ts +460 Drag-drop DOM, outputType/forceOutputType test suites
package.json +20 Version bump, dep updates, peer range
.github/workflows/*.yml +4 Node 24.x
tsconfig.dist.json +8 outDir, include, types path
typings/types/index.d.ts +4 SVG module shim
yarn.lock ±7525 Lockfile regeneration

Test plan

  • yarn lint — 0 errors
  • yarn stylelint — 0 errors
  • yarn build:dist — NPM + browser builds pass
  • tsc -p tsconfig.release.json — no type errors
  • CI build (24.x) — green
  • Playwright: localhost:8084 loads with 0 console errors, all 4 equations render
  • Playwright: balloon popup opens on widget click, form has correct padding
  • Playwright: simulated drag-drop preserves equation rendering (hasKatex: true, 1117 chars)
  • Playwright: outputType radio toggle destroys/recreates editor with correct config
  • Manual: drag-drop equation in span mode — equation survives
  • Manual: drag-drop equation in script mode — equation reconverts (model survives via reconvertItem, but DataTransfer may strip <script>)

Note

High Risk
High due to a major CKEditor dependency/toolchain upgrade (v43→v47) plus changes in editor conversion/rendering paths (reconvertItem on model changes) that can affect data serialization and widget behavior across paste/undo/drag-drop.

Overview
Upgrades the package to CKEditor 47.6.1 (and related tooling like ckeditor5-dev-build-tools, ckeditor5-package-tools, lint configs, and TypeScript), bumps the package version/peer range, and raises the minimum/CI Node.js version to 24.11+.

Fixes several rendering/regression issues: forces math widgets to reconvert after model inserts to restore equation rendering after drag-drop/paste/undo, prevents native dragging by setting draggable="false" on the UI element, and hardens KaTeX rendering by cloning katexRenderOptions (notably macros) to avoid null-prototype pitfalls.

Refreshes the dev sample to CKEditor 47 requirements (adds a dev licenseKey, updates KaTeX CDN and script placement) and adds an outputType (span vs script) toggle; adds a comprehensive new tests/mathdragdrop.ts suite covering clipboard roundtrips, drag-drop simulations, DOM re-rendering, and outputType/forceOutputType behavior. Also updates build TS config (tsconfig.dist.json) and adds an SVG module typing shim.

Written by Cursor Bugbot for commit 6aaf111. This will update automatically on new commits. Configure here.

@tony tony requested a review from a team as a code owner March 25, 2026 11:01
@linear
Copy link
Copy Markdown

linear bot commented Mar 25, 2026

why: CKEditor 5 v47 renamed several engine types to carry explicit
Model/View prefixes. The old short names are no longer exported.

what:
- DowncastWriter -> ViewDowncastWriter (mathediting.ts)
- LivePosition -> ModelLivePosition (automath.ts)
- LiveRange -> ModelLiveRange (automath.ts)
- Element -> ModelElement (mathediting.ts, utils.ts)
- DocumentSelection -> ModelDocumentSelection (utils.ts)
- icons.check / icons.cancel -> IconCheck / IconCancel
  (mainformview.ts; icons object removed in v47, individual
  named exports from @ckeditor/ckeditor5-icons used instead)
- tsconfig.dist.json: add outDir and fix types path for
  bundler moduleResolution required by dev-build-tools v55
- typings/types/index.d.ts: add directory to satisfy
  typeRoots resolution under TypeScript 5.5
@tony tony force-pushed the ckeditor5-spring-2026 branch 3 times, most recently from e9123af to d1ee5cd Compare March 26, 2026 15:04
Copy link
Copy Markdown

@kenny-siu kenny-siu left a comment

Choose a reason for hiding this comment

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

There's the one quibble, but I don't imagine it would happen all that much.

Comment on lines +58 to +70
editor.model.document.on( 'change:data', () => {
for ( const change of editor.model.document.differ.getChanges() ) {
if ( change.type === 'insert' ) {
const item = change.position.nodeAfter;
if ( item && (
item.is( 'element', 'mathtex-inline' ) ||
item.is( 'element', 'mathtex-display' )
) ) {
editor.editing.reconvertItem( item );
}
}
}
} );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

According to claude there's some sort of double-processing that happens here when you past something.

I am really tired right now and can't figure it out, but maybe you can 😅

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

}
}
}
} );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reconversion on every math insert causes double-processing

Medium Severity

The change:data listener calls editor.editing.reconvertItem() for every math element insertion — not just drag-drop. This means paste, undo, and MathCommand.execute() all trigger a second full conversion cycle. reconvertItem() internally calls model.change(), which after the original cycle completes opens a new change block → destroys the just-rendered view element → re-downcasts and re-renders it. With async engines like MathJax this can cause visible flickering. CKEditor's recommended pattern is registerPostFixer with differ.refreshItem(), which processes reconversion within the same cycle. Kenny flagged this exact concern in the PR discussion.

Fix in Cursor Fix in Web

@tony tony force-pushed the ckeditor5-spring-2026 branch 2 times, most recently from 3dff340 to 01baaf3 Compare March 27, 2026 15:44
tony added 10 commits March 27, 2026 10:45
why: CKEditor 47 development tooling requires the newer package-tools stack
and a newer TypeScript baseline than this repo was still pinned to.

what:
- bump @ckeditor/ckeditor5-dev-build-tools 43.0.0 -> 55.2.0
- bump @ckeditor/ckeditor5-package-tools ^2.1.0 -> ^5.1.0
- bump @ckeditor/ckeditor5-inspector >=4.1.0 -> >=5.0.0
- bump eslint-config-ckeditor5 >=7.1.0 -> 9.1.0
- bump stylelint-config-ckeditor5 >=7.1.0 -> 10.0.0
- bump typescript 5.0.4 -> ~5.5 to match CKEditor 47's supported range
- keep mocha-scoped resolutions for debug/diff/minimatch to avoid ESLint 7
  CJS loading breakage from ESM-only brace-expansion hoisting

See also:
- https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools
- https://www.npmjs.com/package/@ckeditor/ckeditor5-package-tools
- https://www.npmjs.com/package/typescript
why: Move the package onto the CKEditor 47 line so the rest of the branch can adapt the plugin to the newer runtime and APIs.

what:
- update devDependencies.ckeditor5 from "latest" to "^47.6.1"
- raise peerDependencies.ckeditor5 from ">=43.2.0 || ^0.0.0-nightly" to ">=47.0.0"
- refresh yarn.lock to resolve ckeditor5 via the pinned 47.6.1 range

See also:
- https://www.npmjs.com/package/ckeditor5
- https://github.com/ckeditor/ckeditor5
- https://ckeditor.com/docs/ckeditor5/latest/updating/guides/update-to-47.html

Release History (newest -> oldest):
- 47.6.1 (March 11, 2026):
  - https://github.com/ckeditor/ckeditor5/releases/tag/v47.6.1
- 47.0.0 (October 2025):
  - https://ckeditor.com/docs/ckeditor5/latest/updating/guides/update-to-47.html
- 46.0.0 (July 2025):
  - https://ckeditor.com/docs/ckeditor5/latest/updating/guides/update-to-46.html
- 45.0.0 (April 2025):
  - https://ckeditor.com/docs/ckeditor5/latest/updating/guides/update-to-45.html
- 44.0.0 (January 2025):
  - https://ckeditor.com/docs/ckeditor5/latest/updating/guides/update-to-44.html
- 43.2.0 (November 2024):
  - https://github.com/ckeditor/ckeditor5/releases/tag/v43.2.0
why: CKEditor 47 tooling requires Node >=24.11.0. The branch had already
moved CI to Node 24.x, but local version files and package engines still
lagged behind and left development setup inconsistent with the tested runtime.

what:
- bump GitHub Actions workflows from Node 20.x -> 24.x
- bump .nvmrc to 24.14.1
- bump .tool-versions to nodejs 24.14.1 and keep Yarn 1.22.22
- raise package.json engines.node to >=24.11.0

See also:
- https://ckeditor.com/docs/ckeditor5/latest/framework/contributing/development-environment.html
- https://nodejs.org/en/blog/release/v24.14.1
- https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools
why: CKEditor v47's @ckeditor/ckeditor5-core throws license-key-missing
(a hard error, not a warning) when no licenseKey is set. Without it,
ClassicEditor.create() rejects and the editor never renders — the page
shows plain browser-rendered static HTML. Key sourced from ariel's
webpack.common.ts DefinePlugin (dev key, same commercial license).

what:
- sample/ckeditor.ts: add licenseKey to ClassicEditor.create() options
why: CKEditor 47 changed config.get() to return null-prototype objects, which
breaks KaTeX macro handling, and the newer auto-render script now throws if it
runs before document.body exists. Together they left the dev sample rendering
broken.

what:
- shallow-copy katexRenderOptions.macros before calling katex.render so KaTeX
  receives an object with Object.prototype
- bump the sample KaTeX CDN from 0.13.5 -> 0.16.43 and drop local-only SRI
  hashes
- move auto-render.min.js to the end of body so it runs after the DOM is ready
- keep katex.min.js in head so the plugin still sees the katex global early

See also:
- https://www.npmjs.com/package/katex
- https://github.com/KaTeX/KaTeX
why: CKEditor v47's `.ck-reset_all :not(.ck-reset_all-excluded *)` reset
rule (specificity 0,2,0) zeroes out padding on all descendants inside the
balloon panel. Our `.ck.ck-math-form` selector has the same specificity
(0,2,0), so the reset wins by source order in the v47 CSS bundle. This
was not an issue in v43 where the reset rule had lower specificity.
what:
- Prefix form selector with `form` element type (`.ck.ck-math-form` →
  `form.ck.ck-math-form`) to boost specificity to 0,2,1
- Add flex column layout with gap to `.ck-math-view` so stacked children
  (input, display toggle, preview label, equation preview) have uniform
  vertical spacing via `gap: var(--ck-spacing-standard)`
- Add explicit padding to `.ck-math-preview` for the equation preview
  area inside the balloon form
why: CKEditor 47 drag-move behavior exposed two issues in the math widget: the
inner UIElement could start a native browser drag that bypassed CKEditor's DnD
pipeline, and Differ move optimization reused the widget view without rerunning
its KaTeX render callback. The result was either deleted or visually empty
widgets after a move.

what:
- mark the inner KaTeX UIElement as draggable="false" so only CKEditor's outer
  widget container participates in drag-and-drop
- reconvert inserted math elements on change:data after a move so the UIElement
  is recreated and katex.render() runs again at the new location
- extend mathdragdrop.ts with clipboard roundtrip and DOM rendering regression
  tests for both inline and display math widgets
why: Browsers sanitize <script> tags from DataTransfer HTML during
drag-and-drop. The default outputType ('script') serializes equations as
<script type="math/tex">, which is stripped by the browser on drop,
causing equations to disappear. Ariel's production integration avoids
this by using outputType: 'span' with forceOutputType: true, which
serializes as <span class="math-tex"> — preserved by DataTransfer.
what:
- Add radio toggle to sample/index.html to switch between span and
  script output types with corresponding HTML templates
- Default sample to span mode (matching ariel's working config)
- Refactor sample/ckeditor.ts to support destroy/recreate on toggle,
  extract shared config into constants
- Add outputType/forceOutputType test suites to mathdragdrop.ts:
  - span mode: downcast format, forced type on upcast from script tags,
    clipboard roundtrip, drag-drop simulation
  - script mode: downcast format for inline/display, type preservation
    for both span and script source HTML
why: Record the CKEditor 47.6.1 prerelease in the changelog using the repo's usual version-section format.

what:
- add a v47.6.1-next.0 section directly under Current
- summarize the CKEditor 47.6.1 compatibility fixes, sample updates, and Node 24 minimum
- leave the Current placeholder intact for future unreleased changes
why: Keep the release metadata split consistent with recent repo history by bumping the prerelease version in its own commit after the changelog update.

what:
- change package.json version from 43.2.0-next.2 to 47.6.1-next.0
- leave dependency, Node minimum, and changelog content unchanged
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.

2 participants