Require space between hash/content in ATX heading (#1140)

While writing some Markdown documentation for Rails, I came across an
interesting case where trying to link to an instance method at the start
of a line would instead parse as an H1 heading:

```markdown
#response_body=
```

Expected:

```html
<a href=""><code>#response_body=</code></a>
```

Actual:

```html
<h1>response_body=</h1>
```

According to the CommonMark spec:

> At least one space or tab is required between the # characters and the
> heading’s contents, unless the heading is empty. Note that many
> implementations currently do not require the space. However, the space
> was required by the original ATX implementation, and it helps prevent
> things like the following from being parsed as headings:
>
> Example 64

So while some implementations do not follow this requirement, I believe
RDoc should because it makes it easy to write text similar to Example 64
(which was used in the new test) and it also enables automatically
linking to instance methods at the start of a line.
This commit is contained in:
Hartley McGuire 2024-07-17 21:39:08 +00:00 committed by Hiroshi SHIBATA
parent 239d54dfbc
commit d0c17cbd09
No known key found for this signature in database
GPG Key ID: F9CF13417264FAC2
2 changed files with 36 additions and 13 deletions

View File

@ -1158,7 +1158,7 @@ class RDoc::Markdown
return _tmp
end
# AtxHeading = AtxStart:s @Sp AtxInline+:a (@Sp /#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }
# AtxHeading = AtxStart:s @Spacechar+ AtxInline+:a (@Sp /#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }
def _AtxHeading
_save = self.pos
@ -1169,12 +1169,22 @@ class RDoc::Markdown
self.pos = _save
break
end
_tmp = _Sp()
_save1 = self.pos
_tmp = _Spacechar()
if _tmp
while true
_tmp = _Spacechar()
break unless _tmp
end
_tmp = true
else
self.pos = _save1
end
unless _tmp
self.pos = _save
break
end
_save1 = self.pos
_save2 = self.pos
_ary = []
_tmp = apply(:_AtxInline)
if _tmp
@ -1187,37 +1197,37 @@ class RDoc::Markdown
_tmp = true
@result = _ary
else
self.pos = _save1
self.pos = _save2
end
a = @result
unless _tmp
self.pos = _save
break
end
_save2 = self.pos
_save3 = self.pos
_save4 = self.pos
while true # sequence
_tmp = _Sp()
unless _tmp
self.pos = _save3
self.pos = _save4
break
end
_tmp = scan(/\G(?-mix:#*)/)
unless _tmp
self.pos = _save3
self.pos = _save4
break
end
_tmp = _Sp()
unless _tmp
self.pos = _save3
self.pos = _save4
end
break
end # end sequence
unless _tmp
_tmp = true
self.pos = _save2
self.pos = _save3
end
unless _tmp
self.pos = _save
@ -16539,7 +16549,7 @@ class RDoc::Markdown
Rules[:_Plain] = rule_info("Plain", "Inlines:a { paragraph a }")
Rules[:_AtxInline] = rule_info("AtxInline", "!@Newline !(@Sp /\#*/ @Sp @Newline) Inline")
Rules[:_AtxStart] = rule_info("AtxStart", "< /\\\#{1,6}/ > { text.length }")
Rules[:_AtxHeading] = rule_info("AtxHeading", "AtxStart:s @Sp AtxInline+:a (@Sp /\#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }")
Rules[:_AtxHeading] = rule_info("AtxHeading", "AtxStart:s @Spacechar+ AtxInline+:a (@Sp /\#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }")
Rules[:_SetextHeading] = rule_info("SetextHeading", "(SetextHeading1 | SetextHeading2)")
Rules[:_SetextBottom1] = rule_info("SetextBottom1", "/={1,}/ @Newline")
Rules[:_SetextBottom2] = rule_info("SetextBottom2", "/-{1,}/ @Newline")

View File

@ -414,10 +414,23 @@ two
end
def test_parse_heading_atx
doc = parse "# heading\n"
# CommonMark Example 62
(1..6).each do |level|
doc = parse "#{"#" * level} heading\n"
expected = @RM::Document.new(
@RM::Heading.new(level, "heading"))
assert_equal expected, doc
end
# CommonMark Example 64
doc = parse "#5 bolt\n\n#hashtag\n"
expected = @RM::Document.new(
@RM::Heading.new(1, "heading"))
para("#5 bolt"),
para("#hashtag"),
)
assert_equal expected, doc
end