/* Copyright 2023 oskarnp Made available under Odin's BSD-3 license. List of contributors: oskarnp: Initial implementation. Feoramund: Unicode support. */ package text_table import "core:io" import "core:fmt" import "core:log" import "core:mem" import "core:mem/virtual" import "core:unicode/utf8" import "base:runtime" Cell :: struct { text: string, width: int, alignment: Cell_Alignment, } Cell_Alignment :: enum { Left, Center, Right, } Aligned_Value :: struct { alignment: Cell_Alignment, value: any, } // Determines the width of a string used in the table for alignment purposes. Width_Proc :: #type proc(str: string) -> int Table :: struct { lpad, rpad: int, // Cell padding (left/right) cells: [dynamic]Cell, caption: string, nr_rows, nr_cols: int, has_header_row: bool, table_allocator: runtime.Allocator, // Used for allocating cells/colw format_allocator: runtime.Allocator, // Used for allocating Cell.text when applicable // The following are computed on build() colw: [dynamic]int, // Width of each column (excluding padding and borders) tblw: int, // Width of entire table (including padding, excluding borders) } Decorations :: struct { // These are strings, because of multi-codepoint Unicode graphemes. // Connecting decorations: nw, n, ne, w, x, e, sw, s, se: string, // Straight lines: vert: string, horz: string, } ascii_width_proc :: proc(str: string) -> int { return len(str) } unicode_width_proc :: proc(str: string) -> (width: int) { _, _, width = #force_inline utf8.grapheme_count(str) return } init :: proc{init_with_allocator, init_with_virtual_arena, init_with_mem_arena} init_with_allocator :: proc(tbl: ^Table, format_allocator := context.temp_allocator, table_allocator := context.allocator) -> ^Table { tbl.table_allocator = table_allocator tbl.cells = make([dynamic]Cell, tbl.table_allocator) tbl.colw = make([dynamic]int, tbl.table_allocator) tbl.format_allocator = format_allocator return tbl } init_with_virtual_arena :: proc(tbl: ^Table, format_arena: ^virtual.Arena, table_allocator := context.allocator) -> ^Table { return init_with_allocator(tbl, virtual.arena_allocator(format_arena), table_allocator) } init_with_mem_arena :: proc(tbl: ^Table, format_arena: ^mem.Arena, table_allocator := context.allocator) -> ^Table { return init_with_allocator(tbl, mem.arena_allocator(format_arena), table_allocator) } destroy :: proc(tbl: ^Table) { free_all(tbl.format_allocator) delete(tbl.cells) delete(tbl.colw) } caption :: proc(tbl: ^Table, value: string) { tbl.caption = value } padding :: proc(tbl: ^Table, lpad, rpad: int) { tbl.lpad = lpad tbl.rpad = rpad } get_cell :: proc(tbl: ^Table, row, col: int, loc := #caller_location) -> ^Cell { assert(col >= 0 && col < tbl.nr_cols, "cell column out of range", loc) assert(row >= 0 && row < tbl.nr_rows, "cell row out of range", loc) resize(&tbl.cells, tbl.nr_cols * tbl.nr_rows) return &tbl.cells[row*tbl.nr_cols + col] } @private to_string :: #force_inline proc(tbl: ^Table, value: any, loc := #caller_location) -> (result: string) { switch val in value { case nil: result = "" case string: result = val case cstring: result = cast(string)val case: result = format(tbl, "%v", val) if result == "" { log.error("text/table.format() resulted in empty string (arena out of memory?)", location = loc) } } return } set_cell_value :: proc(tbl: ^Table, row, col: int, value: any, loc := #caller_location) { cell := get_cell(tbl, row, col, loc) cell.text = to_string(tbl, value, loc) } set_cell_alignment :: proc(tbl: ^Table, row, col: int, alignment: Cell_Alignment, loc := #caller_location) { cell := get_cell(tbl, row, col, loc) cell.alignment = alignment } set_cell_value_and_alignment :: proc(tbl: ^Table, row, col: int, value: any, alignment: Cell_Alignment, loc := #caller_location) { cell := get_cell(tbl, row, col, loc) cell.text = to_string(tbl, value, loc) cell.alignment = alignment } format :: proc(tbl: ^Table, _fmt: string, args: ..any) -> string { context.allocator = tbl.format_allocator return fmt.aprintf(_fmt, ..args) } header :: header_of_values header_of_values :: proc(tbl: ^Table, values: ..any, loc := #caller_location) { if (tbl.has_header_row && tbl.nr_rows != 1) || (!tbl.has_header_row && tbl.nr_rows != 0) { panic("Cannot add headers after rows have been added", loc) } if tbl.nr_rows == 0 { tbl.nr_rows += 1 tbl.has_header_row = true } col := tbl.nr_cols tbl.nr_cols += len(values) for val in values { set_cell_value(tbl, header_row(tbl), col, val, loc) col += 1 } } aligned_header_of_values :: proc(tbl: ^Table, alignment: Cell_Alignment, values: ..any, loc := #caller_location) { if (tbl.has_header_row && tbl.nr_rows != 1) || (!tbl.has_header_row && tbl.nr_rows != 0) { panic("Cannot add headers after rows have been added", loc) } if tbl.nr_rows == 0 { tbl.nr_rows += 1 tbl.has_header_row = true } col := tbl.nr_cols tbl.nr_cols += len(values) for val in values { set_cell_value_and_alignment(tbl, header_row(tbl), col, val, alignment, loc) col += 1 } } header_of_aligned_values :: proc(tbl: ^Table, aligned_values: []Aligned_Value, loc := #caller_location) { if (tbl.has_header_row && tbl.nr_rows != 1) || (!tbl.has_header_row && tbl.nr_rows != 0) { panic("Cannot add headers after rows have been added", loc) } if tbl.nr_rows == 0 { tbl.nr_rows += 1 tbl.has_header_row = true } col := tbl.nr_cols tbl.nr_cols += len(aligned_values) for av in aligned_values { set_cell_value_and_alignment(tbl, header_row(tbl), col, av.value, av.alignment, loc) col += 1 } } row :: row_of_values row_of_values :: proc(tbl: ^Table, values: ..any, loc := #caller_location) { if tbl.nr_cols == 0 { if len(values) == 0 { panic("Cannot create empty row unless the number of columns is known in advance") } else { tbl.nr_cols = len(values) } } tbl.nr_rows += 1 for col in 0.. int { return tbl.nr_rows - 1 } header_row :: proc(tbl: ^Table) -> int { return 0 if tbl.has_header_row else -1 } first_row :: proc(tbl: ^Table) -> int { return header_row(tbl)+1 if tbl.has_header_row else 0 } build :: proc(tbl: ^Table, width_proc: Width_Proc) { resize(&tbl.colw, tbl.nr_cols) mem.zero_slice(tbl.colw[:]) for row in 0.. 0; col = (col + 1) % tbl.nr_cols { tbl.colw[col] += 1 remain -= 1 } return } write_html_table :: proc(w: io.Writer, tbl: ^Table) { io.write_string(w, "\n") if tbl.caption != "" { io.write_string(w, "\t\n") } align_attribute :: proc(cell: ^Cell) -> string { switch cell.alignment { case .Left: return ` align="left"` case .Center: return ` align="center"` case .Right: return ` align="right"` } unreachable() } if tbl.has_header_row { io.write_string(w, "\t\n") io.write_string(w, "\t\t\n") for col in 0..") io.write_string(w, cell.text) io.write_string(w, "\n") } io.write_string(w, "\t\t\n") io.write_string(w, "\t\n") } io.write_string(w, "\t\n") for row in 0..\n") for col in 0..") io.write_string(w, cell.text) io.write_string(w, "\n") } io.write_string(w, "\t\t\n") } io.write_string(w, "\t\n") io.write_string(w, "
") io.write_string(w, tbl.caption) io.write_string(w, "
\n") } write_plain_table :: proc(w: io.Writer, tbl: ^Table, width_proc: Width_Proc = unicode_width_proc) { build(tbl, width_proc) write_caption_separator :: proc(w: io.Writer, tbl: ^Table) { io.write_byte(w, '+') write_byte_repeat(w, tbl.tblw + tbl.nr_cols - 1, '-') io.write_byte(w, '+') io.write_byte(w, '\n') } write_table_separator :: proc(w: io.Writer, tbl: ^Table) { for col in 0..