diff options
| author | Jeroen van Rijn <Kelimion@users.noreply.github.com> | 2024-06-24 21:59:33 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-24 21:59:33 +0200 |
| commit | 185e39e53dd8379cd59cd1d7496d4ad1a2c1e0ce (patch) | |
| tree | b7391697f78dc1bb9425bd1cbad618b3daed64c8 /core | |
| parent | 8eafd9eb82e3982376a8fd85b190d90c9cc9b6ee (diff) | |
| parent | 8b05ec17652543fce3bd0fc66b48c0489a5b8740 (diff) | |
Merge pull request #3791 from Feoramund/fix-text-table-alignment
Refactor `core:text/table`
Diffstat (limited to 'core')
| -rw-r--r-- | core/text/table/doc.odin | 264 | ||||
| -rw-r--r-- | core/text/table/table.odin | 292 |
2 files changed, 418 insertions, 138 deletions
diff --git a/core/text/table/doc.odin b/core/text/table/doc.odin index 76886bdea..30358f31e 100644 --- a/core/text/table/doc.odin +++ b/core/text/table/doc.odin @@ -1,18 +1,27 @@ /* -The package `table` implements ASCII/markdown/HTML/custom rendering of tables. +The package `table` implements plain-text/markdown/HTML/custom rendering of tables. **Custom rendering example:** - 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) + package main + + import "core:io" + import "core:text/table" + + main :: proc() { + stdout := table.stdio_writer() + + tbl := table.init(&table.Table{}) + table.padding(tbl, 0, 1) + table.row(tbl, "A_LONG_ENUM", "= 54,", "// A comment about A_LONG_ENUM") + table.row(tbl, "AN_EVEN_LONGER_ENUM", "= 1,", "// A comment about AN_EVEN_LONGER_ENUM") + table.build(tbl, table.unicode_width_proc) + for row in 0..<tbl.nr_rows { + for col in 0..<tbl.nr_cols { + table.write_table_cell(stdout, tbl, row, col) + } + io.write_byte(stdout, '\n') } - io.write_byte(stdio_writer(), '\n') } This outputs: @@ -20,47 +29,57 @@ 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:** +**Plain-text rendering example:** + + package main - tbl := init(&Table{}) - defer destroy(tbl) + import "core:fmt" + import "core:io" + import "core:text/table" - caption(tbl, "This is a table caption and it is very long") + main :: proc() { + stdout := table.stdio_writer() - padding(tbl, 1, 1) // Left/right padding of cells + tbl := table.init(&table.Table{}) + defer table.destroy(tbl) - header(tbl, "AAAAAAAAA", "B") - header(tbl, "C") // Appends to previous header row. Same as if done header("AAAAAAAAA", "B", "C") from start. + table.caption(tbl, "This is a table caption and it is very long") - // 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") + table.padding(tbl, 1, 1) // Left/right padding of cells - // 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)) + table.header(tbl, "AAAAAAAAA", "B") + table.header(tbl, "C") // Appends to previous header row. Same as if done header("AAAAAAAAA", "B", "C") from start. - // 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") + // Create a row with two values. Since there are three columns the third + // value will become the empty string. + // + // NOTE: table.header() is not allowed anymore after this. + table.row(tbl, 123, "foo") - // 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. + // Use `format()` if you need custom formatting. This will allocate into + // the arena specified at init. + table.row(tbl, + table.format(tbl, "%09d", 5), + table.format(tbl, "%.6f", 6.28318530717958647692528676655900576)) - build(tbl) + // 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. + table.row(tbl) - write_ascii_table(stdio_writer(), tbl) - write_markdown_table(stdio_writer(), tbl) + table.set_cell_value_and_alignment(tbl, table.last_row(tbl), 0, "a", .Center) + table.set_cell_value(tbl, table.last_row(tbl), 1, "bbb") + table.set_cell_value(tbl, table.last_row(tbl), 2, "c") + + // Headers are regular cells, too. Use header_row() as row index to modify + // header cells. + table.set_cell_alignment(tbl, table.header_row(tbl), 1, .Center) // Sets alignment of 'B' column to Center. + table.set_cell_alignment(tbl, table.header_row(tbl), 2, .Right) // Sets alignment of 'C' column to Right. + + table.write_plain_table(stdout, tbl) + fmt.println() + table.write_markdown_table(stdout, tbl) + } This outputs: @@ -83,5 +102,168 @@ and | a | bbb | c | respectively. + + +Additionally, if you want to set the alignment and values in-line while +constructing a table, you can use `aligned_row_of_values` or +`row_of_aligned_values` like so: + + table.aligned_row_of_values(tbl, .Center, "Foo", "Bar") + table.row_of_aligned_values(tbl, {{.Center, "Foo"}, {.Right, "Bar"}}) + +**Caching Results:** + +If you only need to build a table once but display it potentially many times, +it may be more efficient to cache the results of your write into a string. + +Here's an example of how you can do that: + + package main + + import "core:fmt" + import "core:strings" + import "core:text/table" + + main :: proc() { + string_buffer := strings.builder_make() + defer strings.builder_destroy(&string_buffer) + + { + tbl: table.Table + table.init(&tbl) + defer table.destroy(&tbl) + table.caption(&tbl, "Hellope!") + table.row(&tbl, "Hellope", "World") + + builder_writer := strings.to_writer(&string_buffer) + + // The written table will be cached into the string builder after this call. + table.write_plain_table(builder_writer, &tbl) + } + // The table is inaccessible, now that we're back in the first-level scope. + + // But now the results are stored in the string builder, which can be converted to a string. + my_table_string := strings.to_string(string_buffer) + + // Remember that the string's allocated backing data lives in the + // builder and must still be freed. + // + // The deferred call to `builder_destroy` will take care of that for us + // in this simple example. + fmt.println(my_table_string) + } + +**Regarding `Width_Procs`:** + +If you know ahead of time that all the text you're parsing is ASCII, instead of +Unicode, it is more efficient to use `table.ascii_width_proc` instead of the +default `unicode_width_proc`, as that procedure has to perform in-depth lookups +to determine multiple Unicode characteristics of the codepoints parsed in order +to get the proper alignment for a variety of different scripts. + +For example, you may do this instead: + + table.write_plain_table(stdout, tbl, table.ascii_width_proc) + table.write_markdown_table(stdout, tbl, table.ascii_width_proc) + +The output will still be the same, but the preprocessing is much faster. + + +You may also supply your own `Width_Proc`s, if you know more about how the text +is structured than what we can assume. + + simple_cjk_width_proc :: proc(str: string) -> (result: int) { + for r in str { + result += 2 + } + return + } + + table.write_plain_table(stdout, tbl, simple_cjk_width_proc) + +This procedure will output 2 times the number of UTF-8 runes in a string, a +simple heuristic for CJK-only wide text. + +**Unicode Support:** + +This package makes use of the `grapheme_count` procedure from the +`core:unicode/utf8` package. It is a complete, standards-compliant +implementation for counting graphemes and calculating visual width of a Unicode +grapheme cluster in monospace cells. + +Here is a full example of how well-supported Unicode is with this package: + + package main + + import "core:fmt" + import "core:io" + import "core:os" + import "core:text/table" + + scripts :: proc(w: io.Writer) { + t: table.Table + table.init(&t) + table.caption(&t, "Tést Suite") + table.padding(&t, 1, 3) + table.header_of_aligned_values(&t, {{.Left, "Script"}, {.Center, "Sample"}}) + + table.row(&t, "Latin", "At vero eos et accusamus et iusto odio dignissimos ducimus,") + table.row(&t, "Cyrillic", "Ру́сский язы́к — язык восточнославянской группы славянской") + table.row(&t, "Greek", "Η ελληνική γλώσσα ανήκει στην ινδοευρωπαϊκή οικογένεια") + table.row(&t, "Younger Futhark", "ᚴᚢᚱᛘᛦ ᚴᚢᚾᚢᚴᛦ ᚴᛅᚱᚦᛁ ᚴᚢᛒᛚ ᚦᚢᛋᛁ ᛅᚠᛏ ᚦᚢᚱᚢᛁ ᚴᚢᚾᚢ ᛋᛁᚾᛅ ᛏᛅᚾᛘᛅᚱᚴᛅᛦ ᛒᚢᛏ") + table.row(&t, "Chinese hanzi", "官話為汉语的一支,主體分布在中国北部和西南部的大部分地区。") + table.row(&t, "Japanese kana", "いろはにほへとちりぬるをわかよたれそつねならむ") + table.row(&t, "Korean hangul", "한글, 조선글은 한국어의 공식문자로서, 세종이 한국어를") + table.row(&t, "Thai", "ภาษาไทย หรือ ภาษาไทยกลาง เป็นภาษาในกลุ่มภาษาไท ซึ่งเป็นกลุ่มย่อยของตระกูลภาษาขร้า-ไท") + table.row(&t, "Georgian", "ქართული ენა — ქართველურ ენათა ოჯახის ენა. ქართველების მშობლიური ენა,") + table.row(&t, "Armenian", "Իր շուրջ հինգհազարամյա գոյության ընթացքում հայերենը շփվել է տարբեր") + table.row(&t) + table.row_of_aligned_values(&t, {{.Left, "Arabic"}, {.Right, "ٱللُّغَةُ ٱلْعَرَبِيَّة هي أكثر اللغات السامية تحدثًا، وإحدى أكثر"}}) + table.row_of_aligned_values(&t, {{.Left, "Hebrew"}, {.Right, "עִבְרִית היא שפה שמית, ממשפחת השפות האפרו-אסייתיות, הידועה"}}) + table.row(&t) + table.row(&t, "Swedish", "Växjö [ˈvɛkːˌɧøː] är en tätort i södra Smålands inland samt centralort i Växjö kommun") + table.row(&t, "Saxon", "Hwæt! We Gardena in geardagum, þeodcyninga, þrym gefrunon, hu ða æþelingas ellen fremedon.") + table.row(&t) + table.aligned_row_of_values(&t, .Center, "Emoji (Single codepoints)", "\U0001f4ae \U0001F600 \U0001F201 \U0001F21A") + table.row(&t, "Excessive Diacritics", "H̷e̶l̵l̸o̴p̵e̷ ̸w̶o̸r̵l̶d̵!̴") + + table.write_plain_table(w, &t) + fmt.println() + } + + main :: proc() { + stdout := os.stream_from_handle(os.stdout) + + scripts(stdout) + } + +This will print out: + + +----------------------------------------------------------------------------------------------------------------------------+ + | Tést Suite | + +-----------------------------+----------------------------------------------------------------------------------------------+ + | Script | Sample | + +-----------------------------+----------------------------------------------------------------------------------------------+ + | Latin | At vero eos et accusamus et iusto odio dignissimos ducimus, | + | Cyrillic | Ру́сский язы́к — язык восточнославянской группы славянской | + | Greek | Η ελληνική γλώσσα ανήκει στην ινδοευρωπαϊκή οικογένεια | + | Younger Futhark | ᚴᚢᚱᛘᛦ ᚴᚢᚾᚢᚴᛦ ᚴᛅᚱᚦᛁ ᚴᚢᛒᛚ ᚦᚢᛋᛁ ᛅᚠᛏ ᚦᚢᚱᚢᛁ ᚴᚢᚾᚢ ᛋᛁᚾᛅ ᛏᛅᚾᛘᛅᚱᚴᛅᛦ ᛒᚢᛏ | + | Chinese hanzi | 官話為汉语的一支,主體分布在中国北部和西南部的大部分地区。 | + | Japanese kana | いろはにほへとちりぬるをわかよたれそつねならむ | + | Korean hangul | 한글, 조선글은 한국어의 공식문자로서, 세종이 한국어를 | + | Thai | ภาษาไทย หรือ ภาษาไทยกลาง เป็นภาษาในกลุ่มภาษาไท ซึ่งเป็นกลุ่มย่อยของตระกูลภาษาขร้า-ไท | + | Georgian | ქართული ენა — ქართველურ ენათა ოჯახის ენა. ქართველების მშობლიური ენა, | + | Armenian | Իր շուրջ հինգհազարամյա գոյության ընթացքում հայերենը շփվել է տարբեր | + | | | + | Arabic | ٱللُّغَةُ ٱلْعَرَبِيَّة هي أكثر اللغات السامية تحدثًا، وإحدى أكثر | + | Hebrew | עִבְרִית היא שפה שמית, ממשפחת השפות האפרו-אסייתיות, הידועה | + | | | + | Swedish | Växjö [ˈvɛkːˌɧøː] är en tätort i södra Smålands inland samt centralort i Växjö kommun | + | Saxon | Hwæt! We Gardena in geardagum, þeodcyninga, þrym gefrunon, hu ða æþelingas ellen fremedon. | + | | | + | Emoji (Single codepoints) | 💮 😀 🈁 🈚 | + | Excessive Diacritics | H̷e̶l̵l̸o̴p̵e̷ ̸w̶o̸r̵l̶d̵!̴ | + +-----------------------------+----------------------------------------------------------------------------------------------+ + */ package text_table 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, ' ') |