Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ meowdown's CSS is wrapped in a cascade layer, `@layer meowdown` (with sub-layers
@import '@meowdown/core/style.css';
```

Two things the variable list cannot show: `--meowdown-gutter` is the horizontal editor padding, applied to the editable root's `.meowdown-content` class (set by `@meowdown/react`), not `.ProseMirror`, so the block handle's drag preview stays unpadded; floating UI such as the block handle lives inside it, so keep it at least `3.5rem`. A headless mount (like the quick start above) has no `.meowdown-content`, so add that class to the mount element (or your own padding) yourself. The selection variables (`--meowdown-selection`, `--meowdown-node-outline`, `--meowdown-node-selection`) are standalone, not derived from `--meowdown-accent`, so selection can be restyled independently.
Two things the variable list cannot show: `--meowdown-gutter` is the horizontal editor padding, applied to the editable root's `.meowdown-content` class, not `.ProseMirror`, so the block handle's drag preview stays unpadded; floating UI such as the block handle lives inside it, so keep it at least `3.5rem`. `defineEditorExtension` puts `.meowdown-content` on the editable root itself, so every mount (the headless quick start above included) is padded from the first paint. The selection variables (`--meowdown-selection`, `--meowdown-node-outline`, `--meowdown-node-selection`) are standalone, not derived from `--meowdown-accent`, so selection can be restyled independently.

Tags (`#tag`) render as pills via the `.md-tag` class, tinted from `--meowdown-accent`. Wire click handling with `defineTagClickHandler(({ tag, event }) => ...)` (or `@meowdown/react`'s `onTagClick` prop); `tag` is read from the rendered text without the leading `#`.

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/extensions/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { definePendingReplacement } from './pending-replacement.ts'
import { defineScrollToSelection } from './scroll-to-selection.ts'
import { defineSelectDocBoundary } from './select-doc-boundary.ts'
import { defineTable } from './table.ts'
import { defineViewAttributes } from './view-attributes.ts'
import { defineVirtualCaret } from './virtual-caret.ts'
import { defineWikilink } from './wikilink.ts'

Expand All @@ -59,6 +60,7 @@ function defineEditorExtensionImpl(options: EditorExtensionOptions) {
defineInlineMarks(),

// plugins
defineViewAttributes({ class: 'meowdown-content' }),
defineCodeBlockSyntaxHighlight(),
defineEscapeCollapse(),
defineMoveBlock(),
Expand Down
41 changes: 41 additions & 0 deletions packages/core/src/extensions/view-attributes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest'
import { page } from 'vitest/browser'

import { setupFixture } from '../testing/index.ts'

import { defineViewAttributes } from './view-attributes.ts'

const pmRoot = page.locate('.ProseMirror')

describe('defineViewAttributes', () => {
it('gives the editable root the content class from the first paint', async () => {
using fixture = setupFixture()
void fixture
await expect.element(pmRoot).toHaveClass('meowdown-content')
})

it('applies the gutter padding the content class carries', () => {
using fixture = setupFixture()
expect(getComputedStyle(fixture.dom).paddingLeft).not.toBe('0px')
})

it('adds a host class alongside the built-in ones', async () => {
using fixture = setupFixture()
fixture.editor.use(defineViewAttributes({ class: 'host-class' }))

await expect.element(pmRoot).toHaveClass('ProseMirror')
await expect.element(pmRoot).toHaveClass('meowdown-content')
await expect.element(pmRoot).toHaveClass('host-class')
})

it('drops the host class when the extension is removed', async () => {
using fixture = setupFixture()
const dispose = fixture.editor.use(defineViewAttributes({ class: 'host-class' }))
await expect.element(pmRoot).toHaveClass('host-class')

dispose()

await expect.element(pmRoot).not.toHaveClass('host-class')
await expect.element(pmRoot).toHaveClass('meowdown-content')
})
})
13 changes: 13 additions & 0 deletions packages/core/src/extensions/view-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { definePlugin, type PlainExtension } from '@prosekit/core'
import { type EditorState, Plugin } from '@prosekit/pm/state'

/**
* Add DOM attributes to the editable root. `class` and `style` values from
* every such extension are combined, so applying this more than once adds
* classes instead of replacing them.
*/
export function defineViewAttributes(
attributes: { [name: string]: string } | ((state: EditorState) => { [name: string]: string }),
): PlainExtension {
return definePlugin(new Plugin({ props: { attributes } }))
}
49 changes: 25 additions & 24 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ export {
type EditorExtensionOptions,
type TypedEditor,
} from './extensions/extension.ts'
export {
defineFileClickHandler,
type FileClickHandler,
type FileClickPayload,
} from './extensions/file-click.ts'
export {
buildFileMarkdown,
defineFilePaste,
type FilePasteHandler,
type FilePasteOptions,
type FileSaveErrorHandler,
} from './extensions/file-paste.ts'
export {
defineFileClickHandler,
type FileClickHandler,
type FileClickPayload,
} from './extensions/file-click.ts'
export {
defineFileView,
getFileKind,
Expand All @@ -62,8 +62,6 @@ export {
type ImageClickPayload,
} from './extensions/image-click.ts'
export { defaultResolveImageUrl, defineImage, type ImageOptions } from './extensions/image.ts'
export { defineMath } from './extensions/math.ts'
export { loadKaTeX, renderMathInto, type KaTeXRender } from './utils/katex.ts'
export type {
MdFileAttrs,
MdImageAttrs,
Expand All @@ -72,8 +70,8 @@ export type {
MdWikilinkAttrs,
} from './extensions/inline-marks.ts'
export {
inlineTextToMarkChunksWithContext,
inlineTextToMarkChunks,
inlineTextToMarkChunksWithContext,
type FileLinkOptions,
type FileLinkPayload,
type FileLinkResolver,
Expand All @@ -98,18 +96,13 @@ export {
} from './extensions/link-commands.ts'
export type { LinkEditHandler, LinkEditOptions } from './extensions/link-commands.ts'
export { defineLinkHoverHandler, type LinkHoverHandler } from './extensions/link-hover.ts'
export type { ListMarker, MeowdownListAttrs } from './extensions/list.ts'
export { defineLinkPaste } from './extensions/link-paste.ts'
export type { ListMarker, MeowdownListAttrs } from './extensions/list.ts'
export type { MarkChunk } from './extensions/mark-chunk.ts'
export type { MarkMode } from './extensions/mark-mode.ts'
export { isMarkOfType, type MarkName } from './extensions/mark-names.ts'
export { defineMath } from './extensions/math.ts'
export { isNodeOfType, type NodeName } from './extensions/node-names.ts'
export {
collectReferenceDefinitions,
type ReferenceDefinition,
type ReferenceDefinitionIndex,
type ReferenceDefinitions,
} from './extensions/reference-links.ts'
export {
definePendingReplacementHandler,
getPendingReplacement,
Expand All @@ -121,24 +114,27 @@ export {
type PendingReplacementOutcome,
type StartPendingReplacementOptions,
} from './extensions/pending-replacement.ts'
export {
collectReferenceDefinitions,
type ReferenceDefinition,
type ReferenceDefinitionIndex,
type ReferenceDefinitions,
} from './extensions/reference-links.ts'
export { getMarkBuilders, type TypedMarkBuilders } from './extensions/schema.ts'
export { defineSpellCheckPlugin } from './extensions/spell-check.ts'
export { defineSubstitution } from './extensions/substitution.ts'
export { isSelectionInTableCell } from './extensions/table.ts'
export {
getTableColumnAlign,
type MeowdownTableCellAttrs,
type TableColumnAlign,
} from './extensions/table-column-align.ts'
export { isSelectionInTableCell } from './extensions/table.ts'
export {
defineTagClickHandler,
type TagClickHandler,
type TagClickPayload,
} from './extensions/tag-click.ts'
export {
defineWikilinkClickHandler,
type WikilinkClickHandler,
type WikilinkClickPayload,
} from './extensions/wikilink-click.ts'
export { defineViewAttributes } from './extensions/view-attributes.ts'
export {
formatSizedWikiEmbed,
parseWikiEmbed,
Expand All @@ -148,15 +144,20 @@ export {
type WikiEmbedResolution,
type WikiEmbedResolver,
} from './extensions/wiki-embed.ts'
export {
defineWikilinkClickHandler,
type WikilinkClickHandler,
type WikilinkClickPayload,
} from './extensions/wikilink-click.ts'
export {
defineWikilinkHoverHandler,
type WikilinkHoverHandler,
type WikilinkHoverHit,
} from './extensions/wikilink-hover.ts'
export { defineWikilinkTrigger } from './extensions/wikilink-trigger.ts'
export type { PositionRange } from './utils/range.ts'
export { getTextblockDisplayText } from './utils/display-text.ts'
export { formatFileSize } from './utils/format-file-size.ts'
export { loadKaTeX, renderMathInto, type KaTeXRender } from './utils/katex.ts'
export type { PositionRange } from './utils/range.ts'
export { getSelectedText } from './utils/selected-text.ts'
export { getVirtualElementFromRange, type VirtualElement } from './utils/virtual-element.ts'
export { defineSpellCheckPlugin } from './extensions/spell-check.ts'
export { formatFileSize } from './utils/format-file-size.ts'
9 changes: 9 additions & 0 deletions packages/react/src/components/editor-extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
defineSpellCheckPlugin,
defineSubstitution,
defineTagClickHandler,
defineViewAttributes,
defineWikilinkClickHandler,
defineWikilinkTrigger,
type EditorExtension,
Expand Down Expand Up @@ -55,6 +56,7 @@ export interface EditorExtensionsProps {
readOnly?: boolean
wikilinkEnabled?: boolean
spellCheck?: boolean
editorClassName?: string
}

// A leaf that renders nothing and holds every reactive `useExtension` call (each
Expand All @@ -81,6 +83,7 @@ export function EditorExtensions({
readOnly,
wikilinkEnabled,
spellCheck,
editorClassName,
}: EditorExtensionsProps): null {
// The mark-mode plugin ships in the creation extension so the first paint
// already hides the syntax; here only later `markMode` changes are applied.
Expand Down Expand Up @@ -210,5 +213,11 @@ export function EditorExtensions({
}, [spellCheck]),
)

useExtension(
useMemo(() => {
return editorClassName ? defineViewAttributes({ class: editorClassName }) : null
}, [editorClassName]),
)

return null
}
33 changes: 33 additions & 0 deletions packages/react/src/components/editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,39 @@ describe('MeowdownEditor', () => {
await expect.element(page.locate('.test-wrap')).toBeInTheDocument()
})

// React's `className` is a whole-attribute write, so letting it own the
// editable root would drop the `ProseMirror` class ProseMirror adds itself —
// silently unstyling the whole editor until it remounts.
it('keeps the ProseMirror class when editorClassName changes', async () => {
const screen = await render(
<MeowdownEditor initialMarkdown="A [[link]]" editorClassName="first" />,
)
await expect.element(pmRoot).toHaveClass('meowdown-content')
await expect.element(pmRoot).toHaveClass('first')

await screen.rerender(<MeowdownEditor initialMarkdown="A [[link]]" editorClassName="second" />)

await expect.element(pmRoot).toHaveClass('ProseMirror')
await expect.element(pmRoot).toHaveClass('meowdown-content')
await expect.element(pmRoot).toHaveClass('second')
await expect.element(pmRoot).not.toHaveClass('first')
})

// The regression the class loss caused: `.ProseMirror .md-atom-view-content`
// is the only thing hiding a wikilink's raw `[[…]]` source.
it('keeps wikilink source hidden across an editorClassName change', async () => {
const source = page.locate('.md-atom-view-content')
const screen = await render(
<MeowdownEditor initialMarkdown="A [[link]]" mode="hide" editorClassName="first" />,
)
await expect.element(source).toHaveStyle({ fontSize: '0px' })

await screen.rerender(
<MeowdownEditor initialMarkdown="A [[link]]" mode="hide" editorClassName="second" />,
)
await expect.element(source).toHaveStyle({ fontSize: '0px' })
})

// The test browser forces `prefers-reduced-motion: reduce`, which zeroes the
// caret transition outright, so assert the `--meowdown-caret-glide` variable
// the transition reads instead of the transition itself.
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/components/prosekit-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import { createEditor, union, type SelectionJSON } from '@prosekit/core'
import type { EditorNode } from '@prosekit/pm/model'
import { Selection, TextSelection } from '@prosekit/pm/state'
import { ProseKit } from '@prosekit/react'
import { clsx } from 'clsx/lite'
import GithubSlugger from 'github-slugger'
import {
useCallback,
Expand Down Expand Up @@ -464,7 +463,7 @@ export function ProseKitEditor({

return (
<ProseKit editor={editor}>
<div ref={editor.mount} className={clsx('meowdown-content', editorClassName)}></div>
<div ref={editor.mount}></div>
<EditorExtensions
markMode={markMode}
onDocChange={handleDocChange}
Expand All @@ -486,6 +485,7 @@ export function ProseKitEditor({
readOnly={readOnly}
wikilinkEnabled={!!onWikilinkSearch}
spellCheck={spellCheck}
editorClassName={editorClassName}
/>
{blockHandle && !readOnly && <BlockHandle />}
{!readOnly && <TableHandle />}
Expand Down
Loading