diff --git a/.changeset/2026-08-14-fix-jsx-child-boundaries.md b/.changeset/2026-08-14-fix-jsx-child-boundaries.md new file mode 100644 index 0000000000..3e3443f28c --- /dev/null +++ b/.changeset/2026-08-14-fix-jsx-child-boundaries.md @@ -0,0 +1,5 @@ +--- +'@tiptap/core': patch +--- + +Keep mixed JSX children as separate siblings in DOM output. diff --git a/AGENTS.md b/AGENTS.md index aa99f710a6..7458fd2f8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,12 @@ Published packages live in `packages/*`. `demos/` is a Vite app used as playgrou - Add a changeset for user-facing changes. Public API breaks need a major bump and migration notes. - Add or update a demo and tests for user-visible behavior. Prefer unit tests over e2e when deterministic. - Fix fallow findings your change introduced. Don't suppress them. +- Comments should be clear, concise and use plain, simple english. + - Rather use a oneliner + - Try to always be intent-focused + - stay local with comments + - scannable, no long or complex wording + - assume the reader doesn't know what you're talking about ## Before opening a PR @@ -54,10 +60,60 @@ decorations.filter(d => d.visible) ### Comments -- Comment only when the reason is not visible in the code. Never restate what the lines below already say. -- Two lines max. Only genuinely complex or hard to follow code earns more. -- Say why, not what: `// We keep the old value because the transaction may be reverted.` -- JSDoc on public APIs with `@param`, `@returns` and a runnable example. Those examples generate our API docs. +Comments are a last resort. Prefer code that explains itself through clear names, structure, and small functions. + +* Default to **no comment**. +* Comment only when the **reason for a decision is not apparent from the code**. +* Keep comments **short, local, and intent-focused**. +* Prefer a short one-line fragment or sentence. +* Never restate what the code does. +* Never narrate control flow. +* Never explain surrounding architecture, history, edge cases, or implementation details unless they are essential to understanding the decision. +* Never use comments as a substitute for clearer code, naming, or structure. +* Do not write prose paragraphs, mini-documentation, or essay-style explanations in implementation code. +* Do not add examples, scenarios, or parenthetical explanations to comments. +* Do not use multi-line comments just because an explanation can be written. If it cannot be expressed concisely, reconsider whether the comment belongs in the code at all. +* Existing verbose comments are not a style precedent. Do not imitate them. +* When modifying code, remove comments that merely describe code made obvious by the change. + +Prefer: + +```ts +// Skip empty text nodes +// Preserve the original selection +// Stop after the first match +// Avoid dispatching during composition +// Keep inactive editors measurable +// Prevent collisions with imported IDs +``` + +Avoid: + +```ts +// While composition is running, the update handler exits early because the +// view is still composing. The final update may also contain no document or +// selection changes, which means the menu would otherwise never update. +``` + +```ts +/** + * Page content width used for the off-screen host. This is necessary because + * inactive editors need to remain measurable for ResizeObserver to detect + * changes while no overlay is currently open. + */ +``` + +```ts +/** + * Generates a unique endnote ID. Imported documents use numeric DOCX IDs, + * while client-created endnotes use this prefix to ensure that IDs cannot + * collide with imported endnotes or footnotes. + */ +``` + +If a short comment loses useful detail, that detail usually belongs in the code structure, a test, commit/PR description, or documentation instead. + +JSDoc is exempt **only when it documents a public API**. Public API JSDoc should still be concise and include `@param`, `@returns`, and a runnable example where appropriate. ### Writing diff --git a/packages/core/__tests__/jsx-runtime.spec.ts b/packages/core/__tests__/jsx-runtime.spec.ts new file mode 100644 index 0000000000..ec9631ef89 --- /dev/null +++ b/packages/core/__tests__/jsx-runtime.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' + +import { jsx, jsxs } from '../src/jsx-runtime.js' + +describe('JSX runtime', () => { + it('keeps text and element siblings separate', () => { + const element = jsx('strong', { children: 'text' }) + + expect(jsxs('p', { children: ['Before ', element] })).toEqual([ + 'p', + {}, + 'Before ', + ['strong', {}, 'text'], + ]) + }) + + it('keeps text and slot siblings separate', () => { + const slot = jsx('slot', {}) + + expect(jsxs('p', { children: ['Before ', slot] })).toEqual(['p', {}, 'Before ', 0]) + }) +}) diff --git a/packages/core/src/jsx-runtime.ts b/packages/core/src/jsx-runtime.ts index f844f18d1e..ba5013b785 100644 --- a/packages/core/src/jsx-runtime.ts +++ b/packages/core/src/jsx-runtime.ts @@ -37,7 +37,11 @@ export function Fragment(props: { children: JSXRenderer[] }) { return props.children } -export const h: JSXRenderer = (tag, attributes) => { +function render( + tag: Parameters[0], + attributes: Attributes | undefined, + hasMultipleChildren: boolean, +) { // Treat the slot tag as the Prosemirror hole to render content into if (tag === 'slot') { return 0 @@ -56,12 +60,28 @@ export const h: JSXRenderer = (tag, attributes) => { ) } - // Otherwise, return the tag, attributes, and children + if (hasMultipleChildren && Array.isArray(children)) { + return [tag, rest, ...children] as DOMOutputSpecArray + } + return [tag, rest, children] } +export const h: JSXRenderer = (tag, attributes) => render(tag, attributes, false) + +export const jsxs: JSXRenderer = (tag, attributes) => render(tag, attributes, true) + +export const jsxDEV = ( + tag: Parameters[0], + attributes?: Attributes, + _key?: unknown, + isStaticChildren?: boolean, +) => { + return render(tag, attributes, Boolean(isStaticChildren)) +} + // See // https://esbuild.github.io/api/#jsx-import-source // https://www.typescriptlang.org/tsconfig/#jsxImportSource -export { h as createElement, h as jsx, h as jsxDEV, h as jsxs } +export { h as createElement, h as jsx }