mirror of
https://github.com/sbrow/thor.git
synced 2026-10-25 02:01:56 -04:00
fix: Fixed issues with the deflist plugin and TOC builder
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 <dl> HTML block with no
|
||||
// trailing newline, so the blank line the author left after the
|
||||
// list is consumed as the </dl> 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
|
||||
// <h1>) into the document. Re-emit a newline so </dl> 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
|
||||
|
||||
@@ -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 </dl>, CommonMark
|
||||
// treats the fence as raw HTML continuation of the <dl> block and passes its
|
||||
// contents through unescaped (leaking e.g. an example <h1> into the document).
|
||||
@(test)
|
||||
test_deflist_before_code_fence :: proc(t: ^testing.T) {
|
||||
input := "term\n\n: def\n\n```\n<h1>example</h1>\n```"
|
||||
result := convert_deflists(input, context.temp_allocator)
|
||||
testing.expect(t, strings.contains(result, "</dl>\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."
|
||||
|
||||
+17
-1
@@ -1,5 +1,6 @@
|
||||
package markdown
|
||||
|
||||
import "core:log"
|
||||
import "core:strings"
|
||||
|
||||
// generate_toc scans rendered HTML for <h1>-<h6> 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, "<ul>\n")
|
||||
} else if level > current_level {
|
||||
if level - current_level > 1 {
|
||||
log.warnf(
|
||||
"heading level skips from h%d to h%d (\"%s\"); " +
|
||||
"table of contents inserts empty levels to keep valid nesting",
|
||||
current_level,
|
||||
level,
|
||||
text,
|
||||
)
|
||||
}
|
||||
// The first step down nests inside the current, still-open <li>.
|
||||
// Each further skipped level needs its own <li> to host the deeper
|
||||
// <ul>, since a <ul> may only contain <li> — otherwise a jump of
|
||||
// more than one level emits stacked, parentless <ul> tags.
|
||||
strings.write_string(&b, "<ul>\n")
|
||||
current_level += 1
|
||||
for current_level < level {
|
||||
strings.write_string(&b, "<ul>\n")
|
||||
strings.write_string(&b, "<li>\n<ul>\n")
|
||||
current_level += 1
|
||||
}
|
||||
} else if level < current_level {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#+test
|
||||
package markdown
|
||||
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_toc_sequential_nesting :: proc(t: ^testing.T) {
|
||||
html := `<h1 id="a">A</h1><h2 id="b">B</h2><h1 id="c">C</h1>`
|
||||
result := generate_toc(html, context.temp_allocator)
|
||||
testing.expect(t, strings.contains(result, `<a href="#a">A</a>`))
|
||||
testing.expect(t, strings.contains(result, `<a href="#b">B</a>`))
|
||||
testing.expect(t, strings.contains(result, `<a href="#c">C</a>`))
|
||||
// A well-formed list never places a <ul> directly inside a <ul>.
|
||||
testing.expect(t, !strings.contains(result, "<ul>\n<ul>"))
|
||||
}
|
||||
|
||||
// Regression: a heading level that jumps by more than one (h1 -> h3) must not
|
||||
// emit stacked, parentless <ul> tags. Each skipped level is wrapped in an <li>
|
||||
// so the list stays valid.
|
||||
@(test)
|
||||
test_toc_level_jump_stays_valid :: proc(t: ^testing.T) {
|
||||
html := `<h1 id="a">A</h1><h3 id="b">B</h3>`
|
||||
result := generate_toc(html, context.temp_allocator)
|
||||
testing.expect(t, !strings.contains(result, "<ul>\n<ul>"))
|
||||
// The intermediate level is hosted by an <li>.
|
||||
testing.expect(t, strings.contains(result, "<li>\n<ul>"))
|
||||
testing.expect(t, strings.contains(result, `<a href="#b">B</a>`))
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_toc_empty_when_no_headings :: proc(t: ^testing.T) {
|
||||
result := generate_toc("<p>no headings here</p>", context.temp_allocator)
|
||||
testing.expect_value(t, result, "")
|
||||
}
|
||||
Reference in New Issue
Block a user