Skip to content

:: at the start of plain text is escaped too weakly, so it becomes a textDirective after round-trip. #14

Description

@ChristianMurphy

Initial checklist

Affected package

mdast-util-directive 3.1.0, mdast-util-to-markdown 2.1.2, mdast-util-from-markdown 2.0.3, micromark-extension-directive 3.0.2

Steps to reproduce

import {fromMarkdown} from 'mdast-util-from-markdown'
import {toMarkdown} from 'mdast-util-to-markdown'
import {directiveFromMarkdown, directiveToMarkdown} from 'mdast-util-directive'
import {directive} from 'micromark-extension-directive'

const input = '::a}'
const tree1 = fromMarkdown(input, {extensions: [directive()], mdastExtensions: [directiveFromMarkdown()]})
// tree1: root > paragraph > text (value: '::a}')  -- no directive

const text2 = toMarkdown(tree1, {extensions: [directiveToMarkdown()]})
// text2: '\\::a}\n'

const tree2 = fromMarkdown(text2, {extensions: [directive()], mdastExtensions: [directiveFromMarkdown()]})
// tree2: root > paragraph > [text, textDirective(name='a'), text]

The escape rule writes \: before a following :. After decoding the
backslash-escape, the text still contains ::a}. On re-parse, the
directive scanner sees ::a as a text directive start, so a textDirective
node appears in tree₂ that wasn't in tree₁.

Actual behavior

A textDirective node is introduced on re-parse. The escape rule escapes one
colon, but the surviving decoded text still contains ::.

Possible fixes:

  1. escape both colons of a leading :: run, or
  2. use a numeric character reference (:) for one of them, or
  3. widen the escape context to match any :: prefix, not just : + :.

Expected behavior

Text that does not originally parse as a directive should not become one
after round-trip.

Runtime

node v24.13.1

Package manager

No response

Operating system

Ubuntu (WSL2)

Build and bundle tools

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    🤞 phase/openPost is being triaged manually

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions