Skip to content

Fix parsing of as |...| block params in markdown component invocations#2095

Draft
Copilot wants to merge 6 commits intomainfrom
copilot/fix-parsing-issues-as-invocations
Draft

Fix parsing of as |...| block params in markdown component invocations#2095
Copilot wants to merge 6 commits intomainfrom
copilot/fix-parsing-issues-as-invocations

Conversation

Copy link
Contributor

Copilot AI commented Mar 2, 2026

  • Investigate the issue: <Tabs as |Tab|> in markdown content is entity-encoded to &#x3C;Tabs as |Tab|> because:
    1. The | characters in as |Tab| make micromark not recognize <Tabs as |Tab|> as a valid HTML tag
    2. It becomes a text node which gets entity-escaped by rehypeStringify
    3. The existing unescapeComponentsOutsideCode only unescapes tags containing @, not as |...| patterns
  • Fix unescapeComponentsOutsideCode in parse.js to also unescape tags with as |...| block parameter syntax
  • Add a test case for as |...| invocations in markdown
  • Run existing tests to verify no regressions (20/20 pass)
  • Fix Prettier formatting issue in parse.test.ts (CI lint failure)
  • Fix "Closing tag did not match last open tag" error for the simple case: strip spurious <p> wrappers around block-param component opening tags
  • Fix the same error for complex cases (nested components on consecutive lines): use tempered greedy token (?:(?!</p>)[\s\S])* to prevent merging adjacent paragraphs when stripping <p> wrappers
  • Add a more realistic test matching the original issue report (h1 + text + <Tabs as |Tab|> with nested <Tab> components)
Original prompt

This section details on the original issue you should resolve

<issue_title>Parsing issues with as invocations in markdown with live content</issue_title>
<issue_description>Example with tabs

Content:

# Authoring example

once nvp fixes the parsing issue


<Tabs as |Tab|>
  <Tab @label="Ember">

```gjs live
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

export default class HelloWorld extends Component {
  @tracked count = 0;

  increment = () => this.count += 1;

  <template>
    <p>You have clicked the button {{this.count}} times.</p>

    <button type="button" {{on "click" this.increment}}>Click</button>
  </template>
}
```

  </Tab>
  <Tab @label="React">

```jsx react live
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';

function Demo() {
  const notify = () => toast("Wow so easy!");

  return (
    <div>
      <button onClick={notify}>
        Notify!
      </button>
      <ToastContainer />
    </div>
  );
}

export default <Demo />;
```

  </Tab>
</Tabs>

~~~</issue_description>

## Comments on the Issue (you are @copilot in this section)

<comments>
</comments>

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

Copilot AI and others added 2 commits March 2, 2026 20:19
Co-authored-by: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
The `unescapeComponentsOutsideCode` function only unescaped PascalCase
component tags containing `@` (for component args). Tags with Glimmer
block parameter syntax like `<Tabs as |Tab|>` were not unescaped because
`|` chars prevent micromark from recognizing them as HTML blocks.

Add `content.includes('as |')` condition to also unescape these tags.

Co-authored-by: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix parsing issues with as invocations in markdown Fix parsing of as |...| block params in markdown component invocations Mar 2, 2026
@NullVoxPopuli
Copy link
Owner

@copilot you have a lint error in ci

Co-authored-by: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 2, 2026

@copilot you have a lint error in ci

Fixed in be55e75 — ran Prettier on parse.test.ts to resolve the formatting issue.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

Project Preview URL1 Manage
Limber https://copilot-fix-parsing-issues-a.limber-glimdown.pages.dev on Cloudflare
Tutorial https://copilot-fix-parsing-issues-a.limber-glimmer-tutorial.pages.dev on Cloudflare

Logs

Footnotes

  1. if these branch preview links are not working, please check the logs for the commit-based preview link. There is a character limit of 28 for the branch subdomain, as well as some other heuristics, described here for the sake of implementation ease in deploy-preview.yml, that algo has been omitted. The URLs are logged in the wrangler output, but it's hard to get outputs from a matrix job.

@NullVoxPopuli
Copy link
Owner

@copilot when trying this out locally, I get this error:

Closing tag </p> did not match last open tag <Tab> (on line 4): 

|
|  </p>
|

(error occurred in 'an unknown module' @ line 4 : column 20)

(using the code snippet from the issue report)

(requires this diff to test tho)

diff --git a/apps/repl/app/routes/application.ts b/apps/repl/app/routes/application.ts
index 2aa14529..b4830c1f 100644
--- a/apps/repl/app/routes/application.ts
+++ b/apps/repl/app/routes/application.ts
@@ -8,6 +8,7 @@ import { setupTabster } from 'ember-primitives/tabster';
 import { getCompiler, setupCompiler } from 'ember-repl';
 
 import CopyMenu from '#components/copy-menu.gts';
+import { Tabs } from 'nvp.ui/tabs';
 
 import { getHighlighter } from '@nullvoxpopuli/limber-shared';
 
@@ -59,6 +60,7 @@ export default class ApplicationRoute extends Route {
           scope: {
             CopyMenu,
             Shadowed,
+            Tabs,
           },
           rehypePlugins: [
             [

(and installing nvp.ui, ofc)

Co-authored-by: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 2, 2026

@copilot when trying this out locally, I get this error:

Closing tag </p> did not match last open tag <Tab> (on line 4): 

...

Fixed in 69814b2.

The root cause: because | characters make <Tabs as |Tab|> look like inline text to the markdown parser, it gets wrapped in <p>...</p>. So after unescaping you'd get <p><Tabs as |Tab|></p><p>info</p></Tabs> — and Glimmer's parser errors because </p> closes before </Tabs>.

The fix strips those spurious <p> wrappers from block-param component opening tags after unescaping, so the output becomes <Tabs as |Tab|><p>info</p></Tabs> which Glimmer can parse correctly.

`);
});

it('allows component with block params (as |...|) invocation', async () => {
Copy link
Owner

Choose a reason for hiding this comment

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

@copilot add another test with more lines like the original issue report -- something still isn't working (still have the mismatched p tag error)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the more realistic test in d32b42b. The root issue was that when <Tabs as |Tab|> and <Tab @title="First"> appear on consecutive lines (no blank line between them), the markdown parser puts both into the SAME <p> block: <p><Tabs as |Tab|>\n<Tab @title="First"></p>. The previous regex only handled the case where the <p> contained only the opening block-param tag.

Fixed by switching to a tempered greedy token (?:(?!</p>)[\s\S])* that correctly matches content within a single <p> without crossing into adjacent paragraphs. The new test covers the realistic case from the issue report (h1 + text + <Tabs as |Tab|> with nested <Tab> components).

…separator

Co-authored-by: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
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.

Parsing issues with as invocations in markdown with live content

2 participants