package parser import "../mime" import "../token" import "core:strings" StyleToggle :: struct { amount: int, type: token.TokenType, } parse_inline_style :: proc(tok: ^token.Token, symbol: byte, toggles: []StyleToggle) { if tok.children != nil { for i := 0; i < len(tok.children); i += 1 { parse_inline_style(tok.children[i], symbol, toggles) } } if tok.type == .Text { str, ok := tok.value.(string) if !ok { return } symbol_found := false for i := 0; i < len(str); i += 1 { if str[i] == symbol { symbol_found = true break } } if !symbol_found { return } tok.value = nil active_styles: [256]bool current_node := tok i := 0 start := 0 for i < len(str) { if str[i] == symbol { if i > start { push_token(current_node, token.TokenType.Text, str[start:i]) } count := 0 symbol_start := i for i < len(str) && str[i] == symbol { count += 1 i += 1 } preceded_by_space := symbol_start > 0 && is_space_byte(str[symbol_start - 1]) followed_by_space := i < len(str) && is_space_byte(str[i]) if preceded_by_space && followed_by_space { push_token(current_node, token.TokenType.Text, str[symbol_start:i]) start = i continue } for count > 0 { matched := false for toggle in toggles { if count >= toggle.amount { active_styles[int(toggle.type)] = !active_styles[int(toggle.type)] count -= toggle.amount matched = true break } } if !matched { push_token( current_node, token.TokenType.Text, str[symbol_start:symbol_start + count], ) count = 0 break } } current_node = tok for toggle in toggles { if active_styles[int(toggle.type)] { last := current_node.children[len(current_node.children) - 1] if len(current_node.children) > 0 else nil current_node = last if last != nil && last.type == toggle.type else push_token(current_node, toggle.type, nil) } } start = i } else { i += 1 } } if i > start { push_token(current_node, token.TokenType.Text, str[start:i]) } } } parse_inline_code :: proc(tok: ^token.Token) { if tok.type == .CodeBlock || tok.type == .InlineCode { return } if tok.children != nil { for i := 0; i < len(tok.children); i += 1 { parse_inline_code(tok.children[i]) } } if tok.type == .Text { str, ok := tok.value.(string) if !ok { return } start_idx := strings.index_byte(str, '`') if start_idx == -1 { return } end_idx := strings.index_byte(str[start_idx + 1:], '`') if end_idx == -1 { return } end_idx += start_idx + 1 code := str[start_idx + 1:end_idx] tok.value = nil if start_idx > 0 { push_token(tok, .Text, str[:start_idx]) } push_token(tok, .InlineCode, code) if end_idx + 1 < len(str) { rem := push_token(tok, .Text, str[end_idx + 1:]) parse_inline_code(rem) } } } parse_inline_footnotes :: proc(tok: ^token.Token) { if tok.type == .CodeBlock || tok.type == .InlineCode { return } if tok.children != nil { for i := 0; i < len(tok.children); i += 1 { parse_inline_footnotes(tok.children[i]) } } if tok.type == .Text { str, ok := tok.value.(string) if !ok { return } start_idx := strings.index(str, "[&") if start_idx == -1 { return } end_idx := strings.index_byte(str[start_idx:], ']') if end_idx == -1 { return } end_idx += start_idx id := str[start_idx + 2:end_idx] tok.value = nil if start_idx > 0 { push_token(tok, .Text, str[:start_idx]) } push_token(tok, .FootnoteRef, id) if end_idx + 1 < len(str) { rem := push_token(tok, .Text, str[end_idx + 1:]) parse_inline_footnotes(rem) } } } match_pair :: proc(s: string, open1, close1, open2, close2: byte) -> (int, string, string, bool) { if len(s) == 0 || s[0] != open1 { return 0, "", "", false } depth := 1 close1_idx := -1 for i := 1; i < len(s); i += 1 { if s[i] == open1 { depth += 1 } else if s[i] == close1 { depth -= 1 if depth == 0 { close1_idx = i break } } } if close1_idx == -1 { return 0, "", "", false } if close1_idx + 1 < len(s) && s[close1_idx + 1] == open2 { depth = 1 close2_idx := -1 for i := close1_idx + 2; i < len(s); i += 1 { if s[i] == open2 { depth += 1 } else if s[i] == close2 { depth -= 1 if depth == 0 { close2_idx = i break } } } if close2_idx != -1 { return close2_idx + 1, s[1:close1_idx], s[close1_idx + 2:close2_idx], true } } return 0, "", "", false } match_single :: proc(s: string, open, close: byte) -> (int, string, bool) { if len(s) == 0 || s[0] != open { return 0, "", false } close_idx := strings.index_byte(s, close) if close_idx == -1 { return 0, "", false } return close_idx + 1, s[1:close_idx], true } find_link :: proc( str: string, ) -> ( start_idx, end_idx: int, is_link: bool, url: string, alt: string, is_header: bool, ) { for i := 0; i < len(str); i += 1 { if str[i] == '#' && i + 1 < len(str) { if str[i + 1] == '[' { if end, alt_text, url_text, ok := match_pair(str[i + 1:], '[', ']', '(', ')'); ok { return i, i + 1 + end, true, url_text, alt_text, false } if end, inner, ok := match_single(str[i + 1:], '[', ']'); ok { return i, i + 1 + end, true, inner, inner, false } } else if str[i + 1] == '(' { if end, url_text, alt_text, ok := match_pair(str[i + 1:], '(', ')', '[', ']'); ok { return i, i + 1 + end, true, url_text, alt_text, false } if end, inner, ok := match_single(str[i + 1:], '(', ')'); ok { return i, i + 1 + end, true, inner, inner, false } } } if str[i] == '[' { if end, alt_text, url_text, ok := match_pair(str[i:], '[', ']', '(', ')'); ok { if strings.has_prefix(url_text, "#") { return i, i + end, true, url_text[1:], alt_text, true } } if end, inner, ok := match_single(str[i:], '[', ']'); ok { if strings.has_prefix(inner, "#") { return i, i + end, true, inner[1:], inner[1:], true } } } else if str[i] == '(' { if end, url_text, alt_text, ok := match_pair(str[i:], '(', ')', '[', ']'); ok { if strings.has_prefix(url_text, "#") { return i, i + end, true, url_text[1:], alt_text, true } } if end, inner, ok := match_single(str[i:], '(', ')'); ok { if strings.has_prefix(inner, "#") { return i, i + end, true, inner[1:], inner[1:], true } } } } return -1, -1, false, "", "", false } parse_links :: proc(tok: ^token.Token) { if tok.type == .CodeBlock || tok.type == .InlineCode { return } if tok.children != nil { for i := 0; i < len(tok.children); i += 1 { parse_links(tok.children[i]) } } if tok.type == .Text { str, ok := tok.value.(string) if !ok { return } start_idx, end_idx, is_link, url, alt, is_header := find_link(str) if !is_link { return } tok.value = nil if start_idx > 0 { push_token(tok, .Text, str[:start_idx]) } link_token := push_token(tok, .HeaderLink if is_header else .Link, url) push_token(link_token, .Text, alt) if end_idx < len(str) { rem := push_token(tok, .Text, str[end_idx:]) parse_links(rem) } } } find_variable :: proc( str: string, ) -> ( start_idx, end_idx: int, is_var: bool, name: string, val: string, is_def: bool, ) { for i := 0; i < len(str); i += 1 { if str[i] == '$' && i + 1 < len(str) && str[i + 1] == '(' { if end, inner, ok := match_single(str[i + 1:], '(', ')'); ok { eq := strings.index_byte(inner, '=') if eq != -1 { return i, i + 1 + end, true, strings.trim_space(inner[:eq]), strings.trim_space(inner[eq + 1:]), true } else if mime.is_url(inner) { continue } else { return i, i + 1 + end, true, strings.trim_space(inner), "", false } } } } return -1, -1, false, "", "", false } parse_variables :: proc(tok: ^token.Token) { if tok.type == .CodeBlock || tok.type == .InlineCode { return } if tok.children != nil { for i := 0; i < len(tok.children); i += 1 { parse_variables(tok.children[i]) } } if tok.type == .Text { str, ok := tok.value.(string) if !ok { return } start_idx, end_idx, is_var, name, val, is_def := find_variable(str) if !is_var { return } tok.value = nil if start_idx > 0 { push_token(tok, .Text, str[:start_idx]) } if is_def { def_token := push_token(tok, .VariableDef, name) push_token(def_token, .Text, val) } else { push_token(tok, .Variable, name) } if end_idx < len(str) { rem := push_token(tok, .Text, str[end_idx:]) parse_variables(rem) } } } resolve_variables :: proc(tok: ^token.Token, vars: ^map[string]string) { if tok.type == .VariableDef { if name, ok := tok.value.(string); ok { if len(tok.children) > 0 { if val, vok := tok.children[0].value.(string); vok { vars[name] = val } } } return } if tok.type == .Variable { if name, ok := tok.value.(string); ok { if val, found := vars[name]; found { if tok.children == nil { push_token(tok, .Text, val) } } } return } if tok.type == .Link || tok.type == .HeaderLink { if url, ok := tok.value.(string); ok { for i := 0; i < len(url); i += 1 { if url[i] == '$' && i + 1 < len(url) && url[i + 1] == '(' { if end, inner, ok := match_single(url[i + 1:], '(', ')'); ok { var_name := strings.trim_space(inner) if val, found := vars[var_name]; found { b := strings.builder_make(context.temp_allocator) strings.write_string(&b, url[:i]) strings.write_string(&b, val) strings.write_string(&b, url[i + 1 + end:]) tok.value = strings.to_string(b) break } } } } } } if tok.children != nil { for child in tok.children { resolve_variables(child, vars) } } } pass_through_children :: proc(tok: ^token.Token) { if tok.type == .CodeBlock || tok.type == .InlineCode { return } parse_inline_style(tok, '*', []StyleToggle{{2, .Bold}, {1, .Italics}}) parse_inline_style(tok, '~', []StyleToggle{{2, .Strikethrough}}) parse_inline_style(tok, '-', []StyleToggle{{2, .Strikethrough}}) parse_inline_style(tok, '^', []StyleToggle{{1, .Superscript}}) parse_inline_style(tok, '_', []StyleToggle{{2, .Underline}, {1, .Subscript}}) }