diff options
Diffstat (limited to 'core/text/table/table.odin')
| -rw-r--r-- | core/text/table/table.odin | 292 |
1 files changed, 195 insertions, 97 deletions
diff --git a/core/text/table/table.odin b/core/text/table/table.odin index 5423519d3..7b4942478 100644 --- a/core/text/table/table.odin +++ b/core/text/table/table.odin @@ -4,18 +4,22 @@ 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, } @@ -25,6 +29,14 @@ Cell_Alignment :: enum { 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, @@ -34,13 +46,20 @@ Table :: struct { 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) + colw: [dynamic]int, // Width of each column (excluding padding and borders) tblw: int, // Width of entire table (including padding, excluding borders) } +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 { @@ -65,13 +84,11 @@ destroy :: proc(tbl: ^Table) { 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 { @@ -81,43 +98,47 @@ get_cell :: proc(tbl: ^Table, row, col: int, loc := #caller_location) -> ^Cell { 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) +@private +to_string :: #force_inline proc(tbl: ^Table, value: any, loc := #caller_location) -> (result: string) { switch val in value { case nil: - cell.text = "" + result = "" case string: - cell.text = string(val) + result = val case cstring: - cell.text = string(val) + result = cast(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) + result = format(tbl, "%v", val) + if result == "" { + log.error("text/table.format() resulted in empty string (arena out of memory?)", location = loc) } } - tbl.dirty = true + 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 - tbl.dirty = true } -format :: proc(tbl: ^Table, _fmt: string, args: ..any, loc := #caller_location) -> string { +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 :: proc(tbl: ^Table, values: ..any, loc := #caller_location) { +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) } @@ -133,26 +154,108 @@ header :: proc(tbl: ^Table, values: ..any, loc := #caller_location) { 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 - tbl.dirty = true + 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, loc) + } } -row :: proc(tbl: ^Table, values: ..any, loc := #caller_location) { +aligned_row_of_values :: proc(tbl: ^Table, alignment: Cell_Alignment, 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") + 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..<tbl.nr_cols { val := values[col] if col < len(values) else nil - set_cell_value(tbl, last_row(tbl), col, val) + set_cell_value_and_alignment(tbl, last_row(tbl), col, val, alignment, loc) + } +} + +row_of_aligned_values :: proc(tbl: ^Table, aligned_values: []Aligned_Value, loc := #caller_location) { + if tbl.nr_cols == 0 { + if len(aligned_values) == 0 { + panic("Cannot create empty row unless the number of columns is known in advance") + } else { + tbl.nr_cols = len(aligned_values) + } + } + + tbl.nr_rows += 1 + + for col in 0..<tbl.nr_cols { + if col < len(aligned_values) { + val := aligned_values[col].value + alignment := aligned_values[col].alignment + set_cell_value_and_alignment(tbl, last_row(tbl), col, val, alignment, loc) + } else { + set_cell_value_and_alignment(tbl, last_row(tbl), col, "", .Left, loc) + } } - tbl.dirty = true } +// TODO: This should work correctly when #3262 is fixed. +// row :: proc { +// row_of_values, +// aligned_row_of_values, +// row_of_aligned_values, +// } + last_row :: proc(tbl: ^Table) -> int { return tbl.nr_rows - 1 } @@ -165,27 +268,24 @@ 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 - +build :: proc(tbl: ^Table, width_proc: Width_Proc) { 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 - } + cell.width = width_proc(cell.text) + tbl.colw[col] = max(tbl.colw[col], cell.width) } } colw_sum := 0 for v in tbl.colw { - colw_sum += v + colw_sum += v + tbl.lpad + tbl.rpad } - tbl.tblw = max(colw_sum, len(tbl.caption) + tbl.lpad + tbl.rpad) + tbl.tblw = max(colw_sum, width_proc(tbl.caption) + tbl.lpad + tbl.rpad) // Resize columns to match total width of table remain := tbl.tblw-colw_sum @@ -198,13 +298,9 @@ build :: proc(tbl: ^Table) { } 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, "\t<caption>") io.write_string(w, tbl.caption) io.write_string(w, "</caption>\n") } @@ -219,45 +315,43 @@ write_html_table :: proc(w: io.Writer, tbl: ^Table) { } if tbl.has_header_row { - io.write_string(w, "<thead>\n") - io.write_string(w, " <tr>\n") + io.write_string(w, "\t<thead>\n") + io.write_string(w, "\t\t<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, "\t\t\t<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, "\t\t</tr>\n") + io.write_string(w, "\t</thead>\n") } - io.write_string(w, "<tbody>\n") + io.write_string(w, "\t<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") + io.write_string(w, "\t\t<tr>\n") for col in 0..<tbl.nr_cols { cell := get_cell(tbl, row, col) - io.write_string(w, " <td") + io.write_string(w, "\t\t\t<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, "\t\t</tr>\n") } - io.write_string(w, " </tbody>\n") + io.write_string(w, "\t</tbody>\n") io.write_string(w, "</table>\n") } -write_ascii_table :: proc(w: io.Writer, tbl: ^Table) { - if tbl.dirty { - build(tbl) - } +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, '+') @@ -271,7 +365,7 @@ write_ascii_table :: proc(w: io.Writer, tbl: ^Table) { if col == 0 { io.write_byte(w, '+') } - write_byte_repeat(w, tbl.colw[col], '-') + write_byte_repeat(w, tbl.colw[col] + tbl.lpad + tbl.rpad, '-') io.write_byte(w, '+') } io.write_byte(w, '\n') @@ -280,8 +374,8 @@ write_ascii_table :: proc(w: io.Writer, tbl: ^Table) { 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) + write_text_align(w, tbl.caption, .Center, + tbl.lpad, tbl.rpad, tbl.tblw + tbl.nr_cols - 1 - width_proc(tbl.caption) - tbl.lpad - tbl.rpad) io.write_byte(w, '|') io.write_byte(w, '\n') } @@ -304,47 +398,56 @@ write_ascii_table :: proc(w: io.Writer, tbl: ^Table) { } // Renders table according to GitHub Flavored Markdown (GFM) specification -write_markdown_table :: proc(w: io.Writer, tbl: ^Table) { +write_markdown_table :: proc(w: io.Writer, tbl: ^Table, width_proc: Width_Proc = unicode_width_proc) { // NOTE(oskar): Captions or colspans are not supported by GFM as far as I can tell. + build(tbl, width_proc) - if tbl.dirty { - build(tbl) - } - - for row in 0..<tbl.nr_rows { + write_row :: proc(w: io.Writer, tbl: ^Table, row: int, alignment: Cell_Alignment = .Left) { 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) + write_text_align(w, cell.text, alignment, tbl.lpad, tbl.rpad, tbl.colw[col] - cell.width) 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, ':') - } + start := 0 + + if tbl.has_header_row { + row := header_row(tbl) + + write_row(w, tbl, row, .Center) + + for col in 0..<tbl.nr_cols { + cell := get_cell(tbl, row, col) + if col == 0 { io.write_byte(w, '|') } - io.write_byte(w, '\n') + divider_width := tbl.colw[col] + tbl.lpad + tbl.rpad - 1 + switch cell.alignment { + case .Left: + io.write_byte(w, ':') + write_byte_repeat(w, max(1, divider_width), '-') + case .Center: + io.write_byte(w, ':') + write_byte_repeat(w, max(1, divider_width - 1), '-') + io.write_byte(w, ':') + case .Right: + write_byte_repeat(w, max(1, divider_width), '-') + io.write_byte(w, ':') + } + io.write_byte(w, '|') } + io.write_byte(w, '\n') + + start += row + 1 + } + + for row in start..<tbl.nr_rows { + write_row(w, tbl, row) } } @@ -355,27 +458,22 @@ write_byte_repeat :: proc(w: io.Writer, n: int, b: byte) { } 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(w, cell.text, cell.alignment, tbl.lpad, tbl.rpad, tbl.colw[col] - cell.width) } -write_text_align :: proc(w: io.Writer, colw, lpad, rpad: int, text: string, alignment: Cell_Alignment) { +write_text_align :: proc(w: io.Writer, text: string, alignment: Cell_Alignment, lpad, rpad, space: int) { write_byte_repeat(w, lpad, ' ') switch alignment { case .Left: io.write_string(w, text) - write_byte_repeat(w, colw - len(text), ' ') + write_byte_repeat(w, space, ' ') case .Center: - pad := colw - len(text) - odd := pad & 1 != 0 - write_byte_repeat(w, pad/2, ' ') + write_byte_repeat(w, space/2, ' ') io.write_string(w, text) - write_byte_repeat(w, pad/2 + 1 if odd else pad/2, ' ') + write_byte_repeat(w, space/2 + space & 1, ' ') case .Right: - write_byte_repeat(w, colw - len(text), ' ') + write_byte_repeat(w, space, ' ') io.write_string(w, text) } write_byte_repeat(w, rpad, ' ') |