From 772f135180ff08f626f8b74bfa876d9379e91855 Mon Sep 17 00:00:00 2001 From: "Paul W." Date: Thu, 6 Aug 2026 19:22:44 -0400 Subject: [PATCH] Make ltx cli nicer --- ltx/ltx.odin | 307 ++++++++++++++++++++++++++----------------------- ltx/types.odin | 81 +++++++++++++ 2 files changed, 245 insertions(+), 143 deletions(-) create mode 100644 ltx/types.odin diff --git a/ltx/ltx.odin b/ltx/ltx.odin index bb0cc60..0f0250c 100644 --- a/ltx/ltx.odin +++ b/ltx/ltx.odin @@ -9,102 +9,114 @@ import "core:unicode" // TODO(Paul): change the key-value format to allow for true arbitrary string values with the use of quotes (maybe?) and make the rules clean and easy to remember. main :: proc() { - if len(os.args) < 2 { - fmt.eprintf( - "A naive implementation of TeX-based arbitrary markup. Prints syntax tree and space-preserving XML substitution\nUsage:\n\t%s \n", - os.args[0], - ) - return - } + args := parse_args() + if args.type == .None do return + ltx: Ltx - parse_err := ltx_parse_file(<x, os.args[1]) - defer ltx_free_file(<x) + parse_err := parse_file(<x, args.file_path) + defer free_file(<x) if parse_err != .None { - fmt.eprintln(ltx_get_error(<x, parse_err, context.temp_allocator)) + fmt.eprintln(get_error(<x, parse_err, context.temp_allocator)) return } - for node in ltx.nodes { - print_node(node) - } sb := strings.builder_make() defer strings.builder_destroy(&sb) - fmt.println("--- BEGIN XML ---") - strings.builder_reset(&sb) - ltx_to_xml(&sb, ltx.nodes) + switch args.type { + case .None: + return + case .XML: + to_xml(&sb, ltx.nodes) + case .PlainText: + to_plain_text(&sb, ltx.nodes) + case .SmartIndentedText: + to_text_with_smart_indent(&sb, ltx.nodes) + } + fmt.println(strings.to_string(sb)) - fmt.println("--- END XML ---") - fmt.println("--- BEGIN PLAIN TEXT ---") - strings.builder_reset(&sb) - ltx_to_text_with_smart_indent(&sb, ltx.nodes) - fmt.println(strings.to_string(sb)) - fmt.println("--- END PLAIN TEXT ---") + // for node in ltx.nodes { + // print_node(node) + // } + + // sb := strings.builder_make() + // defer strings.builder_destroy(&sb) + + // fmt.println("--- BEGIN XML ---") + // strings.builder_reset(&sb) + // to_xml(&sb, ltx.nodes) + // fmt.println(strings.to_string(sb)) + // fmt.println("--- END XML ---") + + // fmt.println("--- BEGIN PLAIN TEXT ---") + // strings.builder_reset(&sb) + // to_text_with_smart_indent(&sb, ltx.nodes) + // fmt.println(strings.to_string(sb)) + // fmt.println("--- END PLAIN TEXT ---") } -Tokens :: enum { - Backslash, - LeftBrace, - RightBrace, - LeftBracket, - RightBracket, - Assign, - Quote, +print_help :: proc() { + fmt.eprintln( + "A naive implementation of TeX-based arbitrary markup. Prints syntax tree and space-preserving XML substitution", + ) + fmt.eprintln("Usage:") + fmt.eprintf("\t%s \n", os.args[0]) } -TokenArray := [Tokens]rune { - .Assign = '=', - .Backslash = '\\', - .LeftBrace = '{', - .RightBrace = '}', - .LeftBracket = '[', - .RightBracket = ']', - .Quote = '"', +parse_args :: proc() -> Cli_Args { + args := Cli_Args{} + + if len(os.args) < 2 { + print_help() + return args + } + + output_flags_used := 0 + output_type := Cli_Output_Type.None + file_path: string + + for arg in os.args[1:] { + if strings.starts_with(arg, "--") { + output_flags_used += 1 + switch arg[2:] { + case "xml": + output_type = .XML + case "text": + output_type = .PlainText + case "smart": + output_type = .SmartIndentedText + case: + fmt.eprintln("Unknown flag:", arg) + print_help() + return args + } + } else { + if file_path != "" { + fmt.eprintln("Unknown argument:", arg) + print_help() + return args + } + file_path = arg + } + } + + if output_flags_used > 1 { + fmt.eprintln("Too many flags passed") + print_help() + } else { + args.file_path = file_path + args.type = output_flags_used == 0 ? .PlainText : output_type + } + + return args } -Node_Kind :: enum { - Text, - Tag, -} - -Attr_Type :: enum { - Flag, - Attribute, -} - -Field :: struct { - value: string, - type: Attr_Type, -} - -Node :: struct { - name: string, - kind: Node_Kind, - text: string, - attributes: map[string]Field, - children: [dynamic]Node, -} - -Ltx_Error :: enum u32 { - None = 0, - EOF, - KeyExpected, - ClosingBracketExpected, - ClosingBraceExpected, - UnexpectedRightBrace, - ValueExpected, - KeyAlreadyExists, - InvalidKey, - CannotReadFile, - // InvalidKeyStart, -} - -ltx_seek :: proc(ltx: ^Ltx) -> (rune, Ltx_Error) { +seek :: proc(ltx: ^Ltx) -> (rune, Ltx_Error) { next_idx := ltx.idx + 1 if next_idx >= len(ltx.source) do return 0, .EOF - if ltx_current_char(ltx) == '\n' { + if current_char(ltx) == '\n' { ltx.pos = { col = 0, line = ltx.pos.line + 1, @@ -113,26 +125,26 @@ ltx_seek :: proc(ltx: ^Ltx) -> (rune, Ltx_Error) { ltx.pos.col += 1 } ltx.idx = next_idx - c := ltx_current_char(ltx) + c := current_char(ltx) return c, .None } -ltx_has_next :: proc(ltx: ^Ltx) -> b32 { +has_next :: proc(ltx: ^Ltx) -> b32 { return len(ltx.source) > ltx.idx + 1 } -ltx_peek :: proc(ltx: ^Ltx) -> (rune, Ltx_Error) { - if !ltx_has_next(ltx) do return 0, .EOF +peek :: proc(ltx: ^Ltx) -> (rune, Ltx_Error) { + if !has_next(ltx) do return 0, .EOF return rune(ltx.source[ltx.idx + 1]), .None } -ltx_current_char :: proc(ltx: ^Ltx) -> rune { +current_char :: proc(ltx: ^Ltx) -> rune { assert(ltx.idx < len(ltx.source), "index cannot be greater than string length") return rune(ltx.source[ltx.idx]) } -ltx_consume_whitespace :: proc(ltx: ^Ltx) -> Ltx_Error { - for unicode.is_white_space(ltx_current_char(ltx)) do ltx_seek(ltx) or_return // TODO: do prop error handling +consume_whitespace :: proc(ltx: ^Ltx) -> Ltx_Error { + for unicode.is_white_space(current_char(ltx)) do seek(ltx) or_return // TODO: do prop error handling return .None } @@ -145,25 +157,14 @@ validate_key :: proc(key: string) -> Ltx_Error { return .None } -Ltx :: struct { - source: string, - source_path: string, - nodes: [dynamic]Node, - idx: int, - pos: struct { - line: u32, - col: u32, - }, -} - -ltx_parse :: proc(ltx: ^Ltx) -> (err: Ltx_Error) { +parse :: proc(ltx: ^Ltx) -> (err: Ltx_Error) { ltx.idx = 0 stack := make([dynamic]Node) defer delete(stack) text_node_start_idx := 0 - for ltx_has_next(ltx) { - if ltx_current_char(ltx) == TokenArray[.Backslash] { + for has_next(ltx) { + if current_char(ltx) == TokenArray[.Backslash] { // TODO: cehck if the char after slash is a token if text_node_start_idx < ltx.idx { node := Node { @@ -173,10 +174,10 @@ ltx_parse :: proc(ltx: ^Ltx) -> (err: Ltx_Error) { if len(stack) == 0 do append(<x.nodes, node) else do append(&stack[len(stack) - 1].children, node) } - ltx_seek(ltx) or_return // skip \ + seek(ltx) or_return // skip \ tag_start_idx := ltx.idx - for unicode.is_letter(ltx_current_char(ltx)) do ltx_seek(ltx) or_return + for unicode.is_letter(current_char(ltx)) do seek(ltx) or_return assert(tag_start_idx < ltx.idx) tag_name := ltx.source[tag_start_idx:ltx.idx] node := Node { @@ -193,18 +194,18 @@ ltx_parse :: proc(ltx: ^Ltx) -> (err: Ltx_Error) { content = 0, } - ltx_consume_whitespace(ltx) or_return - for ltx_current_char(ltx) == TokenArray[.LeftBracket] { - c := ltx_seek(ltx) or_return + consume_whitespace(ltx) or_return + for current_char(ltx) == TokenArray[.LeftBracket] { + c := seek(ltx) or_return field_start := ltx.idx key: string - for ltx_has_next(ltx) { + for has_next(ltx) { if c == TokenArray[.Assign] { if field_start >= ltx.idx do return .KeyExpected key = strings.trim(ltx.source[field_start:ltx.idx], " \t") if len(key) <= 0 do return .KeyExpected validate_key(key) or_return - ltx_seek(ltx) or_return + seek(ltx) or_return field_start = ltx.idx } else if c == TokenArray[.RightBracket] { if field_start >= ltx.idx do return .ValueExpected @@ -220,17 +221,17 @@ ltx_parse :: proc(ltx: ^Ltx) -> (err: Ltx_Error) { } _, _, found := map_upsert(&node.attributes, key, value) if found do return .KeyAlreadyExists - ltx_seek(ltx) or_return + seek(ltx) or_return end_pos.fields = ltx.idx break } - c = ltx_seek(ltx) or_return + c = seek(ltx) or_return } - ltx_consume_whitespace(ltx) or_return + consume_whitespace(ltx) or_return } - ltx_consume_whitespace(ltx) or_return - if ltx_current_char(ltx) == TokenArray[.LeftBrace] { - ltx_seek(ltx) or_return // consume { + consume_whitespace(ltx) or_return + if current_char(ltx) == TokenArray[.LeftBrace] { + seek(ltx) or_return // consume { append(&stack, node) end_pos.content = ltx.idx } else { @@ -239,7 +240,7 @@ ltx_parse :: proc(ltx: ^Ltx) -> (err: Ltx_Error) { if end_pos.content != 0 do text_node_start_idx = end_pos.content else if end_pos.fields != 0 do text_node_start_idx = end_pos.fields else do text_node_start_idx = end_pos.tag_name - } else if ltx_current_char(ltx) == TokenArray[.RightBrace] { + } else if current_char(ltx) == TokenArray[.RightBrace] { if len(stack) <= 0 do return .UnexpectedRightBrace node := pop(&stack) if text_node_start_idx < ltx.idx { @@ -254,10 +255,10 @@ ltx_parse :: proc(ltx: ^Ltx) -> (err: Ltx_Error) { } else { append(<x.nodes, node) } - ltx_seek(ltx) or_return // consume } + seek(ltx) or_return // consume } text_node_start_idx = ltx.idx } else { - ltx_seek(ltx) or_return + seek(ltx) or_return } } if text_node_start_idx < ltx.idx { @@ -267,17 +268,17 @@ ltx_parse :: proc(ltx: ^Ltx) -> (err: Ltx_Error) { return .None } -ltx_parse_file :: proc(ltx: ^Ltx, file_path: string) -> Ltx_Error { - source, err := os.read_entire_file_from_path(file_path, context.allocator) +parse_file :: proc(ltx: ^Ltx, file_path: string, allocator := context.allocator) -> Ltx_Error { + source, err := os.read_entire_file_from_path(file_path, allocator) if err != nil do return .CannotReadFile abs_path, abs_err := filepath.abs(file_path) if abs_err != nil do return .CannotReadFile ltx.source_path = abs_path ltx.source = string(source) - return ltx_parse(ltx) + return parse(ltx) } -ltx_free_file :: proc(ltx: ^Ltx) -> runtime.Allocator_Error { +free_file :: proc(ltx: ^Ltx) -> runtime.Allocator_Error { delete(ltx.source) or_return delete(ltx.source_path) or_return return .None @@ -314,7 +315,7 @@ print_node :: proc(node: Node, indent_level := 0) { } } -ltx_to_text_with_smart_indent :: proc (sb: ^strings.Builder, nodes: [dynamic]Node, indent_level := 0) { +to_text_with_smart_indent :: proc(sb: ^strings.Builder, nodes: [dynamic]Node, indent_level := 0) { should_inc_indent_level := true for node in nodes { if node.kind == .Text { @@ -326,32 +327,40 @@ ltx_to_text_with_smart_indent :: proc (sb: ^strings.Builder, nodes: [dynamic]Nod for node in nodes { switch node.kind { case .Text: - for i in 0..") for node in nodes { @@ -363,18 +372,18 @@ ltx_to_xml :: proc(sb: ^strings.Builder, nodes: [dynamic]Node, depth := 0) { for k, v in node.attributes do fmt.sbprintf(sb, " %s=\"%s\"", k, v.value) if len(node.children) > 0 { fmt.sbprint(sb, ">") - ltx_to_xml(sb, node.children, depth + 1) + to_xml(sb, node.children, depth + 1) fmt.sbprintf(sb, "", node.name) } else { fmt.sbprint(sb, " />") } } } - if depth == 0 do fmt.sbprintln(sb, "") + if depth == 0 do fmt.sbprint(sb, "\n") } // Ltx_Error to english string -ltx_error_to_string :: proc(error: Ltx_Error) -> string { +error_to_string :: proc(error: Ltx_Error) -> string { switch error { case .None: return "" @@ -401,29 +410,41 @@ ltx_error_to_string :: proc(error: Ltx_Error) -> string { } // Pretty format LTX error -ltx_get_error :: proc(ltx: ^Ltx, error: Ltx_Error, allocator := context.allocator) -> string { +get_error :: proc(ltx: ^Ltx, error: Ltx_Error, allocator := context.allocator) -> string { if error == .None do return "" - file_path := len(ltx.source_path) > 0 ? ltx.source_path : "[source]" + file_path := len(ltx.source_path) > 0 ? ltx.source_path : "" line := ltx.pos.line + 1 col := ltx.pos.col + 1 - error_msg := ltx_error_to_string(error) - return fmt.aprintf("%s(%d,%d): error: %s", file_path, line, col, error_msg, allocator = allocator) + error_msg := error_to_string(error) + return fmt.aprintf( + "%s(%d,%d): error: %s", + file_path, + line, + col, + error_msg, + allocator = allocator, + ) } // Escapes white space characters with in their c-string slash form escape_white_space :: proc(s: string, allocator := context.allocator) -> string { - sb := strings.builder_make(allocator) + sb := strings.builder_make(allocator) - for c in s { - switch c { - case '\t': strings.write_string(&sb, "\\t") - case '\n': strings.write_string(&sb, "\\n") - case '\r': strings.write_string(&sb, "\\r") - case '\\': strings.write_string(&sb, "\\\\") - case: strings.write_rune(&sb, c) - } - } - return strings.to_string(sb) + for c in s { + switch c { + case '\t': + strings.write_string(&sb, "\\t") + case '\n': + strings.write_string(&sb, "\\n") + case '\r': + strings.write_string(&sb, "\\r") + case '\\': + strings.write_string(&sb, "\\\\") + case: + strings.write_rune(&sb, c) + } + } + return strings.to_string(sb) } // Processes and normalizes whitespace in a multi-line string. diff --git a/ltx/types.odin b/ltx/types.odin new file mode 100644 index 0000000..aacf65a --- /dev/null +++ b/ltx/types.odin @@ -0,0 +1,81 @@ +package ltx + +Ltx :: struct { + source: string, + source_path: string, + nodes: [dynamic]Node, + idx: int, + pos: struct { + line: u32, + col: u32, + }, +} + +Tokens :: enum { + Backslash, + LeftBrace, + RightBrace, + LeftBracket, + RightBracket, + Assign, + Quote, +} + +TokenArray := [Tokens]rune { + .Assign = '=', + .Backslash = '\\', + .LeftBrace = '{', + .RightBrace = '}', + .LeftBracket = '[', + .RightBracket = ']', + .Quote = '"', +} + +Node_Kind :: enum { + Text, + Tag, +} + +Attr_Type :: enum { + Flag, + Attribute, +} + +Field :: struct { + value: string, + type: Attr_Type, +} + +Node :: struct { + name: string, + kind: Node_Kind, + text: string, + attributes: map[string]Field, + children: [dynamic]Node, +} + +Ltx_Error :: enum u32 { + None = 0, + EOF, + KeyExpected, + ClosingBracketExpected, + ClosingBraceExpected, + UnexpectedRightBrace, + ValueExpected, + KeyAlreadyExists, + InvalidKey, + CannotReadFile, + // InvalidKeyStart, +} + +Cli_Output_Type :: enum { + None, + PlainText, + XML, + SmartIndentedText, +} + +Cli_Args :: struct { + type: Cli_Output_Type, + file_path: string, +}