Repository: graphql-markdown/graphql-markdown (ts)

Issue:
Bug: exppected a closing tag for `<b>` before the end of `emphasis`
**Describe the bug**

There are some Inputs for which `graphql-markdown` generates incorrect MDX like this:

```markdown
#### [<code style={{ fontWeight: 'normal' }}>Batcher*filter.<b>batches*</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>Batch_filter</b></code>](/docs/api/06-flow/02-the-graph/inputs/batch-filter.mdx) <Badge class="badge badge--secondary " text="input"/>
```

This is not MDX-compliant:

```text
Expected a closing tag for `<b>` (133:61-133:64) before the end of `emphasis`MDX[mdast-util-mdx-jsx:end-tag-mismatch](https://mdxjs.com/docs/troubleshooting-mdx/)
```

**To Reproduce**

Generate the GraphQL Markdown files using the config file. Here's the full source code of the generated MDX file:

<details><summary>Toggle to see MDX file</summary>
<p>


````mdx
---
id: batcher-filter
title: Batcher_filter
---

<head>
<meta content="noindex" name="robots" />
<meta charset="utf-8" />
</head>

export const Bullet = () => <><span style={{ fontWeight: 'normal', fontSize: '.5em', color: 'var(--ifm-color-secondary-darkest)' }}>&nbsp;●&nbsp;</span></>

export const SpecifiedBy = (props) => <>Specification<a className="link" style={{ fontSize:'1.5em', paddingLeft:'4px' }} target="\_blank" href={props.url} title={'Specified by ' + props.url}>⎘</a></>

export const Badge = (props) => <><span className={props.class}>{props.text}</span></>

import { useState } from 'react';

export const Details = ({ dataOpen, dataClose, children, startOpen = false }) => {
const [open, setOpen] = useState(startOpen);
return (
<details {...(open ? { open: true } : {})} className="details" style={{ border:'none', boxShadow:'none', background:'var(--ifm-background-color)' }}>
<summary
onClick={(e) => {
e.preventDefault();
setOpen((open) => !open);
}}
style={{ listStyle:'none' }} >
{open ? dataOpen : dataClose}
</summary>
{open && children}
</details>
);
};

No description

```graphql
input Batcher_filter {
  id: String
  id_not: String
  id_gt: String
  id_lt: String
  id_gte: String
  id_lte: String
  id_in: [String!]
  id_not_in: [String!]
  id_contains: String
  id_contains_nocase: String
  id_not_contains: String
  id_not_contains_nocase: String
  id_starts_with: String
  id_starts_with_nocase: String
  id_not_starts_with: String
  id_not_starts_with_nocase: String
  id_ends_with: String
  id_ends_with_nocase: String
  id_not_ends_with: String
  id_not_ends_with_nocase: String
  batchCounter: BigInt
  batchCounter_not: BigInt
  batchCounter_gt: BigInt
  batchCounter_lt: BigInt
  batchCounter_gte: BigInt
  batchCounter_lte: BigInt
  batchCounter_in: [BigInt!]
  batchCounter_not_in: [BigInt!]
  batches_: Batch_filter
  _change_block: BlockChangedFilter
  and: [Batcher_filter]
  or: [Batcher_filter]
}
```

### Fields

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_not</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_gt</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_lt</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_gte</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_lte</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_in</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>[String!]</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="list"/> <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_not_in</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>[String!]</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="list"/> <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_contains</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_contains_nocase</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_not_contains</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_not_contains_nocase</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_starts_with</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_starts_with_nocase</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_not_starts_with</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_not_starts_with_nocase</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_ends_with</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_ends_with_nocase</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_not_ends_with</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>id_not_ends_with_nocase</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>String</b></code>](/docs/api/06-flow/02-the-graph/scalars/string.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>batchCounter</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>BigInt</b></code>](/docs/api/06-flow/02-the-graph/scalars/big-int.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>batchCounter_not</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>BigInt</b></code>](/docs/api/06-flow/02-the-graph/scalars/big-int.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>batchCounter_gt</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>BigInt</b></code>](/docs/api/06-flow/02-the-graph/scalars/big-int.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>batchCounter_lt</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>BigInt</b></code>](/docs/api/06-flow/02-the-graph/scalars/big-int.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>batchCounter_gte</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>BigInt</b></code>](/docs/api/06-flow/02-the-graph/scalars/big-int.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>batchCounter_lte</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>BigInt</b></code>](/docs/api/06-flow/02-the-graph/scalars/big-int.mdx) <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>batchCounter_in</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>[BigInt!]</b></code>](/docs/api/06-flow/02-the-graph/scalars/big-int.mdx) <Badge class="badge badge--secondary " text="list"/> <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>batchCounter_not_in</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>[BigInt!]</b></code>](/docs/api/06-flow/02-the-graph/scalars/big-int.mdx) <Badge class="badge badge--secondary " text="list"/> <Badge class="badge badge--secondary " text="scalar"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher*filter.<b>batches*</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>Batch_filter</b></code>](/docs/api/06-flow/02-the-graph/inputs/batch-filter.mdx) <Badge class="badge badge--secondary " text="input"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>\_change_block</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>BlockChangedFilter</b></code>](/docs/api/06-flow/02-the-graph/inputs/block-changed-filter.mdx) <Badge class="badge badge--secondary " text="input"/>

Filter for the block changed event.

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>and</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>[Batcher_filter]</b></code>](/docs/api/06-flow/02-the-graph/inputs/batcher-filter.mdx) <Badge class="badge badge--secondary " text="list"/> <Badge class="badge badge--secondary " text="input"/>

#### [<code style={{ fontWeight: 'normal' }}>Batcher_filter.<b>or</b></code>](#)<Bullet />[<code style={{ fontWeight: 'normal' }}><b>[Batcher_filter]</b></code>](/docs/api/06-flow/02-the-graph/inputs/batcher-filter.mdx) <Badge class="badge badge--secondary " text="list"/> <Badge class="badge badge--secondary " text="input"/>

### Member Of

[`Batch_filter`](/docs/api/06-flow/02-the-graph/inputs/batch-filter.mdx) <Badge class="badge badge--secondary badge--relation" text="input"/><Bullet />[`Batcher_filter`](/docs/api/06-flow/02-the-graph/inputs/batcher-filter.mdx) <Badge class="badge badge--secondary badge--relation" text="input"/><Bullet />[`batchers`](/docs/api/06-flow/02-the-graph/queries/batchers.mdx) <Badge class="badge badge--secondary badge--relation" text="query"/>

````

</p>
</details> 


- **GraphQL schema**

https://api.studio.thegraph.com/query/112500/sablier-airdrops-ethereum/version/latest

- **Docusaurus configuration**

```typescript
[
  "@graphql-markdown/docusaurus",
  {
    baseURL: "./docs/api/06-flow/02-the-graph",
    loaders: {
      UrlLoader: {
        module: "@graphql-tools/url-loader",
      },
    } as LoaderOption,
    metatags: [{ content: "noindex", name: "robots" }, { charset: "utf-8" }],
    pretty: true,
    printTypeOptions: {
      hierarchy: "entity",
    },
    rootPath: ".",
    schema: "https://api.studio.thegraph.com/query/112500/sablier-flow-experimental/version/latest",
  } satisfies ConfigOptions,
];
```

**Expected behavior**

Instead of:

```
Stream*filter.<b>actions*
```

It should be:

```
Stream_filter.<b>actions*
```

**Additional context**

Docusaurus v3.8.1 and:

```json
"@graphql-markdown/docusaurus": "^1.29.0",
"@graphql-markdown/types": "^1.7.0",
"@graphql-tools/url-loader": "^8.0.31",
```


Pull request:
:bug:  fix mdx compilation error when underscore used with entity name
# Description

Add missing support for escaping special MDX characters when rendering links.

This should fix #2214 

# Checklist

- [x] My changes follow the [contributing guidelines](https://github.com/graphql-markdown/graphql-markdown/blob/main/CONTRIBUTING.md) of this project.
- [x] I have performed a self-review of my code.
- [x] I have made corresponding changes to the documentation.
- [x] I have added tests that prove my fix is effective or that my changes work.
- [x] New and existing unit tests pass locally with my changes.


Patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 6ccd8934..b3923bf2 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -56,5 +56,5 @@
   // "forwardPorts": [],
 
   // Use 'postCreateCommand' to run commands after the container is created.
-  "postCreateCommand": "/bin/sh -c ./.devcontainer/postCreateCommand.sh",
+  // "postCreateCommand": "/bin/sh -c ./.devcontainer/postCreateCommand.sh",
 }
diff --git a/packages/docusaurus/src/mdx/index.ts b/packages/docusaurus/src/mdx/index.ts
index ba91b272..4a0ffb19 100644
--- a/packages/docusaurus/src/mdx/index.ts
+++ b/packages/docusaurus/src/mdx/index.ts
@@ -16,6 +16,7 @@ import type {
   MetaOptions,
   TypeLink,
 } from "@graphql-markdown/types";
+import { escapeMDX } from "@graphql-markdown/utils";
 
 const MARKDOWN_EOL = "\n" as const;
 const MARKDOWN_EOP = `${MARKDOWN_EOL.repeat(2)}` as const;
@@ -90,7 +91,7 @@ export const formatMDXNameEntity = (
   parentType?: Maybe<string>,
 ): MDXString => {
   const parentName = parentType ? `${parentType}.` : "";
-  return `<code style={{ fontWeight: 'normal' }}>${parentName}<b>${name}</b></code>` as MDXString;
+  return `<code style={{ fontWeight: 'normal' }}>${escapeMDX(parentName)}<b>${escapeMDX(name)}</b></code>` as MDXString;
 };
 
 export const formatMDXLink = ({ text, url }: TypeLink): TypeLink => {
diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts
index f8756f3d..35fc82ea 100644
--- a/packages/utils/src/string.ts
+++ b/packages/utils/src/string.ts
@@ -176,7 +176,7 @@ export const toHTMLUnicode = (char: Maybe<string>): string => {
  */
 export const escapeMDX = (str: unknown): string => {
   return `${String(str)}`.replace(
-    /(?<!`)([{<>}])(?=(?:[^`]*`[^`]*`)*[^`]*$)/g,
+    /(?<!`)([{<_*>}])(?=(?:[^`]*`[^`]*`)*[^`]*$)/g,
     toHTMLUnicode,
   );
 };


Test patch:
diff --git a/packages/utils/tests/unit/string.test.ts b/packages/utils/tests/unit/string.test.ts
index 34e490c6..507fec80 100644
--- a/packages/utils/tests/unit/string.test.ts
+++ b/packages/utils/tests/unit/string.test.ts
@@ -133,16 +133,18 @@ describe("string", () => {
     test("returns string with HTML &#x0000; format for MDX special characters", () => {
       expect.hasAssertions();
 
-      expect(escapeMDX("{MDX} <special> characters")).toBe(
-        "&#x007B;MDX&#x007D; &#x003C;special&#x003E; characters",
+      expect(
+        escapeMDX("{MDX} <special> characters and formatting _test_"),
+      ).toBe(
+        "&#x007B;MDX&#x007D; &#x003C;special&#x003E; characters and formatting &#x005F;test&#x005F;",
       );
     });
 
     test("does not transform MDX special characters enclosed as code", () => {
       expect.hasAssertions();
 
-      expect(escapeMDX(">`{MDX}` `<special>` characters")).toBe(
-        "&#x003E;`{MDX}` `<special>` characters",
+      expect(escapeMDX(">`{MDX}` `<special>` characters `_test_`")).toBe(
+        "&#x003E;`{MDX}` `<special>` characters `_test_`",
       );
     });