Skip to content

Model comments as their own node type rather than raw HTML #36

Description

Comments are how a Markdown document carries instructions that are not content. Linter directives (<!-- markdownlint-disable MD041 -->), formatter pragmas (<!-- prettier-ignore -->), generated-region markers (<!-- BEGIN GENERATED --><!-- END GENERATED -->), and editorial notes to the next author all travel as comments, because Markdown has no other way to say something to a tool rather than to a reader.

Request

Current experience

Markdown has no comment syntax of its own. A comment is HTML, so under the object model a comment arrives as raw HTML — a block-level one as an HTML block, an inline one as raw HTML — carrying its delimiters as part of an opaque string. Nothing distinguishes it from a <div> or a <details>.

Everything a caller wants to do with a comment therefore starts with string matching. Finding the generated region means scanning raw HTML nodes for text beginning <!-- and ending -->, then slicing the delimiters off to read what is inside. That is the same pattern matching against raw text the object model exists to remove, moved one layer in and made less obvious.

The consequences show up in the capabilities built on the model:

  • Section-level merging (#32) is motivated by replacing generated content while leaving hand-written content alone. Where the boundary is a marker comment pair rather than a heading, the boundary is unfindable.
  • Normalization (#24) can move a linter directive away from the line it governs, silently changing which rule is suppressed where. This repository's own pull request template opens with <!-- markdownlint-disable MD041 -->, so the failure is not hypothetical.
  • Validation (#22) cannot report on comments — enforce a required marker pair, or flag a stray directive — without re-implementing comment recognition.

Desired experience

A comment is a thing the model knows about. It can be found, read, written, and removed without touching delimiters or matching text.

$doc = Get-Content -Raw 'README.md' | ConvertFrom-Markdown

# Comments are addressable, like any other construct.
$doc.Descendants('Comment') | ForEach-Object { $_.Text }

# The content is available without stripping delimiters.
$doc.Descendants('Comment') | Where-Object Text -Match 'BEGIN GENERATED'

# Publishing a document can strip editorial notes and keep everything else.
$doc.Descendants('Comment') | ForEach-Object { $_.Remove() }

$doc | ConvertTo-Markdown | Set-Content 'README.md'

Acceptance criteria

  • A comment occupying its own block is a comment in the model, not opaque raw HTML.
  • A comment inside a paragraph, heading, or other inline content is a comment in the model.
  • A comment exposes its inner text without the <!-- and --> delimiters, and exposes whether it was terminated in the source.
  • Text that merely looks like a comment — inside a code span, a fenced code block, or an indented code block — is not treated as one.
  • A comment is preserved through a parse and render cycle, in its original position, with its original delimiters and spacing.
  • A block whose comment is followed by other content on the same line keeps that content, since the specification makes the whole line part of one block.
  • Removing every comment from a document leaves the rest of the document unchanged.
  • Constructing a comment directly, without parsing, produces valid output.

Out of scope

  • Interpreting what a comment means. Recognizing a linter directive, a generated-region marker, or a pragma is the job of whatever consumes the comment, not of the model.
  • Other raw HTML. Tags, processing instructions, declarations, and CDATA sections stay as they are specified in #8.
  • Any comment convention outside CommonMark, including the empty-link-reference trick some authors use as a comment.

Technical decisions

This has to be settled in 1.3, not added later. Adding a node type is normally additive and non-breaking, but this one is not: it changes what the parser returns for input that already parses. A caller matching Type -eq 'HtmlBlock' in 1.3 would silently stop matching comments when a comment type arrived in 1.4. That is exactly the rule #8 sets for itself — anything that changes the shape of what ConvertFrom-Markdown returns is a 1.3 decision. Nothing has shipped yet; the latest release is v1.2.5.

Open: one type or two. Comments occur at both block and inline level, and PowerShell classes have single inheritance, so a single type cannot derive from both MarkdownBlock and MarkdownInline. Three options:

Option Trade-off
Two types — a block comment and an inline comment Mirrors the MarkdownHtmlBlock / MarkdownRawHtml split already in #8, so $_ -is [MarkdownBlock] keeps working. Two names for one concept, and Descendants('Comment') needs both to report the same Type.
One type deriving from the node base One name, but it sits outside the block/inline dichotomy that the filtering idiom depends on.
No new type — an IsComment flag on the existing HTML nodes Smallest surface. Leaves the content as raw text with delimiters, which is most of the problem unsolved.

The existing split is the strongest precedent, but this is the decision that shapes the rest and it is not settled here.

A block-level comment is only a comment when the block is exactly a comment. CommonMark ends an HTML block at the first line containing -->, and everything after the terminator on that line belongs to the same block — example 177 shows <!-- foo -->*bar* producing one HTML block in which *bar* is not emphasized. So a block that is a comment plus trailing content stays an HTML block; promoting it would silently drop the trailing text.

Unterminated comments run to the end of their container. The end condition is a line containing -->, or the last line of the document, or the last line of the container block containing the comment (§4.6). A stray <!-- at document level therefore swallows the rest of the file; inside a block quote or list item it swallows the rest of that container and no more. Both are real authoring accidents and ones a validator should be able to report. The model records whether the comment was terminated rather than pretending it was.

The degenerate forms are comments. §6.6 admits <!--> and <!---> as HTML comments alongside the usual form. They carry no text and must round-trip as written.

Delimiters and inner spacing are preserved. Following the round-trip contract in #8, the node keeps enough of the source form to re-render it as written — <!-- x --> does not become <!--x-->. Inner text is exposed separately for reading.

Not a dialect concern. Comments are core CommonMark at both block and inline level, so this belongs with the base grammar and not with #30.


Implementation plan

Specification

  • Record the comment model, the block-exactness rule, and the unterminated case in docs/markdown-object-model/spec.md
  • Record the one-type-or-two decision and its rationale in docs/markdown-object-model/design.md

Model

  • Add the comment node type or types, with inner text and a terminated indicator
  • Give each a parameterless constructor and one taking the comment text

Parser

  • Recognize a block that is exactly a comment, and leave any other HTML block as it is
  • Recognize comments in inline content, including the <!--> and <!---> forms
  • Record unterminated comments as such, running to the end of their containing block or the document

Renderer

  • Re-render a comment with its original delimiters and inner spacing

Tests

  • A comment as its own block, and a comment inside a paragraph and a heading
  • A comment followed by content on the same line, which stays an HTML block
  • An unterminated comment at document level and inside a container block, and both degenerate forms
  • Comment-looking text inside a code span, a fenced block, and an indented block
  • Removing every comment leaves the remaining document byte-identical
  • Round-trip stability across the commonmark-spec examples containing comments

Documentation

  • Document the comment type in the README alongside the other node types

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions