From 10afe9f22a340e0e4d5cb24cc898cda8ac0d8dc9 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Tue, 14 Jul 2026 22:24:48 +0200 Subject: [PATCH] Scripts --- preview | 30 +++++- src/html.odin | 73 +++++++++++++- src/main.odin | 6 ++ src/parser.odin | 256 ++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 351 insertions(+), 14 deletions(-) diff --git a/preview b/preview index 87dcce3..d50d10f 100755 --- a/preview +++ b/preview @@ -5,7 +5,35 @@ style="" doctype='' meta='' start="$doctypeTypoML Preview$meta$style" -end="" +script=' +' +end="$script" output="$start$input$end" echo "$output" > /tmp/preview.html nohup xdg-open /tmp/preview.html > /dev/null 2>&1 & diff --git a/src/html.odin b/src/html.odin index c1851bd..301ed96 100644 --- a/src/html.odin +++ b/src/html.odin @@ -14,6 +14,34 @@ get_text_content :: proc(token: ^Token, b: ^strings.Builder) { } } +check_only_var_defs :: proc(token: ^Token) -> bool { + for child in token.children { + if child.type == .VariableDef || child.type == .BreakLine { + continue + } + if child.type == .Text { + if child.value != nil { + if v, ok := child.value.(string); ok && v != "" { + return false + } + } + has_non_var_def := false + for gc in child.children { + if gc.type != .VariableDef { + has_non_var_def = true + break + } + } + if has_non_var_def { + return false + } + continue + } + return false + } + return true +} + slugifyifyifyify :: proc(text: string) -> string { lower := strings.to_lower(text, context.temp_allocator) res, _ := strings.replace_all(lower, " ", "-", context.temp_allocator) @@ -51,11 +79,20 @@ print_html :: proc(token: ^Token) { fmt.printf("\n", level) case TokenType.Paragraph: - fmt.print("

") - for child in token.children { - print_html(child) + only_var_defs := check_only_var_defs(token) + if !only_var_defs { + fmt.print("

") + for child in token.children { + print_html(child) + } + fmt.print("

\n") + } else { + for child in token.children { + if child.type != .BreakLine { + print_html(child) + } + } } - fmt.print("

\n") case TokenType.Blockquote: fmt.print("
\n") @@ -316,6 +353,34 @@ print_html :: proc(token: ^Token) { fmt.printf("", url) } + case TokenType.ScriptBlock: + if content, ok := token.value.(string); ok { + fmt.printf("", content) + } + + case TokenType.Variable: + if name, ok := token.value.(string); ok { + fmt.printf("", name) + for child in token.children { + print_html(child) + } + fmt.print("") + } + + case TokenType.VariableDef: + if name, ok := token.value.(string); ok { + b := strings.builder_make(context.temp_allocator) + for child in token.children { + get_text_content(child, &b) + } + val := strings.to_string(b) + fmt.printf( + "", + name, + val, + ) + } + case TokenType.BreakLine: fmt.print("
\n") diff --git a/src/main.odin b/src/main.odin index 9cf2c14..c885968 100644 --- a/src/main.odin +++ b/src/main.odin @@ -85,6 +85,12 @@ print_token :: proc(token: ^Token, depth: int = 0) { type_str = "Iframe" case TokenType.Script: type_str = "Script" + case TokenType.ScriptBlock: + type_str = "ScriptBlock" + case TokenType.Variable: + type_str = "Variable" + case TokenType.VariableDef: + type_str = "VariableDef" } value_str := "" diff --git a/src/parser.odin b/src/parser.odin index c278a2c..debd893 100644 --- a/src/parser.odin +++ b/src/parser.odin @@ -1,5 +1,6 @@ package main +import "core:fmt" import "core:strconv" import "core:strings" @@ -39,6 +40,9 @@ TokenType :: enum { Video, Iframe, Script, + ScriptBlock, + Variable, + VariableDef, } TokenValue :: union { @@ -362,15 +366,39 @@ match_pair :: proc(s: string, open1, close1, open2, close2: byte) -> (int, strin if len(s) == 0 || s[0] != open1 { return 0, "", "", false } - close1_idx := strings.index_byte(s, close1) + + 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 { - close2_idx := strings.index_byte(s[close1_idx + 2:], close2) + 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 { - close2_idx += close1_idx + 2 return close2_idx + 1, s[1:close1_idx], s[close1_idx + 2:close2_idx], true } } @@ -481,6 +509,42 @@ parse_links :: proc(token: ^Token) { } } +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, ) -> ( @@ -593,6 +657,23 @@ parse_embeds :: proc(token: ^Token) { 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 @@ -627,6 +708,126 @@ parse_embeds :: proc(token: ^Token) { } } +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) + } + } +} + add_breaklines :: proc(token: ^Token) { if token.type == .CodeBlock { return @@ -668,6 +869,9 @@ parse :: proc(lines: []string, allocator := context.allocator) -> Token { // 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) @@ -682,6 +886,21 @@ parse :: proc(lines: []string, allocator := context.allocator) -> Token { 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 + } + if strings.has_prefix(line, "```") { lang := strings.trim_space(line[3:]) current_token = push_token(&root, TokenType.CodeBlock, lang) @@ -689,6 +908,17 @@ parse :: proc(lines: []string, allocator := context.allocator) -> Token { 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] @@ -811,17 +1041,25 @@ parse :: proc(lines: []string, allocator := context.allocator) -> Token { // SECOND PASS for token in root.children { - add_breaklines(token) + parse_inline_code(token) + parse_links(token) + parse_variables(token) + parse_embeds(token) + parse_inline_footnotes(token) + pass_through_children(token) } // THIRD PASS for token in root.children { - parse_inline_code(token) - parse_embeds(token) - parse_inline_footnotes(token) - parse_links(token) - pass_through_children(token) + add_breaklines(token) + } + + // VARIABLE RESOLUTION PASS + + vars: map[string]string + for child in root.children { + resolve_variables(child, &vars) } // FOURTH PASS