package markdown import ts "../treesitter" import "core:fmt" import "core:log" import "core:strings" Capture :: struct { start: u32, end: u32, name: string, } find_first_error_line :: proc(root: ts.Node) -> int { if ts.node_is_error(root) { return int(ts.node_start_point(root).row) + 1 } for i in 0 ..< ts.node_child_count(root) { child := ts.node_child(root, u32(i)) if ts.node_has_error(child) { line := find_first_error_line(child) if line > 0 { return line } } } return 0 } write_span_open :: proc(b: ^strings.Builder, buf: ^[128]u8, name: string) { pos := 0 prefix := " start do strings.write_string(b, s[start:i]) strings.write_string(b, "&") start = i + 1 case '<': if i > start do strings.write_string(b, s[start:i]) strings.write_string(b, "<") start = i + 1 case '>': if i > start do strings.write_string(b, s[start:i]) strings.write_string(b, ">") start = i + 1 case '"': if i > start do strings.write_string(b, s[start:i]) strings.write_string(b, """) start = i + 1 } } if start == 0 { strings.write_string(b, s) } else if start < len(s) { strings.write_string(b, s[start:]) } } unescape_html :: proc(s: string) -> string { sb := strings.builder_make() defer strings.builder_destroy(&sb) start := 0 for i in 0 ..< len(s) { if s[i] != '&' do continue semi := strings.index(s[i:], ";") if semi < 0 do break entity := s[i:i + semi + 1] replacement := "" switch entity { case "&": replacement = "&" case "<": replacement = "<" case ">": replacement = ">" case """: replacement = "\"" case "'", "'": replacement = "'" case: continue } if i > start do strings.write_string(&sb, s[start:i]) strings.write_string(&sb, replacement) start = i + semi + 1 } if start == 0 do return s if start < len(s) do strings.write_string(&sb, s[start:]) return strings.to_string(sb) } highlight_block :: proc(code: string, lang: string, file_path: string) -> string { gc := ts.load_grammar(lang) if gc == nil { return code } raw_code := unescape_html(code) raw_c := strings.clone_to_cstring(raw_code) defer delete(raw_c) tree := ts.parser_parse_string(gc.parser, nil, raw_c, u32(len(raw_code))) if tree == nil { return code } defer ts.tree_delete(tree) root := ts.tree_root_node(tree) if ts.node_has_error(root) { line := find_first_error_line(root) if line > 0 { log.warnf( "highlight: syntax errors in %s code block at line %d (%s)", lang, line, file_path, ) } else { log.warnf("highlight: syntax errors in %s code block (%s)", lang, file_path) } } cursor := gc.cursor if cursor == nil { return code } ts.query_cursor_exec(cursor, gc.query, root) captures := make([dynamic]Capture, 0, 64, context.temp_allocator) defer delete(captures) match: ts.Query_Match capture_idx: u32 for ts.query_cursor_next_capture(cursor, &match, &capture_idx) { if capture_idx >= u32(match.capture_count) { continue } cap := match.captures[capture_idx] name_len: u32 name_c := ts.query_capture_name_for_id(gc.query, cap.index, &name_len) if name_c == nil { continue } name_full := string(name_c) name := name_full if len(name_full) > int(name_len) { name = name_full[:int(name_len)] } append( &captures, Capture { start = ts.node_start_byte(cap.node), end = ts.node_end_byte(cap.node), name = name, }, ) } if len(captures) == 0 { return code } sb := strings.builder_make_len(len(code) * 2) last_pos: u32 = 0 stack := make([dynamic]Capture, 0, 16, context.temp_allocator) defer delete(stack) buf: [128]u8 for cap in captures { for len(stack) > 0 { top := stack[len(stack) - 1] if top.end <= cap.start { if top.end > last_pos { write_escaped(&sb, raw_code[last_pos:top.end]) } strings.write_string(&sb, "") last_pos = top.end pop(&stack) } else { break } } if cap.start > last_pos { write_escaped(&sb, raw_code[last_pos:cap.start]) last_pos = cap.start } write_span_open(&sb, &buf, cap.name) append(&stack, cap) } for len(stack) > 0 { top := pop(&stack) if top.end > last_pos { write_escaped(&sb, raw_code[last_pos:top.end]) } strings.write_string(&sb, "") last_pos = top.end } if int(last_pos) < len(raw_code) { write_escaped(&sb, raw_code[last_pos:]) } return strings.to_string(sb) } highlight_code :: proc(html: string, file_path: string) -> string { PREFIX :: `
' {
			code_start += 1
		} else {
			pos = lang_end
			continue
		}

		end_rel := strings.index(html[code_start:], CODE_END)
		if end_rel < 0 {
			break
		}
		end_idx := code_start + end_rel

		code := html[code_start:end_idx]
		highlighted := highlight_block(code, lang, file_path)
		fmt.sbprintf(&sb, `
%s
`, lang, highlighted) pos = end_idx + len(CODE_END) } if pos < len(html) && found { strings.write_string(&sb, html[pos:]) } if !found { return html } return strings.to_string(sb) }