diff --git a/TODOS.md b/TODOS.md index 792a6d0..1847489 100644 --- a/TODOS.md +++ b/TODOS.md @@ -23,6 +23,8 @@ - [ ] Reconsider `"menus": {}` opt-out: it currently discards all page frontmatter menus (with only a warning). Decide whether empty config should instead just suppress the auto-generated menu while still honoring frontmatter. +- [ ] warn when heading levels are skipped in a toc. +- [ ] extract `[TOC]` from `.md` body (or at least from the start.) ## General diff --git a/markdown/deflists.odin b/markdown/deflists.odin index ac542fe..d6770bc 100644 --- a/markdown/deflists.odin +++ b/markdown/deflists.odin @@ -96,6 +96,19 @@ convert_deflists :: proc(body: string, allocator := context.allocator) -> string if term_pos >= 0 { strings.write_string(&sb, body[emitted_up_to:term_pos]) group_end := write_deflist_group(&sb, body, term_pos) + // write_deflist_group emits a raw
HTML block with no + // trailing newline, so the blank line the author left after the + // list is consumed as the
line's terminator rather than a + // separating blank line. CommonMark (type-6 HTML block) then + // swallows the following block — e.g. a fenced code example — + // as raw, unescaped HTML, leaking its markup (a template's + //

) into the document. Re-emit a newline so stands on + // its own line and the author's blank line still separates the + // block. Only when a blank line actually follows: fused + // non-blank fall-through content is left as-is. + if line, _, ok := line_at(body, group_end); ok && is_blank_line(line) { + strings.write_byte(&sb, '\n') + } emitted_up_to = group_end pos = group_end prev1_kind, prev2_kind = .None, .None diff --git a/markdown/deflists_test.odin b/markdown/deflists_test.odin index c076b4a..1b1fdbf 100644 --- a/markdown/deflists_test.odin +++ b/markdown/deflists_test.odin @@ -90,6 +90,17 @@ test_empty_body :: proc(t: ^testing.T) { testing.expect_value(t, result, "") } +// Regression: a fenced code block following a definition list must stay a +// separate block. Without a blank line after the emitted , CommonMark +// treats the fence as raw HTML continuation of the
block and passes its +// contents through unescaped (leaking e.g. an example

into the document). +@(test) +test_deflist_before_code_fence :: proc(t: ^testing.T) { + input := "term\n\n: def\n\n```\n

example

\n```" + result := convert_deflists(input, context.temp_allocator) + testing.expect(t, strings.contains(result, "
\n\n```")) +} + @(test) test_docs_md_pattern :: proc(t: ^testing.T) { input := "content\n\n: `content` holds your pages.\n\n assets\n : `assets` contains files." diff --git a/markdown/toc.odin b/markdown/toc.odin index 5767406..f57969d 100644 --- a/markdown/toc.odin +++ b/markdown/toc.odin @@ -1,5 +1,6 @@ package markdown +import "core:log" import "core:strings" // generate_toc scans rendered HTML for

-

tags with id attributes and @@ -28,8 +29,23 @@ generate_toc :: proc(html: string, allocator := context.allocator) -> string { current_level = level strings.write_string(&b, "