69e5c62382
(I know it would be refactoration, but that sounds much worse)
471 lines
10 KiB
Odin
471 lines
10 KiB
Odin
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}})
|
|
}
|