Add toc; change minor things

This commit is contained in:
2026-08-05 18:37:16 -04:00
parent 7817cd1dac
commit b5f3ed49d3
+270 -19
View File
@@ -146,6 +146,151 @@ create_output_directories :: proc(dir_names: []string) {
} }
} }
html_escape :: proc(sb: ^strings.Builder, html: string) {
strings.write_string(sb, html)
}
render_html :: proc(sb: ^strings.Builder, root: ^md.Node) {
iter := md.iter_new(root)
defer md.iter_free(iter)
should_write_slug: i32 = 0
parent_is_li := false
for md.iter_next(iter) != .Done {
node := md.iter_get_node(iter)
event := md.iter_get_event_type(iter)
#partial switch node.type {
case .Heading:
level := md.node_get_heading_level(node)
if event == .Enter {
should_write_slug = level
} else {
fmt.sbprintf(sb, "</h%d>\n", level)
}
case .Paragraph:
if parent_is_li {
if event != .Enter do parent_is_li = false
} else {
if event == .Enter {
strings.write_string(sb, "<p>")
} else if event == .Exit {
strings.write_string(sb, "</p>\n")
}
}
case .Emph:
if event == .Enter {
strings.write_string(sb, "<em>")
} else {
strings.write_string(sb, "</em>")
}
case .Strong:
if event == .Enter {
strings.write_string(sb, "<strong>")
} else {
strings.write_string(sb, "</strong>")
}
case .Code:
strings.write_string(sb, "<code>")
lit := string(md.node_get_literal(node))
html_escape(sb, lit)
strings.write_string(sb, "</code>")
case .Text:
lit := string(md.node_get_literal(node))
if should_write_slug > 0 {
fmt.sbprintf(sb, "<h%d id='", should_write_slug)
sb_write_slug(sb, lit)
strings.write_string(sb, "'>")
should_write_slug = 0
}
html_escape(sb, lit)
case .Soft_Break:
strings.write_byte(sb, ' ')
case .Line_Break:
strings.write_string(sb, "<br />\n")
case .Code_Block:
fence_info := string(md.node_get_fence_info(node))
i := strings.index_any(fence_info, " \t\r\n")
lang := fence_info
if i != -1 {
lang = fence_info[:i]
}
strings.write_string(sb, "<pre class='language-")
strings.write_string(sb, lang)
strings.write_string(sb, "'><code>")
html_escape(sb, string(md.node_get_literal(node)))
strings.write_string(sb, "</code></pre>\n")
case .HTML_Inline:
if event == .Enter {
strings.write_string(sb, string(md.node_get_literal(node)))
}
case .HTML_Block:
if event == .Enter {
strings.write_string(sb, string(md.node_get_literal(node)))
}
case .Thematic_Break:
if event == .Enter {
strings.write_string(sb, "<hr />\n")
}
case .List:
if event == .Enter {
if md.node_get_list_type(node) == .Bullet {
strings.write_string(sb, "<ul>\n")
} else {
strings.write_string(sb, "<ol>\n")
}
} else {
if md.node_get_list_type(node) == .Bullet {
strings.write_string(sb, "</ul>\n")
} else {
strings.write_string(sb, "</ol>\n")
}
}
case .Item:
if event == .Enter {
parent_is_li = true
strings.write_string(sb, "<li>")
} else {
strings.write_string(sb, "</li>\n")
}
case .Link:
if event == .Enter {
fmt.sbprintf(sb,
"<a href=\"%s\">",
string(md.node_get_url(node)))
} else {
strings.write_string(sb, "</a>")
}
case .Image:
if event == .Enter {
fmt.sbprintf(sb,
"<img src=\"%s\" alt=\"",
string(md.node_get_url(node)))
} else {
strings.write_string(sb, "\" />")
}
}
}
}
remove_title :: proc(root: ^md.Node) { remove_title :: proc(root: ^md.Node) {
heading_state: enum { heading_state: enum {
None, None,
@@ -182,6 +327,91 @@ remove_title :: proc(root: ^md.Node) {
//return title //return title
} }
sb_indent :: proc(sb: ^strings.Builder, level: i32) {
for i in 0..<level {
strings.write_string(sb, " ")
}
}
sb_write_slug :: proc(sb: ^strings.Builder, text: string) {
wrote_any := false
pending_dash := false
for c in text {
switch {
case c >= 'A' && c <= 'Z':
if pending_dash {
strings.write_byte(sb, '-')
pending_dash = false
}
strings.write_byte(sb, byte(c + ('a' - 'A')))
wrote_any = true
case (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'):
if pending_dash {
strings.write_byte(sb, '-')
pending_dash = false
}
strings.write_byte(sb, byte(c))
wrote_any = true
case:
if wrote_any {
pending_dash = true
}
}
}
}
generate_toc :: proc(sb: ^strings.Builder, root: ^md.Node) {
entered_heading := false
started := false
iter := md.iter_new(root)
defer md.iter_free(iter)
current_level: i32 = 0
for md.iter_next(iter) != .Done {
event_type := md.iter_get_event_type(iter)
node := md.iter_get_node(iter)
#partial switch node.type {
case .Heading:
level := md.node_get_heading_level(node)
event_type := md.iter_get_event_type(iter)
if event_type == .Enter {
entered_heading = true
if !started {
for i in 1..<level do strings.write_string(sb, "<ol>")
started = true
} else {
for i in current_level..<level do strings.write_string(sb, "<ol>")
if current_level > level do strings.write_string(sb, "</li>")
for i in level..<current_level do strings.write_string(sb, "</ol></li>")
if current_level == level do strings.write_string(sb, "</li>")
}
strings.write_string(sb, "<li>")
current_level = level
}
case .Text:
if entered_heading {
entered_heading = false
text := string(md.node_get_literal(node))
strings.write_string(sb, "<a href='#")
sb_write_slug(sb, text)
strings.write_string(sb, "'>")
strings.write_string(sb, text)
strings.write_string(sb, "</a>")
}
}
}
if started {
strings.write_string(sb, "</li>")
for i in 1..<current_level do strings.write_string(sb, "</ol>")
}
}
// must free // must free
parse_json :: proc(file_path: string) -> json.Value { parse_json :: proc(file_path: string) -> json.Value {
json_file_content, os_err := os.read_entire_file_from_path(file_path, context.temp_allocator) json_file_content, os_err := os.read_entire_file_from_path(file_path, context.temp_allocator)
@@ -326,14 +556,29 @@ process_markdown :: proc(sb: ^strings.Builder, site: Page) {
{.Validate_UTF8, .Unsafe}, {.Validate_UTF8, .Unsafe},
) )
remove_title(cmark_root) remove_title(cmark_root)
html := md.render_html(cmark_root, {.Unsafe})
output_file_name := fmt.tprint(md_name, ".html", sep = "")
dir, alloc_err3 := os.join_path(
{OUTPUT_ROOT_NAME, markdown_dir, md_name},
context.temp_allocator
)
if alloc_err3 != nil {
fmt.println("allocator error:", alloc_err3)
continue
}
os.make_directory(dir)
output_file_path, alloc_err2 := os.join_path( output_file_path, alloc_err2 := os.join_path(
{OUTPUT_ROOT_NAME, markdown_dir, output_file_name}, {OUTPUT_ROOT_NAME, markdown_dir, md_name, "index.html"},
context.temp_allocator, context.temp_allocator,
) )
if alloc_err2 != nil {
fmt.println("allocator error:", alloc_err3)
continue
}
out_file, os_err2 := os.open(output_file_path, {.Write, .Create, .Trunc}) out_file, os_err2 := os.open(output_file_path, {.Write, .Create, .Trunc})
if os_err2 != nil { if os_err2 != nil {
fmt.eprintfln("failed to open output file '%s' (%s)", output_file_path, os_err2) fmt.eprintfln("failed to open output file '%s' (%s)", output_file_path, os_err2)
@@ -353,10 +598,21 @@ process_markdown :: proc(sb: ^strings.Builder, site: Page) {
last_updated_date(sb, date) last_updated_date(sb, date)
block_begin(sb) {
defer block_end(sb) block_begin(sb)
defer block_end(sb)
fmt.sbprint(sb, html) strings.write_string(sb, "<h2>Contents</h2>")
generate_toc(sb, cmark_root)
}
strings.write_string(sb, "<hr>")
{
block_begin(sb)
defer block_end(sb)
render_html(sb, cmark_root)
}
} }
write_len: int write_len: int
@@ -410,10 +666,11 @@ create_toc_page :: proc(title, toc_path: string, toc_info: ^[dynamic][3]string,
tr_begin(sb) tr_begin(sb)
fmt.sbprintf( fmt.sbprintf(
sb, sb,
"<td><a href='%s.html'>%s</a></td><td><span class='table-last-updated' data-value='%s'>aarfsdf</span></td>", "<td><a href='%s'>%s</a></td><td><span class='table-last-updated' data-value='%s'>%s</span></td>",
item[0], item[0],
item[1], item[1],
item[2], item[2],
item[2],
) )
tr_end(sb) tr_end(sb)
} }
@@ -457,19 +714,13 @@ create_toc_page :: proc(title, toc_path: string, toc_info: ^[dynamic][3]string,
} }
last_updated_date :: proc(sb: ^strings.Builder, date: string) { last_updated_date :: proc(sb: ^strings.Builder, date: string) {
fmt.sbprint(sb, "<span class='last-updated'></span>") strings.write_string(sb, "<span class='last-updated'>Last updated: ")
fmt.sbprint( strings.write_string(sb, date)
sb, strings.write_string(sb, "</span>")
"<script>",
"const date = new Date('",
date,
"');",
"document.querySelector('.last-updated').innerText = 'Last updated: ' + new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short'}).format(date)",
"</script>",
sep = "",
)
strings.write_string(sb, "<script>const date = new Date('")
strings.write_string(sb, date)
strings.write_string(sb, "'); document.querySelector('.last-updated').innerText = 'Last updated: ' + new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short'}).format(date)</script>")
} }
nav :: proc(sb: ^strings.Builder, current_path: string, site: Page) { nav :: proc(sb: ^strings.Builder, current_path: string, site: Page) {