Skip to content

fix: fix rendering content in table magic blocks#1318

Merged
maximilianfalco merged 53 commits intonextfrom
falco/fix-rendering-content-in-table-magic-blocks
Feb 12, 2026
Merged

fix: fix rendering content in table magic blocks#1318
maximilianfalco merged 53 commits intonextfrom
falco/fix-rendering-content-in-table-magic-blocks

Conversation

@maximilianfalco
Copy link
Copy Markdown
Contributor

@maximilianfalco maximilianfalco commented Feb 3, 2026

PR App Fix CX-2870 Fix CX-2830 Fix CX-2872 Fix CX-2869

🧰 Changes

Add preprocessing in parseTableCell of magic-block-transformer to:

  1. Process backslash escapes (\< → &lt;) and col characters (|)
  2. Escape invalid HTML tags
  3. Convert markdown emphasis/strong/code to HTML inside HTML blocks
  4. Normalize whitespace to prevent raw HTML block mode

🧬 QA & Testing

- also created the appropriate transformer
- deleted the old `mdxish-magic-blocks` transformer
- this was necessary since the old logic introduced whitespaces that are no longer here
- fixed issue where callouts without title werent rendering properly
- fixed issue where callouts without body werent rendering properly
- fixed issue where caption of images would be rendered as plain text instead of being parsed
- malformed syntax is returned as raw text instead of erroring
@maximilianfalco
Copy link
Copy Markdown
Contributor Author

@rafegoldberg I did a bit of refactor on this, its a bit more convoluted and complex but it should be more robust in general. it might degrade performance a tiny bit 🤞 but it shouldnt really matter since we rarely see these complex magic block tables anyways...

ive placed all the refactor here 249d6bd. these are the steps now

  1. still preprocess escaped syntax. i dont see any other way around this since we need to transform them into safe HTML strings
  2. we parse the markdown as usual (markdown -> MDAST)

when we hit an html node, we store the entire value as text. including the markdown within

  1. we find all html nodes and run processMarkdownInHtmlString() on them (if we find there is markdown syntax). inside this a few things happen:
  2. we convert the html string to HAST
  3. the HAST has text nodes that contain these markdown syntaxes. we find these nodes and parse them from text -> HAST
  4. we are then left with a single unified HAST tree, containing the original HAST components and the parse markdown syntaxes
  5. we then stringify (HAST -> text) the entire tree so that the original html node in step 2 still maintains a regular HTML node that contains HTML string

its a bit confusing yea... took my head a bit too to get around it but it looks to be the most robust way... we still needed to re-stringify it so that we maintain the original html node and its contents in the MDAST tree

I asked claude to help visualize a test case and it did it pretty well here:

Detailed Flow
Input: "<ul><li>**bold**</li></ul>"

---
Step 1: Escape backslashes

Input:  "<ul><li>**bold**</li></ul>"
Output: "<ul><li>**bold**</li></ul>"  (unchanged, no backslashes)

Example with backslashes:
Input:  "yes\|no"
Output: "yes|no"

---
Step 2: Parse markdown → MDAST

contentParser.parse("<ul><li>**bold**</li></ul>")

┌─────────────────────────────────────────────────────────┐
│ MDAST                                                   │
├─────────────────────────────────────────────────────────┤
│ {                                                       │
│   type: "root",                                         │
│   children: [                                           │
│     {                                                   │
│       type: "html",                                     │
│       value: "<ul><li>**bold**</li></ul>"  ← PROBLEM!   │
│     }                                        still text │
│   ]                                                     │
│ }                                                       │
└─────────────────────────────────────────────────────────┘

Why this happens: CommonMark spec says HTML blocks are opaque.
The parser doesn't look inside.

---
Step 3: processMarkdownInHtmlNodes(tree)

Visits each html node and calls processMarkdownInHtmlString(node.value)

---
Step 3a: Parse HTML string → HAST

htmlToHast.parse("<ul><li>**bold**</li></ul>")

┌─────────────────────────────────────────────────────────┐
│ HAST                                                    │
├─────────────────────────────────────────────────────────┤
│ {                                                       │
│   type: "root",                                         │
│   children: [                                           │
│     {                                                   │
│       type: "element",                                  │
│       tagName: "ul",                                    │
│       children: [                                       │
│         {                                               │
│           type: "element",                              │
│           tagName: "li",                                │
│           children: [                                   │
│             {                                           │
│               type: "text",                             │
│               value: "**bold**"  ← markdown still here! │
│             }                                           │
│           ]                                             │
│         }                                               │
│       ]                                                 │
│     }                                                   │
│   ]                                                     │
│ }                                                       │
└─────────────────────────────────────────────────────────┘

---
Step 3b: Find text nodes, parse as markdown → HAST

For each text node "**bold**":

markdownToHast.parse("**bold**")

┌─────────────────────────────────────────────────────────┐
│ Intermediate MDAST (from markdown parser)               │
├─────────────────────────────────────────────────────────┤
│ {                                                       │
│   type: "root",                                         │
│   children: [                                           │
│     {                                                   │
│       type: "paragraph",                                │
│       children: [                                       │
│         {                                               │
│           type: "strong",                               │
│           children: [                                   │
│             { type: "text", value: "bold" }             │
│           ]                                             │
│         }                                               │
│       ]                                                 │
│     }                                                   │
│   ]                                                     │
│ }                                                       │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼ remarkRehype
┌─────────────────────────────────────────────────────────┐
│ Converted to HAST                                       │
├─────────────────────────────────────────────────────────┤
│ {                                                       │
│   type: "root",                                         │
│   children: [                                           │
│     {                                                   │
│       type: "element",                                  │
│       tagName: "p",          ← unwrap this!             │
│       children: [                                       │
│         {                                               │
│           type: "element",                              │
│           tagName: "strong",                            │
│           children: [                                   │
│             { type: "text", value: "bold" }             │
│           ]                                             │
│         }                                               │
│       ]                                                 │
│     }                                                   │
│   ]                                                     │
│ }                                                       │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼ unwrap <p>
┌─────────────────────────────────────────────────────────┐
│ Final HAST nodes to insert                              │
├─────────────────────────────────────────────────────────┤
│ [                                                       │
│   {                                                     │
│     type: "element",                                    │
│     tagName: "strong",                                  │
│     children: [                                         │
│       { type: "text", value: "bold" }                   │
│     ]                                                   │
│   }                                                     │
│ ]                                                       │
└─────────────────────────────────────────────────────────┘

---
Step 3c: Replace text node in HAST

┌─────────────────────────────────────────────────────────┐
│ HAST (after replacement)                                │
├─────────────────────────────────────────────────────────┤
│ {                                                       │
│   type: "root",                                         │
│   children: [                                           │
│     {                                                   │
│       type: "element",                                  │
│       tagName: "ul",                                    │
│       children: [                                       │
│         {                                               │
│           type: "element",                              │
│           tagName: "li",                                │
│           children: [                                   │
│             {                                           │
│               type: "element",                          │
│               tagName: "strong",  ← was text "**bold**" │
│               children: [                               │
│                 { type: "text", value: "bold" }         │
│               ]                                         │
│             }                                           │
│           ]                                             │
│         }                                               │
│       ]                                                 │
│     }                                                   │
│   ]                                                     │
│ }                                                       │
└─────────────────────────────────────────────────────────┘

---
Step 3d: Stringify HAST → HTML string

hastToHtml.stringify(hast)

Output: "<ul><li><strong>bold</strong></li></ul>"

---
Step 3e: Update MDAST html node

┌─────────────────────────────────────────────────────────┐
│ MDAST (after processMarkdownInHtmlNodes)                │
├─────────────────────────────────────────────────────────┤
│ {                                                       │
│   type: "root",                                         │
│   children: [                                           │
│     {                                                   │
│       type: "html",                                     │
│       value: "<ul><li><strong>bold</strong></li></ul>"  │
│     }                        ↑ markdown now converted!  │
│   ]                                                     │
│ }                                                       │
└─────────────────────────────────────────────────────────┘

---
Step 4: Unwrap and return

return tree.children.flatMap(n =>
  n.type === 'paragraph' ? n.children : [n]
);

Since it's an html node (not paragraph), return as-is:

┌─────────────────────────────────────────────────────────┐
│ Final Output: MdastNode[]                               │
├─────────────────────────────────────────────────────────┤
│ [                                                       │
│   {                                                     │
│     type: "html",                                       │
│     value: "<ul><li><strong>bold</strong></li></ul>"    │
│   }                                                     │
│ ]                                                       │
└─────────────────────────────────────────────────────────┘    

@eaglethrost
Copy link
Copy Markdown
Contributor

Code looks good to me. Yeah it's not ideal we have to create another mini md processor just for magic blocks, but I think works for now. We didn't design the table magic block to convert to the MDX

because technically they're a different and might have different stylings and behaviour. But in the future I do think it's worth doing so to reduce code duplications like this and clean it up further

Copy link
Copy Markdown
Contributor

@rafegoldberg rafegoldberg left a comment

Choose a reason for hiding this comment

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

Appreciate the details @maximilianfalco. I'm (very) cautiously approving this, though I'd love to address the following comments ASAP.

Comment on lines +78 to +88
/** Markdown parser */
const contentParser = unified().use(remarkParse).use(remarkGfm).use(normalizeEmphasisAST);

/** Markdown to HTML processor (mdast → hast → HTML string) */
const markdownToHtml = unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeStringify);

/** HTML parser (HTML string → hast) */
const htmlParser = unified().use(rehypeParse, { fragment: true });

/** HTML stringifier (hast → HTML string) */
const htmlStringifier = unified().use(rehypeStringify);
Copy link
Copy Markdown
Contributor

@rafegoldberg rafegoldberg Feb 11, 2026

Choose a reason for hiding this comment

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

I have two concerns about this pattern of instantiating multiple subprocessors:

  1. Firstly, performance especially on large tables with lots of cells and complex content. Can we add a test for such a scenario, or at least manually run a benchmark to see how well this approach is working?

  2. And secondly, it doesn't seem like these subprocessors will support any of our custom syntax (like variables or glossary terms) within cells. Is that an accurate assumption?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

performance seems to be stable and alright, I added a perf test in da09016 and they seem to be working just fine. the perf deg isnt significant

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Confirmed variables & glossary are rendered in magic blocks including tables

Screen.Recording.2026-02-12.at.2.35.12.pm.mov

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ive added support for the new glossary syntax here as well 7ad3fea

let counter = 0;
const safened = escapeInvalidTags(html).replace(HTML_TAG_RE, match => {
if (!/^<\/?[A-Z]/.test(match)) return match;
const id = `<!--PC${(counter += 1)}-->`;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

need to do some replace and restore here because rehype-parse would mangle away the <Glossary> tags

Copy link
Copy Markdown
Contributor

@rafegoldberg rafegoldberg left a comment

Choose a reason for hiding this comment

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

Appreciate the followup @maximilianfalco. Let's clean up that regex per our thread and get these conflicts resolved. But otherwise this seems to be rendering as expected. 👌

…ontent-in-table-magic-blocks

# Conflicts:
#	__tests__/lib/mdxish/magic-blocks.test.ts
#	processor/transform/mdxish/magic-blocks/magic-block-transformer.ts
@maximilianfalco maximilianfalco merged commit 0ea1cfc into next Feb 12, 2026
17 of 19 checks passed
@maximilianfalco maximilianfalco deleted the falco/fix-rendering-content-in-table-magic-blocks branch February 12, 2026 23:29
rafegoldberg pushed a commit that referenced this pull request Feb 13, 2026
## Version 13.1.2
### 🛠 Fixes & Updates

* **magic blocks:** ensure newline characters processed as hard breaks ([#1329](#1329)) ([bb37d62](bb37d62))
* fix callout magic blocks when rendered directly below a list item ([#1331](#1331)) ([de2b82a](de2b82a))
* fix rendering content in table magic blocks ([#1318](#1318)) ([0ea1cfc](0ea1cfc))
* preserve recipe top level attributes in mdast ([#1324](#1324)) ([98f466b](98f466b))
* **stripComments:** properly pass in the micromark extensions ([#1335](#1335)) ([7ec9d46](7ec9d46))
* **mdxish:** properly terminate html blocks ([#1336](#1336)) ([d221861](d221861))

<!--SKIP CI-->
@rafegoldberg
Copy link
Copy Markdown
Contributor

This PR was released!

🚀 Changes included in v13.1.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants