Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ xhtmlmd is largely implemented using AI, except for the tests. The tests are lar
- HTML-in-Markdown: block containers opened with `markdown="1"`; the control attribute is stripped, indented code blocks are disabled inside the container, and fenced code is the code-block syntax there.
- Math: four modes: `brackets` for `\(...\)`, `\[...\]`, and `$$...$$`, `dollars` for those plus `$...$` using Pandoc's non-space/digit dollar rules, `on` to preserve `\(...\)` and `\[...\]` delimiters for client-side renderers such as KaTeX, and `off`. Brackets mode is the default.
- Attributes and inline spans: Pandoc/kramdown-style `{#id .class key="value"}`, block IALs `{: ...}`, span IALs, ALDs such as `{:note: #id .class}` with references, superscript `^x^`, subscript `~x~`, and highlight `==x==`.
- Pandoc-style escapes: `\ ` is a non-breaking space, and a trailing `\` is a hard break even at the end of a block, so a paragraph of just `\` renders as a `<br />` spacer.
- Definition lists: PHP Markdown Extra/Pandoc-style `Term` followed by `: definition` or `~ definition`.
- Footnotes: `[^id]` references to defined `[^id]:` definitions with indented continuation blocks.
- Abbreviations: `*[HTML]: Hyper Text Markup Language` definitions render matching text as `<abbr>`.
Expand Down
1 change: 1 addition & 0 deletions docs/DIALECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ When Markdown extensions disagree, this crate chooses the behavior closest to Pa
- Definition lists follow PHP Markdown Extra/Pandoc: one-line terms with one or more `:` or `~` definitions.
- Footnotes follow Pandoc/kramdown label rules and render as XHTML endnotes with backlinks. The endnotes `<section>` has no leading `<hr>` (unlike cmark-gfm): separators are a styling concern, so add one with CSS if wanted.
- Inline `~~x~~` renders as strikethrough. Inline `~x~` renders as subscript, using the same no-whitespace rule as superscript `^x^`.
- Backslash escapes follow Pandoc's `escaped_line_breaks`/escaped-space rules: `\ ` (backslash before a space) is a non-breaking space, and a backslash at the end of a line is a hard break even at the end of a block, so a paragraph of just `\` renders as `<p><br /></p>`. CommonMark instead keeps both as literal text. An escaped backslash (`\\`) never starts a break.
- `<tag markdown="1">` parses block Markdown inside the balanced tag. `markdown="span"` parses inline content into a single paragraph child.
- Raw HTML passes through unbalanced, per CommonMark. The opt-in `balance` option closes unclosed elements at the fragment end, drops stray closes, and self-closes void tags, without HTML5 implied-end-tag rules.
- Math defaults to `MathMode::Brackets`, which recognizes `\(...\)`, `\[...\]`, and `$$...$$`. `MathMode::Dollars` also recognizes `$...$` with Pandoc's guard against currency-like spans. `MathMode::On` preserves backslashes before `[]()` so client-side renderers such as KaTeX can see TeX delimiters. `MathMode::Off` treats TeX delimiters as ordinary Markdown text.
19 changes: 17 additions & 2 deletions src/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ fn parse_inner(src: &str, ctx: &InlineContext<'_>, depth: usize) -> Vec<Inline>
};
let mut failed = FailedScans::default();
let mut i = 0;
let mut last_escape_end = usize::MAX;
while i < src.len() {
if starts(src, i, "\\[")
&& matches!(ctx.options.math, MathMode::Brackets | MathMode::Dollars)
Expand Down Expand Up @@ -210,24 +211,33 @@ fn parse_inner(src: &str, ctx: &InlineContext<'_>, depth: usize) -> Vec<Inline>
if starts(src, i, "\\") {
if i + 1 < src.len() {
let next = next_char(src, i + 1);
if next == ' ' {
scanner.text.push('\u{a0}');
i += 2;
last_escape_end = i;
continue;
}
if is_escapable(next) {
if ctx.options.math == MathMode::On && matches!(next, '[' | ']' | '(' | ')') {
scanner.text.push('\\');
scanner.text.push(next);
i += 1 + next.len_utf8();
last_escape_end = i;
continue;
}
scanner
.text
.push(if next == '&' { ESCAPED_AMP } else { next });
i += 1 + next.len_utf8();
last_escape_end = i;
continue;
}
}
}
if ch == '\n' {
if scanner.text.ends_with(" ") || scanner.text.ends_with('\\') {
if scanner.text.ends_with('\\') {
let backslash_break = scanner.text.ends_with('\\') && last_escape_end != i;
if backslash_break || scanner.text.ends_with(" ") {
if backslash_break {
scanner.text.pop();
} else {
while scanner.text.ends_with(' ') {
Expand All @@ -246,6 +256,11 @@ fn parse_inner(src: &str, ctx: &InlineContext<'_>, depth: usize) -> Vec<Inline>
i += ch.len_utf8();
}
}
if scanner.text.ends_with('\\') && last_escape_end != src.len() {
scanner.text.pop();
scanner.flush_text();
scanner.push_inline(Inline::HardBreak);
}
scanner.flush_text();
process_delimiters(&mut nodes, &mut delimiters);
nodes_to_inlines(&nodes)
Expand Down
8 changes: 4 additions & 4 deletions tests/source/cmark-gfm/spec.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1155,7 +1155,7 @@ Foo

Nor does a backslash at the end:

```````````````````````````````` example
```````````````````````````````` example disabled
Foo\
----
.
Expand Down Expand Up @@ -5786,7 +5786,7 @@ Any ASCII punctuation character may be backslash-escaped:
Backslashes before other characters are treated as literal
backslashes:

```````````````````````````````` example
```````````````````````````````` example disabled
\→\A\a\ \3\φ\«
.
<p>\→\A\a\ \3\φ\«</p>
Expand Down Expand Up @@ -9770,7 +9770,7 @@ Hard line breaks are for separating inline content within a block.
Neither syntax for hard line breaks works at the end of a paragraph or
other block element:

```````````````````````````````` example
```````````````````````````````` example disabled
foo\
.
<p>foo\</p>
Expand All @@ -9784,7 +9784,7 @@ foo
````````````````````````````````


```````````````````````````````` example
```````````````````````````````` example disabled
### foo\
.
<h3>foo\</h3>
Expand Down
15 changes: 15 additions & 0 deletions tests/test_focused.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ def test_balance_ignores_rawtext_and_voids():
assert "</div>" not in html
assert "<br />" in html

def test_escaped_space_is_nbsp():
assert to_xhtml("a\\ b\n") == "<p>a\u00a0b</p>\n"
assert to_xhtml("# foo\\ bar\n") == "<h1>foo\u00a0bar</h1>\n"

def test_trailing_backslash_is_hard_break_at_block_end():
assert to_xhtml("text\\\n\nnext\n") == "<p>text<br />\n</p>\n<p>next</p>\n"
assert to_xhtml("\\\n") == "<p><br />\n</p>\n"
assert to_xhtml("\\ \n") == "<p><br />\n</p>\n"
assert to_xhtml("# foo\\\n") == "<h1>foo<br />\n</h1>\n"
assert to_xhtml("Foo\\\n----\n") == "<h2>Foo<br />\n</h2>\n"

def test_escaped_backslash_is_never_a_break():
assert to_xhtml("a\\\\\nb\n") == "<p>a\\\nb</p>\n"
assert to_xhtml("a\\\\\n") == "<p>a\\</p>\n"

def test_long_nonascii_words_near_autolink_cap_do_not_error():
for boundary in ("(", "a: ", "x '"):
for count in (126, 127, 128, 129, 130, 200):
Expand Down
Loading