From 88ee5d1a6de96cb45951eed936df9e7bf9c8022c Mon Sep 17 00:00:00 2001 From: oskarnp Date: Sun, 26 Mar 2023 16:10:27 +0200 Subject: [PATCH 1/8] text/table: Initial implementation --- core/text/table/doc.odin | 100 ++++++++++ core/text/table/table.odin | 399 +++++++++++++++++++++++++++++++++++++ 2 files changed, 499 insertions(+) create mode 100644 core/text/table/doc.odin create mode 100644 core/text/table/table.odin diff --git a/core/text/table/doc.odin b/core/text/table/doc.odin new file mode 100644 index 000000000..00bde5862 --- /dev/null +++ b/core/text/table/doc.odin @@ -0,0 +1,100 @@ +/* + package table implements ascii/markdown/html/custom rendering of tables. + + --- + + Custom rendering example: + + ```odin + tbl := init(&Table{}) + padding(tbl, 0, 1) + row(tbl, "A_LONG_ENUM", "= 54,", "// A comment about A_LONG_ENUM") + row(tbl, "AN_EVEN_LONGER_ENUM", "= 1,", "// A comment about AN_EVEN_LONGER_ENUM") + build(tbl) + for row in 0.. + Made available under Odin's BSD-3 license. + + List of contributors: + oskarnp: Initial implementation. +*/ + +package text_table + +import "core:io" +import "core:os" +import "core:fmt" +import "core:mem" +import "core:mem/virtual" +import "core:runtime" +import "core:strings" + +Cell :: struct { + text: string, + alignment: Cell_Alignment, +} + +Cell_Alignment :: enum { + Left = 1, + Center, + Right, +} + +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 + + dirty: bool, // True if build() needs to be called before rendering + + // The following are computed on build() + colw: [dynamic]int, // Width of each column (including padding, excluding borders) + tblw: int, // Width of entire table (including padding, excluding borders) +} + +init :: proc{_init, _init_with_virtual_arena, _init_with_mem_arena} + +_init :: 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(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(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 + tbl.dirty = true +} + +padding :: proc(tbl: ^Table, lpad, rpad: int) { + tbl.lpad = lpad + tbl.rpad = rpad + tbl.dirty = true +} + +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] +} + +set_cell_value_and_alignment :: proc(tbl: ^Table, row, col: int, value: string, alignment: Cell_Alignment) { + cell := get_cell(tbl, row, col) + cell.text = format(tbl, "%v", value) + cell.alignment = alignment + tbl.dirty = true +} + +set_cell_value :: proc(tbl: ^Table, row, col: int, value: any, loc := #caller_location) { + cell := get_cell(tbl, row, col, loc) + if cell.alignment == nil { + cell.alignment = .Left + } + switch val in value { + case nil: + cell.text = "" + case string: + cell.text = string(val) + case cstring: + cell.text = string(val) + case: + cell.text = format(tbl, "%v", val) + if cell.text == "" { + fmt.eprintf("{} text/table: format() resulted in empty string (arena out of memory?)\n", loc) + } + } + tbl.dirty = true +} + +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 + tbl.dirty = true +} + +format :: proc(tbl: ^Table, _fmt: string, args: ..any, loc := #caller_location) -> string { + context.allocator = tbl.format_allocator + return fmt.aprintf(fmt = _fmt, args = args) +} + +header :: 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 + } + + tbl.dirty = true +} + +row :: proc(tbl: ^Table, values: ..any, loc := #caller_location) { + if tbl.nr_cols == 0 { + if len(values) == 0 { + panic("Cannot create row without values unless knowing amount of columns in advance") + } else { + tbl.nr_cols = len(values) + } + } + tbl.nr_rows += 1 + for col := 0; col < tbl.nr_cols; col += 1 { + val := values[col] if col < len(values) else nil + set_cell_value(tbl, last_row(tbl), col, val) + } + tbl.dirty = true +} + +last_row :: proc(tbl: ^Table) -> 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) { + tbl.dirty = false + + resize(&tbl.colw, tbl.nr_cols) + mem.zero_slice(tbl.colw[:]) + + for row in 0.. tbl.colw[col] { + tbl.colw[col] = w + } + } + } + + colw_sum := 0 + for v in tbl.colw { + colw_sum += v + } + + tbl.tblw = max(colw_sum, len(tbl.caption) + tbl.lpad + tbl.rpad) + + // Resize columns to match total width of table + remain := tbl.tblw-colw_sum + for col := 0; remain > 0; col = (col + 1) % tbl.nr_cols { + tbl.colw[col] += 1 + remain -= 1 + } + + return +} + +write_html_table :: proc(w: io.Writer, tbl: ^Table) { + if tbl.dirty { + build(tbl) + } + + io.write_string(w, "\n") + if tbl.caption != "" { + io.write_string(w, "\n") + } + + align_attribute :: proc(cell: ^Cell) -> string { + #partial switch cell.alignment { + case .Left: return ` align="left"` + case .Center: return ` align="center"` + case .Right: return ` align="right"` + } + return "" + } + + if tbl.has_header_row { + io.write_string(w, "\n") + io.write_string(w, " \n") + for col in 0..") + io.write_string(w, cell.text) + io.write_string(w, "\n") + } + io.write_string(w, " \n") + io.write_string(w, "\n") + } + + io.write_string(w, "\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, " \n") + } + io.write_string(w, " \n") + + io.write_string(w, "
") + io.write_string(w, tbl.caption) + io.write_string(w, "
\n") +} + +write_ascii_table :: proc(w: io.Writer, tbl: ^Table) { + if tbl.dirty { + build(tbl) + } + + 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.. io.Writer { + return io.to_writer(os.stream_from_handle(os.stdout)) +} + +strings_builder_writer :: proc(b: ^strings.Builder) -> io.Writer { + return strings.to_writer(b) +} From 020b14722208c55d0cf1d3999166ae31f1f71df4 Mon Sep 17 00:00:00 2001 From: oskarnp Date: Sun, 26 Mar 2023 21:33:27 +0200 Subject: [PATCH 2/8] Move helper procs into utility.odin --- core/text/table/table.odin | 8 -------- core/text/table/utility.odin | 13 +++++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 core/text/table/utility.odin diff --git a/core/text/table/table.odin b/core/text/table/table.odin index c15a92bae..bd72455aa 100644 --- a/core/text/table/table.odin +++ b/core/text/table/table.odin @@ -389,11 +389,3 @@ write_text_align :: proc(w: io.Writer, colw, lpad, rpad: int, text: string, alig } write_byte_repeat(w, rpad, ' ') } - -stdio_writer :: proc() -> io.Writer { - return io.to_writer(os.stream_from_handle(os.stdout)) -} - -strings_builder_writer :: proc(b: ^strings.Builder) -> io.Writer { - return strings.to_writer(b) -} diff --git a/core/text/table/utility.odin b/core/text/table/utility.odin new file mode 100644 index 000000000..0e56fd968 --- /dev/null +++ b/core/text/table/utility.odin @@ -0,0 +1,13 @@ +package text_table + +import "core:io" +import "core:os" +import "core:strings" + +stdio_writer :: proc() -> io.Writer { + return io.to_writer(os.stream_from_handle(os.stdout)) +} + +strings_builder_writer :: proc(b: ^strings.Builder) -> io.Writer { + return strings.to_writer(b) +} From b6d4853a338b4b0058fd8e07fa085edf65c7545a Mon Sep 17 00:00:00 2001 From: oskarnp Date: Sun, 26 Mar 2023 21:41:16 +0200 Subject: [PATCH 3/8] Fix cell alignment to default to Left using ZII --- core/text/table/table.odin | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/core/text/table/table.odin b/core/text/table/table.odin index bd72455aa..3bb92f7f6 100644 --- a/core/text/table/table.odin +++ b/core/text/table/table.odin @@ -22,7 +22,7 @@ Cell :: struct { } Cell_Alignment :: enum { - Left = 1, + Left, Center, Right, } @@ -92,9 +92,6 @@ set_cell_value_and_alignment :: proc(tbl: ^Table, row, col: int, value: string, set_cell_value :: proc(tbl: ^Table, row, col: int, value: any, loc := #caller_location) { cell := get_cell(tbl, row, col, loc) - if cell.alignment == nil { - cell.alignment = .Left - } switch val in value { case nil: cell.text = "" @@ -345,8 +342,6 @@ write_markdown_table :: proc(w: io.Writer, tbl: ^Table) { case .Right: write_byte_repeat(w, max(1, tbl.colw[col]-1, tbl.lpad, tbl.rpad), '-') io.write_byte(w, ':') - case: - panic("unhandled cell alignment") } io.write_byte(w, '|') } @@ -384,8 +379,6 @@ write_text_align :: proc(w: io.Writer, colw, lpad, rpad: int, text: string, alig case .Right: write_byte_repeat(w, colw - len(text), ' ') io.write_string(w, text) - case: - panic("unhandled alignment") } write_byte_repeat(w, rpad, ' ') } From 2da81a4a26eac631eeb93bb220c4e7a6bbaac4d3 Mon Sep 17 00:00:00 2001 From: oskarnp Date: Sun, 26 Mar 2023 21:44:31 +0200 Subject: [PATCH 4/8] Remove unnecessary C style loop --- core/text/table/table.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/text/table/table.odin b/core/text/table/table.odin index 3bb92f7f6..a5203fcd0 100644 --- a/core/text/table/table.odin +++ b/core/text/table/table.odin @@ -148,7 +148,7 @@ row :: proc(tbl: ^Table, values: ..any, loc := #caller_location) { } } tbl.nr_rows += 1 - for col := 0; col < tbl.nr_cols; col += 1 { + for col in 0.. Date: Sun, 26 Mar 2023 21:45:37 +0200 Subject: [PATCH 5/8] Remove unnecessary #partial switch --- core/text/table/table.odin | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/text/table/table.odin b/core/text/table/table.odin index a5203fcd0..65ce5e423 100644 --- a/core/text/table/table.odin +++ b/core/text/table/table.odin @@ -212,12 +212,12 @@ write_html_table :: proc(w: io.Writer, tbl: ^Table) { } align_attribute :: proc(cell: ^Cell) -> string { - #partial switch cell.alignment { + switch cell.alignment { case .Left: return ` align="left"` case .Center: return ` align="center"` case .Right: return ` align="right"` } - return "" + unreachable() } if tbl.has_header_row { From 51f295caccd2c963c61d8e9252b2c37f7727ba51 Mon Sep 17 00:00:00 2001 From: oskarnp Date: Sun, 26 Mar 2023 21:46:36 +0200 Subject: [PATCH 6/8] Rename init procs --- core/text/table/table.odin | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/text/table/table.odin b/core/text/table/table.odin index 65ce5e423..44042cbce 100644 --- a/core/text/table/table.odin +++ b/core/text/table/table.odin @@ -43,20 +43,20 @@ Table :: struct { tblw: int, // Width of entire table (including padding, excluding borders) } -init :: proc{_init, _init_with_virtual_arena, _init_with_mem_arena} +init :: proc{init_with_allocator, init_with_virtual_arena, init_with_mem_arena} -_init :: proc(tbl: ^Table, format_allocator := context.temp_allocator, table_allocator := context.allocator) -> ^Table { +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(tbl, virtual.arena_allocator(format_arena), table_allocator) +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(tbl, mem.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) { From e2e98672bda84f64fc72f9602127e85097d0ffed Mon Sep 17 00:00:00 2001 From: oskarnp Date: Sun, 26 Mar 2023 21:51:57 +0200 Subject: [PATCH 7/8] Fix typo --- core/text/table/table.odin | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/text/table/table.odin b/core/text/table/table.odin index 44042cbce..df93ee44e 100644 --- a/core/text/table/table.odin +++ b/core/text/table/table.odin @@ -334,13 +334,13 @@ write_markdown_table :: proc(w: io.Writer, tbl: ^Table) { switch cell.alignment { case .Left: io.write_byte(w, ':') - write_byte_repeat(w, max(1, tbl.colw[col]-1, tbl.lpad, tbl.rpad), '-') + write_byte_repeat(w, max(1, tbl.colw[col]-1), '-') case .Center: io.write_byte(w, ':') - write_byte_repeat(w, max(1, tbl.colw[col]-2, tbl.lpad, tbl.rpad), '-') + write_byte_repeat(w, max(1, tbl.colw[col]-2), '-') io.write_byte(w, ':') case .Right: - write_byte_repeat(w, max(1, tbl.colw[col]-1, tbl.lpad, tbl.rpad), '-') + write_byte_repeat(w, max(1, tbl.colw[col]-1), '-') io.write_byte(w, ':') } io.write_byte(w, '|') From 8862f9118b7efa79a09e3b397f517f16ed342016 Mon Sep 17 00:00:00 2001 From: oskarnp Date: Mon, 27 Mar 2023 09:31:24 +0200 Subject: [PATCH 8/8] Fix typos in doc --- core/text/table/doc.odin | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/text/table/doc.odin b/core/text/table/doc.odin index 00bde5862..9b5c1f932 100644 --- a/core/text/table/doc.odin +++ b/core/text/table/doc.odin @@ -11,8 +11,8 @@ row(tbl, "A_LONG_ENUM", "= 54,", "// A comment about A_LONG_ENUM") row(tbl, "AN_EVEN_LONGER_ENUM", "= 1,", "// A comment about AN_EVEN_LONGER_ENUM") build(tbl) - for row in 0..