mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
164 lines
3.5 KiB
Odin
164 lines
3.5 KiB
Odin
package markdown
|
|
|
|
import "core:strings"
|
|
|
|
// generate_toc scans rendered HTML for <h1>-<h6> tags with id attributes and
|
|
// builds a nested <ul> table of contents. Returns "" if no headings with IDs
|
|
// are found. Must be called after inject_heading_ids.
|
|
generate_toc :: proc(html: string, allocator := context.allocator) -> string {
|
|
b: strings.Builder
|
|
strings.builder_init(&b, allocator)
|
|
|
|
current_level := 0
|
|
min_level := 7
|
|
pos := 0
|
|
|
|
for {
|
|
idx, level, id, text, next_pos := next_heading(html, pos)
|
|
if level == 0 {
|
|
break
|
|
}
|
|
pos = next_pos
|
|
|
|
if level < min_level {
|
|
min_level = level
|
|
}
|
|
|
|
if current_level == 0 {
|
|
current_level = level
|
|
strings.write_string(&b, "<ul>\n")
|
|
} else if level > current_level {
|
|
for current_level < level {
|
|
strings.write_string(&b, "<ul>\n")
|
|
current_level += 1
|
|
}
|
|
} else if level < current_level {
|
|
strings.write_string(&b, "</li>\n")
|
|
for current_level > level {
|
|
strings.write_string(&b, "</ul>\n</li>\n")
|
|
current_level -= 1
|
|
}
|
|
} else {
|
|
strings.write_string(&b, "</li>\n")
|
|
}
|
|
|
|
strings.write_string(&b, `<li><a href="#`)
|
|
strings.write_string(&b, id)
|
|
strings.write_string(&b, `">`)
|
|
strings.write_string(&b, text)
|
|
strings.write_string(&b, `</a>`)
|
|
}
|
|
|
|
if current_level == 0 {
|
|
return ""
|
|
}
|
|
|
|
strings.write_string(&b, "</li>\n")
|
|
for current_level > min_level {
|
|
strings.write_string(&b, "</ul>\n</li>\n")
|
|
current_level -= 1
|
|
}
|
|
strings.write_string(&b, "</ul>\n")
|
|
|
|
return strings.to_string(b)
|
|
}
|
|
|
|
// next_heading finds the next <hN> tag with an id attribute starting from pos.
|
|
// Returns level=0 if none found.
|
|
next_heading :: proc(
|
|
html: string,
|
|
start: int,
|
|
) -> (
|
|
idx: int,
|
|
level: int,
|
|
id: string,
|
|
text: string,
|
|
next_pos: int,
|
|
) {
|
|
i := start
|
|
for i + 3 < len(html) {
|
|
if html[i] == '<' && html[i + 1] == 'h' {
|
|
d := html[i + 2]
|
|
if d >= '1' && d <= '6' {
|
|
level = int(d - '0')
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
i += 1
|
|
}
|
|
|
|
if level == 0 {
|
|
return 0, 0, "", "", len(html)
|
|
}
|
|
|
|
// Find end of opening tag
|
|
tag_end := strings.index_byte(html[idx:], '>')
|
|
if tag_end < 0 {
|
|
return 0, 0, "", "", len(html)
|
|
}
|
|
tag_end += idx
|
|
|
|
// Find id="..." within the tag
|
|
tag := html[idx:tag_end + 1]
|
|
id_pos := strings.index(tag, `id="`)
|
|
if id_pos < 0 {
|
|
// No id — skip this heading, continue searching
|
|
return next_heading(html, tag_end + 1)
|
|
}
|
|
|
|
id_start := idx + id_pos + 4
|
|
id_end_rel := strings.index_byte(html[id_start:], '"')
|
|
if id_end_rel < 0 {
|
|
return 0, 0, "", "", len(html)
|
|
}
|
|
id = html[id_start:id_start + id_end_rel]
|
|
|
|
// Text between > and </hN>
|
|
text_start := tag_end + 1
|
|
close_idx := strings.index(html[text_start:], "</h")
|
|
if close_idx < 0 {
|
|
return 0, 0, "", "", len(html)
|
|
}
|
|
text_end := text_start + close_idx
|
|
text = strip_tags(html[text_start:text_end])
|
|
|
|
// Find end of closing tag
|
|
next_pos = text_end + close_tag_len(html, text_end)
|
|
|
|
return idx, level, id, text, next_pos
|
|
}
|
|
|
|
// close_tag_len returns the length of the </hN> tag at pos.
|
|
close_tag_len :: proc(html: string, pos: int) -> int {
|
|
if pos + 4 > len(html) {
|
|
return 4
|
|
}
|
|
end := strings.index_byte(html[pos:], '>')
|
|
if end < 0 {
|
|
return 4
|
|
}
|
|
return end + 1
|
|
}
|
|
|
|
// strip_tags removes HTML tags from a string, leaving only text content.
|
|
strip_tags :: proc(s: string) -> string {
|
|
b: strings.Builder
|
|
strings.builder_init(&b, context.temp_allocator)
|
|
|
|
i := 0
|
|
for i < len(s) {
|
|
if s[i] == '<' {
|
|
end := strings.index_byte(s[i:], '>')
|
|
if end >= 0 {
|
|
i += end + 1
|
|
continue
|
|
}
|
|
}
|
|
strings.write_byte(&b, s[i])
|
|
i += 1
|
|
}
|
|
return strings.to_string(b)
|
|
}
|
|
|