From 0a53bb82b2f04b17bb5a1fb5cdc58c8cad38bd4a Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:41:48 +0200
Subject: [PATCH 1/2] fix(markdown): apply disallowedNodes to MDX elements by
rule key
With remark-mdx, the node filters saw an inline HTML element only as
mdxJsxTextElement, so disallowedNodes: ['a'] dropped Markdown links but
kept anchors. customMdxDeserialize now removes an element whose
deserialize rule key is listed in disallowedNodes before the rule runs.
allowedNodes keeps its behavior: mdxJsxTextElement admits inline HTML and
MDX elements and mdxJsxFlowElement admits block ones.
---
.changeset/markdown-mdx-node-filters.md | 5 ++
.../docs/(plugins)/(serializing)/markdown.mdx | 9 +++-
docs/plans/5140-mdx-element-node-filters.md | 41 ++++++++++++++++
packages/markdown/src/lib/MarkdownPlugin.ts | 8 +++-
.../lib/deserializer/deserializeMd.spec.ts | 47 +++++++++++++++++++
.../utils/customMdxDeserialize.ts | 14 ++++--
6 files changed, 115 insertions(+), 9 deletions(-)
create mode 100644 .changeset/markdown-mdx-node-filters.md
create mode 100644 docs/plans/5140-mdx-element-node-filters.md
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..04df5157bd
--- /dev/null
+++ b/docs/plans/5140-mdx-element-node-filters.md
@@ -0,0 +1,41 @@
+# Issue 5140: node filters for inline HTML elements
+
+Status: in review
+
+## 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',
From 5ee4bd4031d4ee0820ba47e3860c991759a0ff0b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:42:21 +0200
Subject: [PATCH 2/2] docs(plans): link the 5140 plan to PR 5141
---
docs/plans/5140-mdx-element-node-filters.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/plans/5140-mdx-element-node-filters.md b/docs/plans/5140-mdx-element-node-filters.md
index 04df5157bd..e6838502f5 100644
--- a/docs/plans/5140-mdx-element-node-filters.md
+++ b/docs/plans/5140-mdx-element-node-filters.md
@@ -1,6 +1,7 @@
# Issue 5140: node filters for inline HTML elements
Status: in review
+PR: https://github.com/udecode/plate/pull/5141
## Outcome