Skip to content

Bold/Strong text not parsed correctly when ending with punctuation followed by CJK characters #454

Open

Description

@Red-Asuka

Description

Bold syntax (**text**) fails to render when:

  1. The bold text ends with a punctuation mark
  2. Immediately followed by a non-space character

Reproduction

https://stackblitz.com/edit/github-ezruxrr6?file=app%2Fapp.vue

Test Cases

**文字:**后续    <!-- ❌ Fails -->
**文字:** 后续   <!-- ✅ Works (space after **) -->
**文字**:后续    <!-- ✅ Works (punctuation outside **) -->
**English:**next  <!-- ❌ Fails -->
**English:** next <!-- ✅ Works -->

Affected Punctuation

Chinese: : ! ? 。 , ) "
English: : ! ? . , ) "

Workaround

Add a space after the closing **:

**标题:** 内容

Activity

  1. Red-Asuka commented on Jan 8, 2026

    @Red-Asuka
    Author

    I've implemented a workaround in my project using mdc.config.ts. Since remark plugins run after micromark has already parsed the markdown into AST, modifying file.value at that stage has no effect.

    My solution is to post-process the AST and find unprocessed **...** patterns that remain as text nodes (instead of being converted to strong nodes), then manually convert them.

    Here's the workaround:

    // mdc.config.ts
    import type { Parent, Root, Text } from 'mdast'
    import { defineConfig } from '@nuxtjs/mdc/config'
    import { SKIP, visit } from 'unist-util-visit'
    
    const boldPattern = /\*\*([^*]+)\*\*/g
    
    function remarkFixCjkEmphasis() {
      return (tree: Root) => {
        visit(tree, 'text', (node: Text, index, parent: Parent | undefined) => {
          if (!parent || index === undefined)
            return
    
          const text = node.value
    
          // Quick check: skip if no ** at all
          if (!text.includes('**'))
            return
    
          const matches = [...text.matchAll(boldPattern)]
          if (matches.length === 0)
            return
    
          // Build new nodes array
          const newNodes: (Text | { type: 'strong', children: Text[] })[] = []
          let lastIndex = 0
    
          for (const match of matches) {
            const matchStart = match.index!
            const matchEnd = matchStart + match[0].length
            const boldContent = match[1]
    
            // Text before the match
            if (matchStart > lastIndex) {
              newNodes.push({ type: 'text', value: text.slice(lastIndex, matchStart) } as Text)
            }
    
            // The strong node
            newNodes.push({
              type: 'strong',
              children: [{ type: 'text', value: boldContent } as Text],
            })
    
            lastIndex = matchEnd
          }
    
          // Text after the last match
          if (lastIndex < text.length) {
            newNodes.push({ type: 'text', value: text.slice(lastIndex) } as Text)
          }
    
          // Replace the original node with new nodes
          parent.children.splice(index, 1, ...newNodes as never[])
    
          return SKIP
        })
      }
    }
    
    export default defineConfig({
      unified: {
        remark(processor) {
          return processor.use(remarkFixCjkEmphasis)
        },
      },
    })

    This works because:

    • Only text nodes are processed (code blocks become code nodes, so they're safe)
    • Failed emphasis parsing leaves **...** as raw text in text nodes
    • Successfully parsed emphasis becomes strong nodes and won't be affected

    This is a temporary workaround. A proper fix would need to be in micromark's attention parsing logic.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions