package markdown import cm "vendor:commonmark" import "core:strings" // DefList_Entry represents a single term-definition pair in a definition list. DefList_Entry :: struct { term: string, definition: string, } // convert_deflists scans markdown text for definition list patterns and // converts them to
HTML blocks before cmark processing. // // A definition line starts with optional whitespace followed by a colon and // a space. The term is the nearest preceding non-blank line (immediately or // within one blank line). Consecutive term+definition pairs are grouped into // a single
block. // // Terms and definitions are rendered through cmark individually so that // inline markdown (code, links, emphasis) is processed. convert_deflists :: proc(body: string, allocator := context.allocator) -> string { lines := strings.split(body, "\n", allocator = context.temp_allocator) sb := strings.builder_make(context.temp_allocator) first := true need_blank := false i := 0 for i < len(lines) { entries, matched, next := try_match_deflist(lines, i) if matched { html := render_deflist(entries) if !first { strings.write_string(&sb, "\n\n") } strings.write_string(&sb, html) first = false need_blank = true i = next continue } if need_blank { strings.write_string(&sb, "\n\n") need_blank = false } else if !first { strings.write_string(&sb, "\n") } strings.write_string(&sb, lines[i]) first = false i += 1 } return strings.clone(strings.to_string(sb), allocator) } // try_match_deflist attempts to match a definition list group starting at // lines[start]. A group is one or more term+definition pairs. Returns the // matched entries, whether a match was found, and the index past the group. try_match_deflist :: proc( lines: []string, start: int, ) -> ( entries: [dynamic]DefList_Entry, ok: bool, end: int, ) { entries = make([dynamic]DefList_Entry, 0, allocator = context.temp_allocator) end = start i := start for i < len(lines) { // A term must be non-blank and not itself a def line if is_blank_line(lines[i]) || is_def_line(lines[i]) { break } // Look for a def line: immediately after or with one blank line def_idx := i + 1 if def_idx < len(lines) && is_blank_line(lines[def_idx]) { def_idx += 1 } if def_idx >= len(lines) || !is_def_line(lines[def_idx]) { break } // Found a term + def pair append( &entries, DefList_Entry { term = strings.trim_space(lines[i]), definition = def_content(lines[def_idx]), }, ) i = def_idx + 1 // After a pair, check if another pair follows (optionally // separated by one blank line). If so, continue the group. // If not, break without consuming the blank line. if i < len(lines) && is_blank_line(lines[i]) { after_blank := i + 1 if after_blank < len(lines) && !is_blank_line(lines[after_blank]) && !is_def_line(lines[after_blank]) { // Check whether a def follows this potential term check_def := after_blank + 1 if check_def < len(lines) && is_blank_line(lines[check_def]) { check_def += 1 } if check_def < len(lines) && is_def_line(lines[check_def]) { i = after_blank continue } } break } } if len(entries) > 0 { ok = true end = i } return } // is_def_line returns true if the line is a definition line: // optional leading whitespace, a colon, then whitespace or end-of-line. is_def_line :: proc(line: string) -> bool { trimmed := strings.trim_left(line, " \t") if len(trimmed) < 1 || trimmed[0] != ':' { return false } if len(trimmed) == 1 { return true } return trimmed[1] == ' ' || trimmed[1] == '\t' } // def_content extracts the definition text from a definition line, // stripping the leading colon and surrounding whitespace. def_content :: proc(line: string) -> string { trimmed := strings.trim_left(line, " \t") content := trimmed[1:] content = strings.trim_left(content, " \t") return content } // is_blank_line returns true for empty or whitespace-only lines. is_blank_line :: proc(line: string) -> bool { return strings.trim_space(line) == "" } // render_deflist builds the
HTML block from a list of entries. // Each term and definition is rendered through cmark to process inline // markdown. Result lives in context.temp_allocator. render_deflist :: proc(entries: [dynamic]DefList_Entry) -> string { sb := strings.builder_make(context.temp_allocator) strings.write_string(&sb, "
") for entry in entries { term_html := render_inline_md(entry.term) def_html := render_inline_md(entry.definition) strings.write_string(&sb, "
") strings.write_string(&sb, term_html) strings.write_string(&sb, "
") strings.write_string(&sb, def_html) strings.write_string(&sb, "
") } strings.write_string(&sb, "
") return strings.to_string(sb) } // render_inline_md renders a snippet of markdown through cmark and strips // the surrounding

tags. Result lives in context.temp_allocator. render_inline_md :: proc(text: string) -> string { raw := cm.markdown_to_html_from_string(text, {.Unsafe}) defer cm.free_string(raw) return strings.clone(strip_p_tags(raw), context.temp_allocator) } // strip_p_tags removes surrounding

if the HTML is a single paragraph. strip_p_tags :: proc(html: string) -> string { s := html if len(s) > 0 && s[len(s) - 1] == '\n' { s = s[:len(s) - 1] } if strings.has_prefix(s, "

") && strings.has_suffix(s, "

") { return s[3:len(s) - 4] } return s }