diff --git a/.changeset/markdown-mdx-node-filters.md b/.changeset/markdown-mdx-node-filters.md new file mode 100644 index 0000000000..88176b6fcc --- /dev/null +++ b/.changeset/markdown-mdx-node-filters.md @@ -0,0 +1,5 @@ +--- +"@platejs/markdown": patch +--- + +- Apply `disallowedNodes` to HTML and MDX elements by the key of their deserialize rule, so with `remark-mdx`, `disallowedNodes: ['a']` also removes ``. diff --git a/content/docs/(plugins)/(serializing)/markdown.mdx b/content/docs/(plugins)/(serializing)/markdown.mdx index bf7385682f..e18ed5e22f 100644 --- a/content/docs/(plugins)/(serializing)/markdown.mdx +++ b/content/docs/(plugins)/(serializing)/markdown.mdx @@ -269,11 +269,16 @@ The core plugin configuration object. Use `MarkdownPlugin.configure({ options: { Whitelist specific node types (Plate types and Markdown AST types like `strong`). Cannot be used with `disallowedNodes`. If set, only listed - types are processed. Default: `null` (all allowed). + types are processed. With `remark-mdx`, `mdxJsxTextElement` admits + inline HTML and MDX elements and `mdxJsxFlowElement` admits block ones. + An admitted element with a deserialize rule converts without its rule key + listed. Default: `null` (all allowed). Blacklist specific node types. Cannot be used with `allowedNodes`. Listed - types are filtered out. Default: `null`. + types are filtered out. With `remark-mdx`, this includes HTML and MDX + elements by the key of their deserialize rule, such as `a` for + ``, `underline` for `` or `br` for `
`. Default: `null`. Fine-grained node filtering with custom functions, applied *after* diff --git a/docs/plans/5140-mdx-element-node-filters.md b/docs/plans/5140-mdx-element-node-filters.md new file mode 100644 index 0000000000..e6838502f5 --- /dev/null +++ b/docs/plans/5140-mdx-element-node-filters.md @@ -0,0 +1,42 @@ +# Issue 5140: node filters for inline HTML elements + +Status: in review +PR: https://github.com/udecode/plate/pull/5141 + +## Outcome + +[Issue 5140](https://github.com/udecode/plate/issues/5140): with `remark-mdx`, `deserializeMd` checked `allowedNodes` and `disallowedNodes` against an inline HTML element's mdast type, `mdxJsxTextElement`, and never against the Plate type it becomes. So `disallowedNodes: ['a']` dropped Markdown links but kept `
` anchors, and since PR 5139 those anchors carry a working URL. + +After the fix, `disallowedNodes` also checks an HTML or MDX element by the key of its deserialize rule, at the top level and inside marks: `a` for ``, `underline` for ``, `br` for `
`. `disallowedNodes: ['a']` removes `
`. Allowlists behave exactly as on `main`, so `
` and `` styles keep their text wherever `main` kept it. Elements without a rule still fall back to source text. + +## Main changes + +- `customMdxDeserialize` resolves the element's rule key once: the plugin key the tag resolves to, or the tag name when no plugin owns it. When a rule exists and `disallowedNodes` lists that key, it returns nothing before the rule runs. +- `allowNode.deserialize` is unchanged. It still receives the MDX node, with `name` available for element checks. +- The `disallowedNodes` JSDoc in `MarkdownPlugin.ts` and the Markdown docs page state the new behavior with `remark-mdx`. The `allowedNodes` JSDoc and docs now say what `main` already did: `mdxJsxTextElement` admits inline HTML and MDX elements, `mdxJsxFlowElement` admits block ones, and an admitted element with a rule converts without its rule key listed. A patch changeset describes the change from `main`. + +## Defaults + +| Decision | Pick | Alternative | Word | +| --- | --- | --- | --- | +| Allowlist rule | Unchanged from `main`; the key check applies to `disallowedNodes` only | Check allowlists by key too, which drops `
` and `` text from allowlists that admit `mdxJsxTextElement` | "check allowlists" | +| Cell-list fallback after filtering | A table-cell list whose blocked block child the filter removed converts, keeping only allowed content | Keep the text fallback whenever the unfiltered item held a block | "keep fallback" | +| `allowNode` | Unchanged; it still sees the MDX node | Call it with the resolved type | "allowNode resolved" | +| Chinese docs | Left to languine, as with PR 5139 | Translate the two sentences by hand | "translate cn" | +| `next` | Not ported here; `next` has its own markdown package and the main-to-next sync owns it | Port the fix now | "port" | + +## Proof + +- Red then green: `a
b c
d` under `disallowedNodes: ['underline']` keeps `` without the check line and removes only `` with it, keeping the line break and the link (`deserializer/deserializeMd.spec.ts`). The issue's own case, `` under `disallowedNodes: ['a']`, failed on `main` at `ef90c27119` and passed with the first version of the test. +- Red then green for allowlists: `a
b c` under `allowedNodes: ['p', 'text', 'mdxJsxTextElement']` lost `
` and `` with the first version of the fix, which also checked allowlists, and keeps both now (`deserializer/deserializeMd.spec.ts`). +- `pnpm --filter @platejs/markdown test` in the worktree: 274 pass, 0 fail. `pnpm turbo typecheck --filter=./packages/markdown`: 13/13. +- Probes (deleted after): inside bold, `
` is removed under `disallowedNodes: ['a']`, while `**[l](https://p.org)**` keeps its link, which is the pre-existing bypass listed under Follow-ups. `b` falls back to source text. + +## Follow-ups + +- Node filters skip Markdown children of `**`, `*`, `~~` and list items (`x **[l](https://p.org)**` keeps its link under `disallowedNodes: ['a']`); filed as a follow-up issue. +- `` resolves to the rule key `code_block` through the Markdown type table, so `disallowedNodes: ['code_block']` removes inline `` and `['code']` does not. Pre-existing mapping; owner: zbeyens, untracked. +- An HTML element nested in an unknown tag, or in a tail re-parsed without `remark-mdx` after an MDX error, stays literal source text and is not removed. No live element is created on either path. Owner: zbeyens, untracked. +- An element left with no children after filtering, such as a heading holding only a removed link, is not padded. `main` has the same gap for Markdown children. Owner: zbeyens, untracked. +- `allowNode.deserialize` still sees `mdxJsxTextElement` for HTML elements. Owner: zbeyens, untracked. +- The unknown-element fallback builds a block `p` that no allowlist checks, and drops the mark of its context, so `a **bar**` returns unbolded tag text. Pre-existing; owner: zbeyens, untracked. diff --git a/packages/markdown/src/lib/MarkdownPlugin.ts b/packages/markdown/src/lib/MarkdownPlugin.ts index 69c5c79098..7d46205e40 100644 --- a/packages/markdown/src/lib/MarkdownPlugin.ts +++ b/packages/markdown/src/lib/MarkdownPlugin.ts @@ -27,12 +27,16 @@ export type MarkdownConfig = PluginConfig< { /** * Configuration for allowed node types. Cannot be combined with - * disallowedNodes. + * disallowedNodes. With remark-mdx, `mdxJsxTextElement` admits inline HTML + * and MDX elements and `mdxJsxFlowElement` admits block ones. An admitted + * element with a deserialize rule converts without its rule key listed. */ allowedNodes: PlateType[] | null; /** * Configuration for disallowed node types. Cannot be combined with - * allowedNodes. + * allowedNodes. With remark-mdx, this also removes HTML and MDX elements by + * the key of their deserialize rule, such as `a` for ``, + * `underline` for `` or `br` for `
`. * * @default null */ diff --git a/packages/markdown/src/lib/deserializer/deserializeMd.spec.ts b/packages/markdown/src/lib/deserializer/deserializeMd.spec.ts index d63ab83623..da2582c5bb 100644 --- a/packages/markdown/src/lib/deserializer/deserializeMd.spec.ts +++ b/packages/markdown/src/lib/deserializer/deserializeMd.spec.ts @@ -193,6 +193,53 @@ describe('deserializeMd', () => { ]); }); + it('removes inline html elements whose deserialize rule key is disallowed', () => { + const editor = createTestEditor(); + + expect( + deserializeMd( + editor, + 'a
b c
d', + { disallowedNodes: ['underline'] } + ) + ).toEqual([ + { + children: [ + { text: 'a' }, + { text: '\n' }, + { text: 'b ' }, + { text: ' ' }, + { + children: [{ text: 'd' }], + type: 'a', + url: 'https://platejs.org', + }, + ], + type: 'p', + }, + ]); + }); + + it('keeps inline html under an allowlist that admits mdxJsxTextElement', () => { + const editor = createTestEditor(); + + expect( + deserializeMd(editor, 'a
b c', { + allowedNodes: ['p', 'text', 'mdxJsxTextElement'], + }) + ).toEqual([ + { + children: [ + { text: 'a' }, + { text: '\n' }, + { text: 'b ' }, + { text: 'c', underline: true }, + ], + type: 'p', + }, + ]); + }); + it('preserves raw html blocks as editable source text paragraphs', () => { const editor = createTestEditor(); diff --git a/packages/markdown/src/lib/deserializer/utils/customMdxDeserialize.ts b/packages/markdown/src/lib/deserializer/utils/customMdxDeserialize.ts index 006bef3730..ad414f5162 100644 --- a/packages/markdown/src/lib/deserializer/utils/customMdxDeserialize.ts +++ b/packages/markdown/src/lib/deserializer/utils/customMdxDeserialize.ts @@ -87,13 +87,17 @@ export const customMdxDeserialize = ( getPluginKey(options.editor!, customJsxElementKey as any) ?? mdastNode.name; if (key) { - const nodeParserDeserialize = getDeserializerByKey( - mdastToPlate(options.editor!, key as any), - options - ); + const ruleKey = mdastToPlate(options.editor!, key as any); + const nodeParserDeserialize = getDeserializerByKey(ruleKey, options); + + if (nodeParserDeserialize) { + // disallowedNodes names rule keys, such as `a` or `underline`, which an + // mdx node's type does not reveal. Allowlists are not checked here, since + // rule keys like `br` and `span` have no plugin a user would list. + if (options.disallowedNodes?.includes(ruleKey)) return []; - if (nodeParserDeserialize) return nodeParserDeserialize(mdastNode, deco, options) as any; + } } else { console.warn( 'This MDX node does not have a parser for deserialization',