fix(parser): accept spaceless headings (==Heading==) like vimwiki (#7)
The heading lexer required a space after the opening `=`s, so `==Heading==` fell through to a literal `<p>==Heading==</p>` instead of an `<h2>`. vimwiki accepts the spaceless form, so a migrated page whose top heading was `==TODO==` (or `====Progress====`) rendered with no heading at all. Drop the space-after-marker requirement; the existing title trim already handles one optional space per side, so both `== H ==` and `==H==` work. Marker-only (`======`) and unbalanced (`==a==b`, `==> x`) lines still stay paragraphs (title-present + matching trailing-`=` checks). Regression tests added at the lexer and renderer levels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -526,10 +526,9 @@ impl<'src> LexState<'src> {
|
|||||||
if level == 0 {
|
if level == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Must be followed by at least one space.
|
// vimwiki accepts both spaced (`== H ==`) and spaceless (`==H==`)
|
||||||
if leading_ws + level >= bytes.len() || bytes[leading_ws + level] != b' ' {
|
// headings, so we don't require a space after the opening `=`s. The
|
||||||
return false;
|
// title is still trimmed of one optional space per side below.
|
||||||
}
|
|
||||||
|
|
||||||
// Trailing `=`s of the same level.
|
// Trailing `=`s of the same level.
|
||||||
let trimmed_end = line.trim_end_matches([' ', '\t']);
|
let trimmed_end = line.trim_end_matches([' ', '\t']);
|
||||||
|
|||||||
@@ -77,6 +77,27 @@ fn centered_heading_gets_centered_class() {
|
|||||||
assert!(out.contains("<h1 class=\"centered\" id=\"T\">T</h1>"));
|
assert!(out.contains("<h1 class=\"centered\" id=\"T\">T</h1>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spaceless_heading_renders_as_heading() {
|
||||||
|
// `==Heading==` (no surrounding spaces) is a heading, like vimwiki — not a
|
||||||
|
// literal `<p>==Heading==</p>`.
|
||||||
|
let out = render("==Plain==\n");
|
||||||
|
assert!(
|
||||||
|
out.contains(
|
||||||
|
"<div id=\"Plain\"><h2 id=\"Plain\" class=\"header\">\
|
||||||
|
<a href=\"#Plain\">Plain</a></h2></div>"
|
||||||
|
),
|
||||||
|
"got: {out}"
|
||||||
|
);
|
||||||
|
// The spaceless + keyword combo from the report: a header, not a badge.
|
||||||
|
let out2 = render("==TODO==\n");
|
||||||
|
assert!(
|
||||||
|
out2.contains("<h2 id=\"TODO\" class=\"header\">"),
|
||||||
|
"got: {out2}"
|
||||||
|
);
|
||||||
|
assert!(!out2.contains("<p>"), "not a paragraph: {out2}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn heading_keyword_renders_as_plain_text_not_badge() {
|
fn heading_keyword_renders_as_plain_text_not_badge() {
|
||||||
// `== TODO ==` is a section title, not an inline keyword: it must render as
|
// `== TODO ==` is a section title, not an inline keyword: it must render as
|
||||||
|
|||||||
@@ -85,6 +85,32 @@ fn centered_heading_is_marked_centered() {
|
|||||||
assert!(matches!(lexed[2], HeadingClose));
|
assert!(matches!(lexed[2], HeadingClose));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spaceless_heading_is_a_heading() {
|
||||||
|
// vimwiki accepts `==Heading==` with no spaces around the text.
|
||||||
|
let lexed = lex("==Plain==\n");
|
||||||
|
assert_eq!(
|
||||||
|
lexed,
|
||||||
|
vec![
|
||||||
|
HeadingOpen {
|
||||||
|
level: 2,
|
||||||
|
centered: false
|
||||||
|
},
|
||||||
|
text("Plain"),
|
||||||
|
HeadingClose,
|
||||||
|
Newline,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn marker_only_or_unbalanced_lines_are_not_headings() {
|
||||||
|
// No title between markers, and trailing `=` not at line end → plain text.
|
||||||
|
assert!(!matches!(lex("======\n")[0], HeadingOpen { .. }));
|
||||||
|
assert!(!matches!(lex("==> arrow\n")[0], HeadingOpen { .. }));
|
||||||
|
assert!(!matches!(lex("==a==b\n")[0], HeadingOpen { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn heading_with_inline_bold() {
|
fn heading_with_inline_bold() {
|
||||||
let lexed = lex("== Hello *world* ==\n");
|
let lexed = lex("== Hello *world* ==\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user