Skip to content

Structure intrinsic MDX elements in Studio#155

Merged
iipanda merged 2 commits into
mainfrom
codex/structured-intrinsic-mdx-elements
May 28, 2026
Merged

Structure intrinsic MDX elements in Studio#155
iipanda merged 2 commits into
mainfrom
codex/structured-intrinsic-mdx-elements

Conversation

@iipanda
Copy link
Copy Markdown
Collaborator

@iipanda iipanda commented May 28, 2026

Summary

  • parse lowercase MDX/HTML tags as structured Studio blocks instead of raw JSX text
  • add intrinsic element node-view rendering, selection, prop/style editing, and serialization
  • document the editor contract and add the @mdcms/studio changeset

Validation

  • bun test packages/studio/src/lib/markdown-pipeline.test.ts packages/studio/src/lib/runtime-ui/components/editor/mdx-component-selection.test.ts packages/studio/src/lib/runtime-ui/components/editor/mdx-component-panel-selection.test.ts packages/studio/src/lib/runtime-ui/pages/content-document-page.test.tsx
  • bun run typecheck
  • bun run format:check
  • bun run check
  • bun run changeset:check

Summary by CodeRabbit

  • New Features
    • Lowercase HTML/MDX elements (<div>, <form>, <input>, <button>, etc.) are now rendered as structured, editable blocks in the editor.
    • Full support for void elements with proper self-closing serialization.
    • Props panel enables editing of intrinsic element attributes and inline styles.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

📝 Walkthrough

Walkthrough

This PR adds structured block support for lowercase MDX/HTML intrinsic elements (e.g., <div>, <form>, <input>) in the Studio editor. Lowercase tags are parsed into dedicated mdxIntrinsicElement TipTap nodes with tag name, props, and void semantics; they serialize back to HTML with correct self-closing syntax. Selection, props editing, and node views are generalized to treat intrinsic elements as first-class MDX blocks distinct from catalog components.

Changes

MDX Intrinsic Element Architecture

Layer / File(s) Summary
Design & Specification
.ai/plans/2026-05-28-..., .ai/research/2026-05-28-..., .changeset/..., docs/specs/SPEC-007-..., docs/specs/SPEC-014-...
Design decisions and specification updates document intrinsic elements as structured blocks with tag name, props, isVoid, and editable children; raw JSX becomes a last-resort fallback for unparseable content.
HTML Void Elements Constant
packages/studio/src/lib/html-void-elements.ts
New HTML_VOID_ELEMENTS set exports standard void tag names (area, base, br, input, img, etc.) for determining self-closing intrinsic elements.
MDX Intrinsic Element Extension
packages/studio/src/lib/mdx-intrinsic-element-extension.ts
TipTap node extension with opening/closing tag parser, attribute extraction, void-element detection, markdown tokenizer, and serialization; includes ProseMirror transaction guards preventing void nodes from acquiring content.
MDX Parser Intrinsic Conversion
packages/studio/src/lib/mdx-markdown-parser.ts
Adds isLowercaseIntrinsicName helper and intrinsic-element conversion logic in convertMdxJsxElementNode; routes lowercase JSX elements to mdxIntrinsicElement when parseable, else falls back to raw JSX.
Round-trip Parser Tests
packages/studio/src/lib/markdown-pipeline.test.ts
New tests verify parsing intrinsic wrapper/form tags into structured nodes, serialization with correct void/self-closing syntax, and raw-JSX fallback for unsupported content.
Selection System Generalization
packages/studio/src/lib/runtime-ui/components/editor/mdx-component-selection.ts, mdx-component-selection.test.ts
SelectedMdxComponent gains kind discriminator ("component" | "intrinsic"); selection detection, prop updates, and catalog lookup now handle both mdxComponent and mdxIntrinsicElement nodes.
Selection Snapshot Tracking
packages/studio/src/lib/runtime-ui/components/editor/mdx-component-panel-selection.ts, mdx-component-panel-selection.test.ts
PublishedMdxComponentSelectionSnapshot includes kind field; equality checks account for kind changes.
Intrinsic Node View Rendering
packages/studio/src/lib/runtime-ui/components/editor/mdx-intrinsic-element-node-view.tsx
React component rendering intrinsic blocks: sanitizes props for void-element previews, wraps non-void children in editable slot with event interception, integrates collapse/expand state, and supports edit/duplicate/unwrap/delete commands.
Props Panel & Style Inspector
packages/studio/src/lib/runtime-ui/components/editor/mdx-props-panel.tsx, visual-style-inspector.tsx
Props panel renders intrinsic selections with simplified "style-only" component; VisualStyleInspector type narrowed to require only name and extractedProps.
Extension Registration in Editor
packages/studio/src/lib/editor-extensions.ts, packages/studio/src/lib/runtime-ui/components/editor/tiptap-editor.tsx
MdxIntrinsicElementExtension and MdxIntrinsicElementNodeView wired into editor extensions array and TipTap configuration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • mdcms-ai/mdcms#111: Both modify mdx-props-panel.tsx to route selection rendering by kind discriminator (main adds "intrinsic" path, referenced refactors "wrapper" guidance).
  • mdcms-ai/mdcms#133: Main PR integrates collapse/expand state via useMdxComponentCollapseSnapshot in intrinsic node view, which was introduced by referenced PR to manage MDX component collapse state.
  • mdcms-ai/mdcms#141: Both touch editor-extensions.ts to extend createEditorExtensions TipTap wiring (main adds mdxIntrinsicElement, referenced adds EmptyParagraphHint).

Poem

🐰 Lowercase tags now have a home,
No more raw JSX to roam,
Structured blocks for div and form,
Studio edits take shape and form,
<input /> void, without a care—
Intrinsic elements everywhere!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Structure intrinsic MDX elements in Studio' clearly and concisely summarizes the main change: adding support for lowercase MDX/HTML elements as structured Studio blocks rather than raw JSX.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/structured-intrinsic-mdx-elements

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/studio/src/lib/runtime-ui/components/editor/mdx-props-panel.tsx (1)

27-29: ⚡ Quick win

Make kind required on MdxPropsPanelSelection.

Keeping kind optional allows intrinsic selections to silently fall into the non-intrinsic path if a caller forgets to pass it.

🧩 Suggested type tightening
 export type MdxPropsPanelSelection = {
-  kind?: "component" | "intrinsic";
+  kind: "component" | "intrinsic";
   component: MdxCatalogComponent | undefined;
   componentName: string;
   isVoid: boolean;

As per coding guidelines, "Use TypeScript in strict mode with nodenext module resolution and composite projects enabled".

Also applies to: 60-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/studio/src/lib/runtime-ui/components/editor/mdx-props-panel.tsx`
around lines 27 - 29, The MdxPropsPanelSelection type currently has an optional
kind which can cause intrinsic selections to be misrouted; make kind required by
changing its declaration in the MdxPropsPanelSelection type (the symbol to edit
is MdxPropsPanelSelection with fields kind and component) so kind is not
optional, then update any usages that construct or narrow MdxPropsPanelSelection
(e.g., code paths referenced around the selection handling logic) to always
supply a valid "component" or "intrinsic" value for kind or adjust callers to
pass the correct discriminant; ensure related type checks and switch/case
branches rely on the now-required discriminant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.ai/plans/2026-05-28-structured-intrinsic-mdx-elements.md:
- Line 13: The heading "### Task 1: Parser Contract Tests" jumps from an H1 to
H3 causing MD001; change that heading to an H2 by replacing "### Task 1: Parser
Contract Tests" with "## Task 1: Parser Contract Tests" (or alternatively add a
parent H2 above it), ensuring the heading level sequence is H1 → H2 → H3 for
downstream sections.

In `@docs/specs/SPEC-007-editor-mdx-and-collaboration.md`:
- Around line 259-276: The spec contains a contract conflict between the
"Lowercase intrinsic MDX/HTML elements" section (which treats parseable
lowercase HTML as structured intrinsic blocks) and the "Children / Nested
Content" section (which claims lowercase HTML remains raw-island content); pick
and document a single behavior and update the "Children / Nested Content" text
to match: declare that parseable lowercase intrinsic elements (e.g., div, form,
label, input, button) are parsed into intrinsic blocks with props/void/editable
children and only unparseable/unsupported syntax is preserved as a raw MDX
island; make sure to reference the terms "intrinsic blocks" and "raw MDX
islands" in the revised paragraph so implementers know which path to follow.

In
`@packages/studio/src/lib/runtime-ui/components/editor/mdx-intrinsic-element-node-view.tsx`:
- Around line 22-30: Update the intrinsic preview sanitization to block
URL-based resource loads: add URL-bearing attribute names such as "srcset" (and
any case-insensitive variants you check) to the URL_BEARING_ATTRIBUTES set and
ensure the attribute-checking logic (the code that iterates attributes for MDX
intrinsic nodes) rejects values that contain URL patterns (e.g., /url\s*\(/i or
value starting with "http:", "https:", "//", or data: for non-allowed types) as
well as disallowing style attributes that include url(...) by treating "style"
as unsafe or by explicitly scanning style values for url(...) and rejecting
them; also keep UNSAFE_ATTRIBUTES (like "srcdoc" and "dangerouslySetInnerHTML")
intact and apply the same case-insensitive checks in the other attribute-filter
locations referenced around the 90-119 and 121-125 ranges so all attribute
filters use the updated URL_BEARING_ATTRIBUTES and the url(...) / protocol
checks.

---

Nitpick comments:
In `@packages/studio/src/lib/runtime-ui/components/editor/mdx-props-panel.tsx`:
- Around line 27-29: The MdxPropsPanelSelection type currently has an optional
kind which can cause intrinsic selections to be misrouted; make kind required by
changing its declaration in the MdxPropsPanelSelection type (the symbol to edit
is MdxPropsPanelSelection with fields kind and component) so kind is not
optional, then update any usages that construct or narrow MdxPropsPanelSelection
(e.g., code paths referenced around the selection handling logic) to always
supply a valid "component" or "intrinsic" value for kind or adjust callers to
pass the correct discriminant; ensure related type checks and switch/case
branches rely on the now-required discriminant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ff9c1b1-655c-4b07-b202-c16e92173167

📥 Commits

Reviewing files that changed from the base of the PR and between 55e7e4e and 89ccd9e.

📒 Files selected for processing (18)
  • .ai/plans/2026-05-28-structured-intrinsic-mdx-elements.md
  • .ai/research/2026-05-28-structured-intrinsic-mdx-elements-design.md
  • .changeset/silly-months-kneel.md
  • docs/specs/SPEC-007-editor-mdx-and-collaboration.md
  • docs/specs/SPEC-014-ai-assisted-studio-editing.md
  • packages/studio/src/lib/editor-extensions.ts
  • packages/studio/src/lib/html-void-elements.ts
  • packages/studio/src/lib/markdown-pipeline.test.ts
  • packages/studio/src/lib/mdx-intrinsic-element-extension.ts
  • packages/studio/src/lib/mdx-markdown-parser.ts
  • packages/studio/src/lib/runtime-ui/components/editor/mdx-component-panel-selection.test.ts
  • packages/studio/src/lib/runtime-ui/components/editor/mdx-component-panel-selection.ts
  • packages/studio/src/lib/runtime-ui/components/editor/mdx-component-selection.test.ts
  • packages/studio/src/lib/runtime-ui/components/editor/mdx-component-selection.ts
  • packages/studio/src/lib/runtime-ui/components/editor/mdx-intrinsic-element-node-view.tsx
  • packages/studio/src/lib/runtime-ui/components/editor/mdx-props-panel.tsx
  • packages/studio/src/lib/runtime-ui/components/editor/tiptap-editor.tsx
  • packages/studio/src/lib/runtime-ui/components/editor/visual-style-inspector.tsx


---

### Task 1: Parser Contract Tests
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix heading level jump to keep markdown lint-clean.

Line 13 jumps from an H1 section to H3 (###) without an intermediate H2, which triggers MD001. Change it to ## (or insert a missing ## parent heading).

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 13-13: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.ai/plans/2026-05-28-structured-intrinsic-mdx-elements.md at line 13, The
heading "### Task 1: Parser Contract Tests" jumps from an H1 to H3 causing
MD001; change that heading to an H2 by replacing "### Task 1: Parser Contract
Tests" with "## Task 1: Parser Contract Tests" (or alternatively add a parent H2
above it), ensuring the heading level sequence is H1 → H2 → H3 for downstream
sections.

Comment on lines +259 to +276
Lowercase intrinsic MDX/HTML elements such as `<div>`, `<form>`, `<label>`,
`<input>`, and `<button>` remain valid advanced MDX authoring syntax. When their
attributes can be represented as Studio props, Studio parses them into
first-class visual composition blocks with their tag name, props, void status,
and editable children. Intrinsic blocks are not catalog components, are not
shown in the insert component palette by default, and do not participate in host
component prop-schema validation. They still render in the canvas with the same
block affordances as MDX components so editors never see a partial raw HTML
preview as the normal editing surface.

Studio preserves intrinsic HTML semantics during serialization: an intrinsic
`div` block serializes as `<div>`, a `form` block serializes as `<form>`, and
void intrinsic elements such as `<input />` serialize as self-closing lowercase
HTML. Built-ins are the supported visual-editing primitives for common layout
and inline content, while intrinsic blocks cover valid native HTML semantics and
legacy MDX content. Raw MDX islands are a last-resort preservation fallback only
for unparseable or unsupported syntax; they must be clearly labeled as
unsupported content rather than rendered as a half-raw inline preview.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve contract conflict on lowercase intrinsic handling.

This new section says parseable lowercase intrinsic HTML should be structured blocks, but the later “Children / Nested Content” section still states raw lowercase HTML remains raw-island content. Please align both sections to one behavior to avoid conflicting implementation guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/SPEC-007-editor-mdx-and-collaboration.md` around lines 259 - 276,
The spec contains a contract conflict between the "Lowercase intrinsic MDX/HTML
elements" section (which treats parseable lowercase HTML as structured intrinsic
blocks) and the "Children / Nested Content" section (which claims lowercase HTML
remains raw-island content); pick and document a single behavior and update the
"Children / Nested Content" text to match: declare that parseable lowercase
intrinsic elements (e.g., div, form, label, input, button) are parsed into
intrinsic blocks with props/void/editable children and only
unparseable/unsupported syntax is preserved as a raw MDX island; make sure to
reference the terms "intrinsic blocks" and "raw MDX islands" in the revised
paragraph so implementers know which path to follow.

Comment on lines +22 to +30
const URL_BEARING_ATTRIBUTES = new Set([
"action",
"formaction",
"href",
"poster",
"src",
"xlinkhref",
]);
const UNSAFE_ATTRIBUTES = new Set(["srcdoc", "dangerouslysetinnerhtml"]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden intrinsic preview sanitization against URL-based resource loads.

The current filter still allows URL-bearing inputs (e.g., srcSet) and style values containing url(...), which can trigger unintended external requests when previewing user-authored content.

🔒 Proposed hardening
 const URL_BEARING_ATTRIBUTES = new Set([
   "action",
+  "background",
+  "cite",
+  "data",
   "formaction",
   "href",
+  "ping",
   "poster",
   "src",
+  "srcset",
+  "usemap",
   "xlinkhref",
 ]);
+
+function hasCssUrl(value: string): boolean {
+  return /\burl\s*\(/i.test(value);
+}
@@
     if (name === "style") {
       if (isFlatStyleRecord(value)) {
-        safeProps[name] = value;
+        const sanitizedStyle = Object.fromEntries(
+          Object.entries(value).filter(([, entry]) => {
+            return typeof entry === "number" || !hasCssUrl(entry);
+          }),
+        );
+        if (Object.keys(sanitizedStyle).length > 0) {
+          safeProps[name] = sanitizedStyle;
+        }
       }
       continue;
     }

Also applies to: 90-119, 121-125

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/studio/src/lib/runtime-ui/components/editor/mdx-intrinsic-element-node-view.tsx`
around lines 22 - 30, Update the intrinsic preview sanitization to block
URL-based resource loads: add URL-bearing attribute names such as "srcset" (and
any case-insensitive variants you check) to the URL_BEARING_ATTRIBUTES set and
ensure the attribute-checking logic (the code that iterates attributes for MDX
intrinsic nodes) rejects values that contain URL patterns (e.g., /url\s*\(/i or
value starting with "http:", "https:", "//", or data: for non-allowed types) as
well as disallowing style attributes that include url(...) by treating "style"
as unsafe or by explicitly scanning style values for url(...) and rejecting
them; also keep UNSAFE_ATTRIBUTES (like "srcdoc" and "dangerouslySetInnerHTML")
intact and apply the same case-insensitive checks in the other attribute-filter
locations referenced around the 90-119 and 121-125 ranges so all attribute
filters use the updated URL_BEARING_ATTRIBUTES and the url(...) / protocol
checks.

@iipanda iipanda merged commit 8c7b896 into main May 28, 2026
6 checks passed
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.

1 participant