diff options
| author | gingerBill <gingerBill@users.noreply.github.com> | 2023-03-29 15:08:33 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-29 15:08:33 +0100 |
| commit | 2f771bee7bed7282e84ce0cf16825a9cf625f994 (patch) | |
| tree | 8f47fe922902e089e0f679885cdc65671361f87f | |
| parent | ae5214c1f256433c0d5fe718ea377335ffec2f24 (diff) | |
| parent | 8862f9118b7efa79a09e3b397f517f16ed342016 (diff) | |
Merge pull request #2412 from oskarnp/text_table
text/table: Initial implementation
| -rw-r--r-- | core/text/table/doc.odin | 100 | ||||
| -rw-r--r-- | core/text/table/table.odin | 384 | ||||
| -rw-r--r-- | core/text/table/utility.odin | 13 |
3 files changed, 497 insertions, 0 deletions
diff --git a/core/text/table/doc.odin b/core/text/table/doc.odin new file mode 100644 index 000000000..9b5c1f932 --- /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..<tbl.nr_rows { + for col in 0..<tbl.nr_cols { + write_table_cell(stdio_writer(), tbl, row, col) + } + io.write_byte(stdio_writer(), '\n') + } + ``` + + This outputs: + ``` + A_LONG_ENUM = 54, // A comment about A_LONG_ENUM + AN_EVEN_LONGER_ENUM = 1, // A comment about AN_EVEN_LONGER_ENUM + ``` + + --- + + ASCII rendering example: + + ```odin + tbl := init(&Table{}) + defer destroy(tbl) + + caption(tbl, "This is a table caption and it is very long") + + padding(tbl, 1, 1) // Left/right padding of cells + + header(tbl, "AAAAAAAAA", "B") + header(tbl, "C") // Appends to previous header row. Same as if done header("AAAAAAAAA", "B", "C") from start. + + // Create a row with two values. Since there are three columns the third + // value will become the empty string. + // + // NOTE: header() is not allowed anymore after this. + row(tbl, 123, "foo") + + // Use `format()` if you need custom formatting. This will allocate into + // the arena specified at init. + row(tbl, + format(tbl, "%09d", 5), + format(tbl, "%.6f", 6.28318530717958647692528676655900576)) + + // A row with zero values is allowed as long as a previous row or header + // exist. The value and alignment of each cell can then be set + // individually. + row(tbl) + set_cell_value_and_alignment(tbl, last_row(tbl), 0, "a", .Center) + set_cell_value(tbl, last_row(tbl), 1, "bbb") + set_cell_value(tbl, last_row(tbl), 2, "c") + + // Headers are regular cells, too. Use header_row() as row index to modify + // header cells. + set_cell_alignment(tbl, header_row(tbl), 1, .Center) // Sets alignment of 'B' column to Center. + set_cell_alignment(tbl, header_row(tbl), 2, .Right) // Sets alignment of 'C' column to Right. + + build(tbl) + + write_ascii_table(stdio_writer(), tbl) + write_markdown_table(stdio_writer(), tbl) + ``` + + This outputs: + ``` + +-----------------------------------------------+ + | This is a table caption and it is very long | + +------------------+-----------------+----------+ + | AAAAAAAAA | B | C | + +------------------+-----------------+----------+ + | 123 | foo | | + | 000000005 | 6.283185 | | + | a | bbb | c | + +------------------+-----------------+----------+ + ``` + + and + + ``` + | AAAAAAAAA | B | C | + |:-----------------|:---------------:|---------:| + | 123 | foo | | + | 000000005 | 6.283185 | | + | a | bbb | c | + ``` + + respectively. +*/ + +package text_table diff --git a/core/text/table/table.odin b/core/text/table/table.odin new file mode 100644 index 000000000..df93ee44e --- /dev/null +++ b/core/text/table/table.odin @@ -0,0 +1,384 @@ +/* + Copyright 2023 oskarnp <oskarnp@proton.me> + 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, + 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_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 + 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) + 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 in 0..<tbl.nr_cols { + 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.nr_rows { + for col in 0..<tbl.nr_cols { + cell := get_cell(tbl, row, col) + if w := len(cell.text) + tbl.lpad + tbl.rpad; w > 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, "<table>\n") + if tbl.caption != "" { + io.write_string(w, "<caption>") + io.write_string(w, tbl.caption) + io.write_string(w, "</caption>\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, "<thead>\n") + io.write_string(w, " <tr>\n") + for col in 0..<tbl.nr_cols { + cell := get_cell(tbl, header_row(tbl), col) + io.write_string(w, " <th") + io.write_string(w, align_attribute(cell)) + io.write_string(w, ">") + io.write_string(w, cell.text) + io.write_string(w, "</th>\n") + } + io.write_string(w, " </tr>\n") + io.write_string(w, "</thead>\n") + } + + io.write_string(w, "<tbody>\n") + for row in 0..<tbl.nr_rows { + if tbl.has_header_row && row == header_row(tbl) { + continue + } + io.write_string(w, " <tr>\n") + for col in 0..<tbl.nr_cols { + cell := get_cell(tbl, row, col) + io.write_string(w, " <td") + io.write_string(w, align_attribute(cell)) + io.write_string(w, ">") + io.write_string(w, cell.text) + io.write_string(w, "</td>\n") + } + io.write_string(w, " </tr>\n") + } + io.write_string(w, " </tbody>\n") + + io.write_string(w, "</table>\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..<tbl.nr_cols { + if col == 0 { + io.write_byte(w, '+') + } + write_byte_repeat(w, tbl.colw[col], '-') + io.write_byte(w, '+') + } + io.write_byte(w, '\n') + } + + if tbl.caption != "" { + write_caption_separator(w, tbl) + io.write_byte(w, '|') + write_text_align(w, tbl.tblw - tbl.lpad - tbl.rpad + tbl.nr_cols - 1, + tbl.lpad, tbl.rpad, tbl.caption, .Center) + io.write_byte(w, '|') + io.write_byte(w, '\n') + } + + write_table_separator(w, tbl) + for row in 0..<tbl.nr_rows { + for col in 0..<tbl.nr_cols { + if col == 0 { + io.write_byte(w, '|') + } + write_table_cell(w, tbl, row, col) + io.write_byte(w, '|') + } + io.write_byte(w, '\n') + if tbl.has_header_row && row == header_row(tbl) { + write_table_separator(w, tbl) + } + } + write_table_separator(w, tbl) +} + +// Renders table according to GitHub Flavored Markdown (GFM) specification +write_markdown_table :: proc(w: io.Writer, tbl: ^Table) { + // NOTE(oskar): Captions or colspans are not supported by GFM as far as I can tell. + + if tbl.dirty { + build(tbl) + } + + for row in 0..<tbl.nr_rows { + for col in 0..<tbl.nr_cols { + cell := get_cell(tbl, row, col) + if col == 0 { + io.write_byte(w, '|') + } + write_text_align(w, tbl.colw[col] - tbl.lpad - tbl.rpad, tbl.lpad, tbl.rpad, cell.text, + .Center if tbl.has_header_row && row == header_row(tbl) else .Left) + io.write_string(w, "|") + } + io.write_byte(w, '\n') + + if tbl.has_header_row && row == header_row(tbl) { + for col in 0..<tbl.nr_cols { + cell := get_cell(tbl, row, col) + if col == 0 { + io.write_byte(w, '|') + } + switch cell.alignment { + case .Left: + io.write_byte(w, ':') + 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), '-') + io.write_byte(w, ':') + case .Right: + write_byte_repeat(w, max(1, tbl.colw[col]-1), '-') + io.write_byte(w, ':') + } + io.write_byte(w, '|') + } + io.write_byte(w, '\n') + } + } +} + +write_byte_repeat :: proc(w: io.Writer, n: int, b: byte) { + for _ in 0..<n { + io.write_byte(w, b) + } +} + +write_table_cell :: proc(w: io.Writer, tbl: ^Table, row, col: int) { + if tbl.dirty { + build(tbl) + } + cell := get_cell(tbl, row, col) + write_text_align(w, tbl.colw[col]-tbl.lpad-tbl.rpad, tbl.lpad, tbl.rpad, cell.text, cell.alignment) +} + +write_text_align :: proc(w: io.Writer, colw, lpad, rpad: int, text: string, alignment: Cell_Alignment) { + write_byte_repeat(w, lpad, ' ') + switch alignment { + case .Left: + io.write_string(w, text) + write_byte_repeat(w, colw - len(text), ' ') + case .Center: + pad := colw - len(text) + odd := pad & 1 != 0 + write_byte_repeat(w, pad/2, ' ') + io.write_string(w, text) + write_byte_repeat(w, pad/2 + 1 if odd else pad/2, ' ') + case .Right: + write_byte_repeat(w, colw - len(text), ' ') + io.write_string(w, text) + } + write_byte_repeat(w, rpad, ' ') +} 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) +} |