package main

import "core:strconv"
import "core:strings"

TokenType :: enum {
	Text,
	Italics,
	Bold,
	BreakLine,
	Header,
	Paragraph,
	Root,
	Blockquote,
	UnorderedList,
	OrderedList,
	ListItem,
	Strikethrough,
	Table,
	TableRow,
	TableHeaderRow,
	TableCell,
	TableSeparator,
	Left,
	Right,
	Middle,
	FootnoteDef,
	FootnoteRef,
	CodeBlock,
	InlineCode,
	HorizontalRule,
	Underline,
	Superscript,
	Subscript,
	HeaderLink,
	Link,
	Image,
	Audio,
	Video,
	Iframe,
	Script,
	ScriptBlock,
	Variable,
	VariableDef,
}

TokenValue :: union {
	string,
	int,
}

Token :: struct {
	type:     TokenType,
	value:    TokenValue,
	children: [dynamic]^Token,
	parent:   ^Token,
}

push_token :: proc(parent: ^Token, type: TokenType, value: TokenValue = nil) -> ^Token {
	token := new(Token)
	token^ = Token{type, value, nil, parent}
	append(&parent.children, token)
	return token
}

is_table_separator :: proc(line: string) -> bool {
	inner := line[1:len(line) - 1]
	cells := strings.split(inner, "|", context.temp_allocator)
	if len(cells) == 0 {
		return false
	}
	for cell in cells {
		trimmed := strings.trim_space(cell)
		if len(trimmed) == 0 {
			return false
		}
		for ch in trimmed {
			if ch != '-' {
				return false
			}
		}
	}
	return true
}

parse_table_cells :: proc(line: string) -> []string {
	inner := line[1:len(line) - 1]
	cells := strings.split(inner, "|", context.temp_allocator)
	result := make([]string, len(cells), context.temp_allocator)
	for cell, i in cells {
		result[i] = strings.trim_space(cell)
	}
	return result
}

get_alignment_token :: proc(text: string) -> (TokenType, bool) {
	trimmed := strings.trim_space(text)
	if len(trimmed) == 0 {
		return .Text, false
	}
	if strings.has_suffix(trimmed, ">>") || strings.has_prefix(trimmed, ">>") {
		return .Right, true
	}
	if strings.has_suffix(trimmed, "<<") || strings.has_prefix(trimmed, "<<") {
		return .Left, true
	}
	if strings.has_suffix(trimmed, "^^") || strings.has_prefix(trimmed, "^^") {
		return .Middle, true
	}
	return .Text, false
}

push_text :: proc(parent: ^Token, text: string) {
	align_type, has_align := get_alignment_token(text)
	cleaned := strip_alignment_markers(text)

	target := parent
	if has_align {
		target = push_token(parent, align_type, nil)
	}
	push_token(target, TokenType.Text, cleaned)
}

push_text_line :: proc(target: ^Token, text: string) {
	if len(target.children) > 0 {
		br := new(Token)
		br^ = Token{TokenType.BreakLine, nil, nil, target}
		append(&target.children, br)
	}
	push_text(target, text)
}

strip_alignment_markers :: proc(text: string) -> string {
	trimmed := strings.trim_space(text)
	if strings.has_suffix(trimmed, ">>") {
		return strings.trim_space(trimmed[:len(trimmed) - 2])
	}
	if strings.has_prefix(trimmed, ">>") {
		return strings.trim_space(trimmed[2:])
	}
	if strings.has_suffix(trimmed, "<<") {
		return strings.trim_space(trimmed[:len(trimmed) - 2])
	}
	if strings.has_prefix(trimmed, "<<") {
		return strings.trim_space(trimmed[2:])
	}
	if strings.has_suffix(trimmed, "^^") {
		return strings.trim_space(trimmed[:len(trimmed) - 2])
	}
	if strings.has_prefix(trimmed, "^^") {
		return strings.trim_space(trimmed[2:])
	}
	return trimmed
}

ESCAPE_PLACEHOLDER_BASE :: 0xE000 // Unicode Private Use Area

is_escapable_char :: proc(b: byte) -> bool {
	switch b {
	case '\\',
	     '*',
	     '_',
	     '~',
	     '^',
	     '`',
	     '[',
	     ']',
	     '(',
	     ')',
	     '$',
	     '&',
	     '!',
	     '>',
	     '|',
	     '-',
	     '=',
	     '#',
	     '.':
		return true
	}
	return false
}

escape_placeholder_rune :: proc(b: byte) -> rune {
	return rune(ESCAPE_PLACEHOLDER_BASE + int(b))
}

is_escape_placeholder :: proc(r: rune) -> bool {
	return r >= ESCAPE_PLACEHOLDER_BASE && r < ESCAPE_PLACEHOLDER_BASE + 256
}

escape_placeholder_to_byte :: proc(r: rune) -> byte {
	return byte(int(r) - ESCAPE_PLACEHOLDER_BASE)
}

unescape_backslashes :: proc(s: string, allocator := context.temp_allocator) -> string {
	if !strings.contains(s, "\\") {
		return s
	}

	b := strings.builder_make(allocator)
	i := 0
	for i < len(s) {
		if s[i] == '\\' && i + 1 < len(s) && is_escapable_char(s[i + 1]) {
			strings.write_rune(&b, escape_placeholder_rune(s[i + 1]))
			i += 2
		} else {
			strings.write_byte(&b, s[i])
			i += 1
		}
	}
	return strings.to_string(b)
}

decode_escapes :: proc(s: string, allocator := context.temp_allocator) -> string {
	has_placeholder := false
	for r in s {
		if is_escape_placeholder(r) {
			has_placeholder = true
			break
		}
	}
	if !has_placeholder {
		return s
	}

	b := strings.builder_make(allocator)
	for r in s {
		if is_escape_placeholder(r) {
			strings.write_byte(&b, escape_placeholder_to_byte(r))
		} else {
			strings.write_rune(&b, r)
		}
	}
	return strings.to_string(b)
}

is_space_byte :: proc(b: byte) -> bool {
	return b == ' ' || b == '\t'
}

parse_header :: proc(line: string) -> (int, string) {
	if len(line) == 0 {
		return 0, line
	}

	lvl := 0
	for lvl < len(line) && line[lvl] == '!' {
		lvl += 1
	}
	if lvl == 0 || lvl >= len(line) || line[lvl] != ' ' {
		return 0, line
	}
	return lvl, line[lvl + 1:]
}

parse_footnote_def :: proc(line: string) -> (string, string, bool) {
	if strings.has_prefix(line, "[&&") {
		idx := strings.index_byte(line, ']')
		if idx != -1 {
			id := line[3:idx]
			text := strings.trim_space(line[idx + 1:])
			return id, text, true
		}
	}
	return "", "", false
}

StyleToggle :: struct {
	amount: int,
	type:   TokenType,
}

parse_inline_style :: proc(token: ^Token, symbol: byte, toggles: []StyleToggle) {
	if token.children != nil {
		for i := 0; i < len(token.children); i += 1 {
			parse_inline_style(token.children[i], symbol, toggles)
		}
	}

	if token.type == .Text {
		str, ok := token.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
		}

		token.value = nil

		active_styles: [256]bool
		current_node := token

		i := 0
		start := 0

		for i < len(str) {
			if str[i] == symbol {
				if i > start {
					push_token(current_node, 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, 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,
							TokenType.Text,
							str[symbol_start:symbol_start + count],
						)
						count = 0
						break
					}
				}

				current_node = token
				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, TokenType.Text, str[start:i])
		}
	}
}

pass_through_children :: proc(token: ^Token) {
	if token.type == .CodeBlock || token.type == .InlineCode {
		return
	}
	parse_inline_style(token, '*', []StyleToggle{{2, .Bold}, {1, .Italics}})
	parse_inline_style(token, '~', []StyleToggle{{2, .Strikethrough}})
	parse_inline_style(token, '-', []StyleToggle{{2, .Strikethrough}})
	parse_inline_style(token, '^', []StyleToggle{{1, .Superscript}})
	parse_inline_style(token, '_', []StyleToggle{{2, .Underline}, {1, .Subscript}})
}

parse_inline_code :: proc(token: ^Token) {
	if token.type == .CodeBlock || token.type == .InlineCode {
		return
	}
	if token.children != nil {
		for i := 0; i < len(token.children); i += 1 {
			parse_inline_code(token.children[i])
		}
	}

	if token.type == .Text {
		str, ok := token.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]

		token.value = nil

		if start_idx > 0 {
			push_token(token, .Text, str[:start_idx])
		}
		push_token(token, .InlineCode, code)

		if end_idx + 1 < len(str) {
			rem := push_token(token, .Text, str[end_idx + 1:])
			parse_inline_code(rem)
		}
	}
}

parse_inline_footnotes :: proc(token: ^Token) {
	if token.type == .CodeBlock || token.type == .InlineCode {
		return
	}
	if token.children != nil {
		for i := 0; i < len(token.children); i += 1 {
			parse_inline_footnotes(token.children[i])
		}
	}

	if token.type == .Text {
		str, ok := token.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]

		token.value = nil

		if start_idx > 0 {
			push_token(token, .Text, str[:start_idx])
		}
		push_token(token, .FootnoteRef, id)

		if end_idx + 1 < len(str) {
			rem := push_token(token, .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(token: ^Token) {
	if token.type == .CodeBlock || token.type == .InlineCode {
		return
	}
	if token.children != nil {
		for i := 0; i < len(token.children); i += 1 {
			parse_links(token.children[i])
		}
	}

	if token.type == .Text {
		str, ok := token.value.(string)
		if !ok {
			return
		}

		start_idx, end_idx, is_link, url, alt, is_header := find_link(str)
		if !is_link {
			return
		}

		token.value = nil

		if start_idx > 0 {
			push_token(token, .Text, str[:start_idx])
		}

		link_token := push_token(token, .HeaderLink if is_header else .Link, url)
		push_token(link_token, .Text, alt)

		if end_idx < len(str) {
			rem := push_token(token, .Text, str[end_idx:])
			parse_links(rem)
		}
	}
}

find_script_block :: proc(
	str: string,
) -> (
	start_idx, end_idx: int,
	is_block: bool,
	block_type: string,
	content: string,
) {
	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 {
				inner = strings.trim_space(inner)
				if strings.has_prefix(inner, "/") {
					continue
				}
				if inner == "script" || inner == "js" || inner == "javascript" {
					b := strings.builder_make(context.temp_allocator)
					strings.write_string(&b, "$[/")
					strings.write_string(&b, inner)
					strings.write_string(&b, "]")
					close_tag := strings.to_string(b)
					close_idx := strings.index(str[i + 1 + end:], close_tag)
					if close_idx != -1 {
						close_idx += i + 1 + end
						content := str[i + 1 + end:close_idx]
						return i, close_idx + len(
							close_tag,
						), true, inner, strings.trim_space(content)
					}
				}
			}
		}
	}
	return -1, -1, false, "", ""
}

find_embed :: proc(
	str: string,
) -> (
	start_idx, end_idx: int,
	is_embed: bool,
	url: string,
	alt: string,
	embed_type: string,
) {
	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 {
					clean_url := url_text
					type_hint := ""
					if pipe_idx := strings.index_byte(url_text, '|'); pipe_idx != -1 {
						clean_url = url_text[:pipe_idx]
						type_hint = url_text[pipe_idx + 1:]
					}

					if is_url(clean_url) {
						e_type := ""
						if type_hint != "" {
							e_type = parse_type_hint(type_hint)
						}
						if e_type == "" {
							e_type = detect_embed_type(clean_url)
						}
						return i, i + 1 + end, true, clean_url, alt_text, e_type
					}
				}
				if end, inner, ok := match_single(str[i + 1:], '[', ']'); ok {
					url_text := inner
					type_hint := ""
					if pipe_idx := strings.index_byte(inner, '|'); pipe_idx != -1 {
						url_text = inner[:pipe_idx]
						type_hint = inner[pipe_idx + 1:]
					}

					if is_url(url_text) {
						e_type := ""
						if type_hint != "" {
							e_type = parse_type_hint(type_hint)
						}
						if e_type == "" {
							e_type = detect_embed_type(url_text)
						}
						return i, i + 1 + end, true, url_text, url_text, e_type
					}
				}
			} else if str[i + 1] == '(' {
				if end, url_text, alt_text, ok := match_pair(str[i + 1:], '(', ')', '[', ']'); ok {
					clean_url := url_text
					type_hint := ""
					if pipe_idx := strings.index_byte(url_text, '|'); pipe_idx != -1 {
						clean_url = url_text[:pipe_idx]
						type_hint = url_text[pipe_idx + 1:]
					}

					if is_url(clean_url) {
						e_type := ""
						if type_hint != "" {
							e_type = parse_type_hint(type_hint)
						}
						if e_type == "" {
							e_type = detect_embed_type(clean_url)
						}
						return i, i + 1 + end, true, clean_url, alt_text, e_type
					}
				}
				if end, inner, ok := match_single(str[i + 1:], '(', ')'); ok {
					url_text := inner
					type_hint := ""
					if pipe_idx := strings.index_byte(inner, '|'); pipe_idx != -1 {
						url_text = inner[:pipe_idx]
						type_hint = inner[pipe_idx + 1:]
					}

					if is_url(url_text) {
						e_type := ""
						if type_hint != "" {
							e_type = parse_type_hint(type_hint)
						}
						if e_type == "" {
							e_type = detect_embed_type(url_text)
						}
						return i, i + 1 + end, true, url_text, url_text, e_type
					}
				}
			}
		}
	}

	return -1, -1, false, "", "", ""
}

parse_embeds :: proc(token: ^Token) {
	if token.type == .CodeBlock || token.type == .InlineCode {
		return
	}
	if token.children != nil {
		for i := 0; i < len(token.children); i += 1 {
			parse_embeds(token.children[i])
		}
	}

	if token.type == .Text {
		str, ok := token.value.(string)
		if !ok {
			return
		}

		s_start, s_end, is_script, s_type, s_content := find_script_block(str)
		if is_script {
			token.value = nil

			if s_start > 0 {
				push_token(token, .Text, str[:s_start])
			}

			script_token := push_token(token, .ScriptBlock, s_content)

			if s_end < len(str) {
				rem := push_token(token, .Text, str[s_end:])
				parse_embeds(rem)
			}
			return
		}

		start_idx, end_idx, is_embed, url, alt, e_type := find_embed(str)
		if !is_embed {
			return
		}

		token.value = nil

		if start_idx > 0 {
			push_token(token, .Text, str[:start_idx])
		}

		embed_token_type: TokenType
		if e_type == "image" {
			embed_token_type = .Image
		} else if e_type == "audio" {
			embed_token_type = .Audio
		} else if e_type == "video" {
			embed_token_type = .Video
		} else if e_type == "script" {
			embed_token_type = .Script
		} else {
			embed_token_type = .Iframe
		}

		embed_token := push_token(token, embed_token_type, url)
		push_token(embed_token, .Text, alt)

		if end_idx < len(str) {
			rem := push_token(token, .Text, str[end_idx:])
			parse_embeds(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 is_url(inner) {
					continue
				} else {
					return i, i + 1 + end, true, strings.trim_space(inner), "", false
				}
			}
		}
	}

	return -1, -1, false, "", "", false
}

parse_variables :: proc(token: ^Token) {
	if token.type == .CodeBlock || token.type == .InlineCode {
		return
	}
	if token.children != nil {
		for i := 0; i < len(token.children); i += 1 {
			parse_variables(token.children[i])
		}
	}

	if token.type == .Text {
		str, ok := token.value.(string)
		if !ok {
			return
		}

		start_idx, end_idx, is_var, name, val, is_def := find_variable(str)
		if !is_var {
			return
		}

		token.value = nil

		if start_idx > 0 {
			push_token(token, .Text, str[:start_idx])
		}

		if is_def {
			def_token := push_token(token, .VariableDef, name)
			push_token(def_token, .Text, val)
		} else {
			push_token(token, .Variable, name)
		}

		if end_idx < len(str) {
			rem := push_token(token, .Text, str[end_idx:])
			parse_variables(rem)
		}
	}
}

resolve_variables :: proc(token: ^Token, vars: ^map[string]string) {
	if token.type == .VariableDef {
		if name, ok := token.value.(string); ok {
			if len(token.children) > 0 {
				if val, vok := token.children[0].value.(string); vok {
					vars[name] = val
				}
			}
		}
		return
	}
	if token.type == .Variable {
		if name, ok := token.value.(string); ok {
			if val, found := vars[name]; found {
				if token.children == nil {
					push_token(token, .Text, val)
				}
			}
		}
		return
	}
	if token.type == .Link || token.type == .HeaderLink {
		if url, ok := token.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:])
							token.value = strings.to_string(b)
							break
						}
					}
				}
			}
		}
	}
	if token.children != nil {
		for child in token.children {
			resolve_variables(child, vars)
		}
	}
}

parse :: proc(lines: []string, allocator := context.allocator) -> Token {
	root := Token{TokenType.Root, nil, nil, nil}
	current_token := &root

	// FIRST PASS

	in_code_block := false
	in_script_block := false
	script_block_content: [dynamic]string
	script_block_type := ""

	for untrimmed_line, line_index in lines {
		line := strings.trim_space(untrimmed_line)

		if in_code_block {
			if strings.has_prefix(line, "```") {
				in_code_block = false
				current_token = &root
			} else {
				push_token(current_token, TokenType.Text, untrimmed_line)
			}
			continue
		}

		if in_script_block {
			if strings.has_prefix(line, "$[/") {
				full_content := strings.join(script_block_content[:], "\n")
				script_token := push_token(&root, TokenType.ScriptBlock, full_content)
				push_token(script_token, TokenType.Text, script_block_type)
				in_script_block = false
				script_block_content = nil
				script_block_type = ""
				current_token = &root
			} else {
				append(&script_block_content, untrimmed_line)
			}
			continue
		}

		line = unescape_backslashes(line)

		if strings.has_prefix(line, "```") {
			lang := strings.trim_space(line[3:])
			current_token = push_token(&root, TokenType.CodeBlock, lang)
			in_code_block = true
			continue
		}

		if strings.has_prefix(line, "$[") && strings.has_suffix(line, "]") {
			inner := strings.trim_space(line[2:len(line) - 1])
			if inner == "script" || inner == "js" || inner == "javascript" {
				in_script_block = true
				script_block_type = inner
				script_block_content = nil
				current_token = &root
				continue
			}
		}

		if len(line) >= 3 {
			is_hr := true
			ch := line[0]
			if ch == '-' || ch == '_' || ch == '=' {
				for i := 1; i < len(line); i += 1 {
					if line[i] != ch {
						is_hr = false
						break
					}
				}
			} else {
				is_hr = false
			}

			if is_hr {
				push_token(&root, TokenType.HorizontalRule, nil)
				current_token = &root
				continue
			}
		}

		if len(line) == 0 {
			current_token = &root
			continue
		}

		header_level, text := parse_header(line)
		if header_level > 0 {
			current_token = push_token(&root, TokenType.Header, header_level)
			push_text(current_token, text)
			current_token = &root
			continue
		}

		if id, ftext, ok := parse_footnote_def(line); ok {
			current_token = push_token(&root, TokenType.FootnoteDef, id)
			push_text(current_token, ftext)
			current_token = &root
			continue
		}

		if strings.has_prefix(text, "> ") {
			if current_token.type != TokenType.Blockquote {
				current_token = push_token(&root, TokenType.Blockquote, nil)
				push_token(current_token, TokenType.Paragraph, nil)
			}
			last_p := current_token.children[len(current_token.children) - 1]
			push_text_line(last_p, text[2:])
			continue
		}

		if strings.has_prefix(text, ". ") || strings.has_prefix(text, "- ") {
			if current_token.type != TokenType.UnorderedList {
				current_token = push_token(&root, TokenType.UnorderedList, nil)
			}
			li := push_token(current_token, TokenType.ListItem, nil)
			push_text(li, text[2:])
			continue
		}

		if len(text) >= 3 {
			item := 1
			idx := 0
			for idx < len(text) && text[idx] >= '0' && text[idx] <= '9' {
				idx += 1
			}

			if idx > 0 && idx + 1 < len(text) && text[idx] == '.' && text[idx + 1] == ' ' {
				item, _ = strconv.parse_int(text[:idx])
				text = text[idx + 2:]

				if current_token.type != TokenType.OrderedList {
					current_token = push_token(&root, TokenType.OrderedList, item)
				}
				li := push_token(current_token, TokenType.ListItem, nil)
				push_text(li, text)
				continue
			}
		}

		if len(text) >= 3 && text[0] == '|' && text[len(text) - 1] == '|' {
			if current_token.type != TokenType.Table {
				current_token = push_token(&root, TokenType.Table, nil)
			}

			if is_table_separator(text) {
				row_count := len(current_token.children)
				if row_count == 1 {
					current_token.children[0].type = TokenType.TableHeaderRow
				} else if row_count > 1 {
					push_token(current_token, TokenType.TableSeparator, nil)
				}
			} else {
				row := push_token(current_token, TokenType.TableRow, nil)
				cells := parse_table_cells(text)
				for cell_text in cells {
					cell := push_token(row, TokenType.TableCell, nil)
					push_text(cell, cell_text)
				}
			}
			continue
		}

		// LAST-CASE SCENARIO: NORMAL TEXT

		if current_token.type == .Blockquote {
			last_p := current_token.children[len(current_token.children) - 1]
			push_text_line(last_p, line)
		} else if current_token.type == .UnorderedList || current_token.type == .OrderedList {
			last_li := current_token.children[len(current_token.children) - 1]
			push_text_line(last_li, line)
		} else {
			if current_token.type != .Paragraph {
				current_token = push_token(&root, TokenType.Paragraph, nil)
			}
			push_text_line(current_token, line)
		}
	}

	// SECOND PASS

	for token in root.children {
		parse_inline_code(token)
		parse_inline_footnotes(token)
		parse_links(token)
		parse_variables(token)
		parse_embeds(token)
		pass_through_children(token)
	}

	// VARIABLE RESOLUTION PASS

	vars: map[string]string
	for child in root.children {
		resolve_variables(child, &vars)
	}

	// FOURTH PASS

	footnotes: [dynamic]^Token
	new_root_children: [dynamic]^Token
	for child in root.children {
		if child.type == .FootnoteDef {
			append(&footnotes, child)
		} else {
			append(&new_root_children, child)
		}
	}
	for child in footnotes {
		append(&new_root_children, child)
	}
	root.children = new_root_children

	return root
}
