├── .gitignore ├── LICENSE ├── README.md ├── example.lua ├── releases ├── tabular-0.1-1.rockspec ├── tabular-0.2-1.rockspec ├── tabular-0.3-1.rockspec └── tabular-0.4-1.rockspec ├── screenshots └── color.png ├── tabular-dev-1.rockspec ├── tabular.lua └── tabular.tl /.gitignore: -------------------------------------------------------------------------------- 1 | /luarocks 2 | /lua 3 | /lua_modules 4 | /.luarocks 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Hisham Muhammad 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # tabular 3 | 4 | This is `tabular`, yet another library for visualizing Lua tables! 5 | 6 | This module is especially useful for visualizing arrays of tables, a common 7 | occurrence in Lua. That is, if you have an array like this: 8 | 9 | ``` 10 | local data = { 11 | { 12 | id = 19401009, 13 | name = "John", 14 | plays = { "rhythm guitar", "acoustic guitar", "harmonica", "piano", "vocals" }, 15 | handed = "right", 16 | }, 17 | { 18 | id = 19420618, 19 | name = "Paul", 20 | plays = { "bass guitar", "electric guitar", "piano", "vocals", "drums" }, 21 | handed = "left", 22 | }, 23 | { 24 | id = 19430225, 25 | name = "George", 26 | plays = { "lead guitar", "sitar", "vocals", "synthesizer" }, 27 | handed = "right", 28 | }, 29 | { 30 | id = 19400707, 31 | name = "Ringo", 32 | plays = { "drums", "percussion", "vocals" }, 33 | handed = "left", 34 | }, 35 | } 36 | ``` 37 | 38 | running this 39 | 40 | ``` 41 | print(tabular(data)) 42 | ``` 43 | 44 | will display it like this: 45 | 46 | ``` 47 | ┌──────┬────────┬──────┬─────────────────────┐ 48 | │handed│id │name │plays │ 49 | │──────│── │──── │───── │ 50 | │right │19401009│John │┌───┬───────────────┐│ 51 | │ │ │ ││1 :│rhythm guitar ││ 52 | │ │ │ ││2 :│acoustic guitar││ 53 | │ │ │ ││3 :│harmonica ││ 54 | │ │ │ ││4 :│piano ││ 55 | │ │ │ ││5 :│vocals ││ 56 | │ │ │ │└───┴───────────────┘│ 57 | │left │19420618│Paul │┌───┬───────────────┐│ 58 | │ │ │ ││1 :│bass guitar ││ 59 | │ │ │ ││2 :│electric guitar││ 60 | │ │ │ ││3 :│piano ││ 61 | │ │ │ ││4 :│vocals ││ 62 | │ │ │ ││5 :│drums ││ 63 | │ │ │ │└───┴───────────────┘│ 64 | │right │19430225│George│┌───┬───────────┐ │ 65 | │ │ │ ││1 :│lead guitar│ │ 66 | │ │ │ ││2 :│sitar │ │ 67 | │ │ │ ││3 :│vocals │ │ 68 | │ │ │ ││4 :│synthesizer│ │ 69 | │ │ │ │└───┴───────────┘ │ 70 | │left │19400707│Ringo │┌───┬──────────┐ │ 71 | │ │ │ ││1 :│drums │ │ 72 | │ │ │ ││2 :│percussion│ │ 73 | │ │ │ ││3 :│vocals │ │ 74 | │ │ │ │└───┴──────────┘ │ 75 | └──────┴────────┴──────┴─────────────────────┘ 76 | 77 | ``` 78 | 79 | With the optional second argument, you can specify the order of the 80 | top-level table and also filter columns: 81 | 82 | ``` 83 | print(tabular(data, { "id", "name", "handed" })) 84 | ``` 85 | 86 | produces 87 | 88 | ``` 89 | ┌────────┬──────┬──────┐ 90 | │id │name │handed│ 91 | │── │──── │──────│ 92 | │19401009│John │right │ 93 | │19420618│Paul │left │ 94 | │19430225│George│right │ 95 | │19400707│Ringo │left │ 96 | └────────┴──────┴──────┘ 97 | ``` 98 | 99 | The third argument enables coloring: 100 | 101 | ``` 102 | print(tabular(_G, nil, true)) 103 | ``` 104 | 105 | ![Colorful output](screenshots/color.png) 106 | 107 | ## Install 108 | 109 | Install it using [LuaRocks](https://luarocks.org): 110 | 111 | ``` 112 | luarocks install tabular 113 | ``` 114 | 115 | ## Reference 116 | 117 | The return value of `require` is a table which can be used as a function, 118 | which is the same as `tabular.show`: 119 | 120 | ### `tabular.show` 121 | 122 | ``` 123 | function tabular.show(t: any, column_order: {string}, color: boolean): string 124 | ``` 125 | 126 | #### Arguments 127 | 128 | * `t: any` is a Lua value. `tabular` does its best work handling tables and arrays, 129 | which can be nested, but it will `tostring` any other kind of data. 130 | * `column_order: {string}` is an array of top-level column names. It determines which 131 | columns are displayed and in which order. 132 | * `color: boolean` enables ANSI coloring, and "stripes" the output (alternates colors 133 | on each row) for better readability of complex structures. 134 | 135 | #### Returns 136 | 137 | * `{string}` It returns the tabular representation as a string. 138 | 139 | ## Credits and license 140 | 141 | `tabular` was written by [Hisham Muhammad](https://hisham.hm) using [tl](http://github.com/hishamhm/tl). 142 | 143 | License is MIT, the same as Lua. 144 | -------------------------------------------------------------------------------- /example.lua: -------------------------------------------------------------------------------- 1 | local tabular = require("tabular") 2 | 3 | local data = { 4 | { 5 | id = 19401009, 6 | name = "John", 7 | plays = { "rhythm guitar", "acoustic guitar", "harmonica", "piano", "vocals" }, 8 | handed = "right", 9 | }, 10 | { 11 | id = 19420618, 12 | name = "Paul", 13 | plays = { "bass guitar", "electric guitar", "piano", "vocals", "drums" }, 14 | handed = "left", 15 | }, 16 | { 17 | id = 19430225, 18 | name = "George", 19 | plays = { "lead guitar", "sitar", "vocals", "synthesizer" }, 20 | handed = "right", 21 | }, 22 | { 23 | id = 19400707, 24 | name = "Ringo", 25 | plays = { "drums", "percussion", "vocals" }, 26 | handed = "left", 27 | }, 28 | } 29 | 30 | print(tabular(data)) 31 | 32 | print() 33 | 34 | print(tabular(data, { "id", "name", "handed" })) 35 | 36 | print() 37 | 38 | print(tabular(_G, nil, true)) 39 | -------------------------------------------------------------------------------- /releases/tabular-0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "tabular" 2 | version = "0.1-1" 3 | source = { 4 | url = "git+https://github.com/hishamhm/tabular.git", 5 | tag = "0.1", 6 | } 7 | description = { 8 | summary = "yet another library for visualizing Lua tables", 9 | detailed = [[ 10 | This module is especially useful for visualizing arrays of tables, 11 | displaying keys as columns in tabular fashion. 12 | ]], 13 | homepage = "https://github.com/hishamhm/tabular", 14 | license = "MIT" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "compat53", 19 | "ansicolors ~> 1.0" 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | tabular = "tabular.lua" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /releases/tabular-0.2-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "tabular" 2 | version = "0.2-1" 3 | source = { 4 | url = "git+https://github.com/hishamhm/tabular.git", 5 | tag = "0.2" 6 | } 7 | description = { 8 | summary = "yet another library for visualizing Lua tables", 9 | detailed = [[ 10 | This module is especially useful for visualizing arrays of tables, 11 | displaying keys as columns in tabular fashion. 12 | ]], 13 | homepage = "https://github.com/hishamhm/tabular", 14 | license = "MIT" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "compat53", 19 | "ansicolors ~> 1.0" 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | tabular = "tabular.lua" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /releases/tabular-0.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "tabular" 2 | version = "0.3-1" 3 | source = { 4 | url = "git+https://github.com/hishamhm/tabular.git", 5 | tag = "0.3" 6 | } 7 | description = { 8 | summary = "yet another library for visualizing Lua tables", 9 | detailed = [[ 10 | This module is especially useful for visualizing arrays of tables, 11 | displaying keys as columns in tabular fashion. 12 | ]], 13 | homepage = "https://github.com/hishamhm/tabular", 14 | license = "MIT" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "compat53", 19 | "ansicolors ~> 1.0" 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | tabular = "tabular.lua" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /releases/tabular-0.4-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "tabular" 2 | version = "0.4-1" 3 | source = { 4 | url = "git+https://github.com/hishamhm/tabular.git", 5 | tag = "0.4" 6 | } 7 | description = { 8 | summary = "yet another library for visualizing Lua tables", 9 | detailed = [[ 10 | This module is especially useful for visualizing arrays of tables, 11 | displaying keys as columns in tabular fashion. 12 | ]], 13 | homepage = "https://github.com/hishamhm/tabular", 14 | license = "MIT" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "compat53", 19 | "ansicolors ~> 1.0" 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | tabular = "tabular.lua" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /screenshots/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamhm/tabular/2baa73ed806aa97294430f4d5e950e24fe9ba448/screenshots/color.png -------------------------------------------------------------------------------- /tabular-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "tabular" 2 | version = "dev-1" 3 | source = { 4 | url = "git+https://github.com/hishamhm/tabular.git" 5 | } 6 | description = { 7 | summary = "yet another library for visualizing Lua tables", 8 | detailed = [[ 9 | This module is especially useful for visualizing arrays of tables, 10 | displaying keys as columns in tabular fashion. 11 | ]], 12 | homepage = "https://github.com/hishamhm/tabular", 13 | license = "MIT" 14 | } 15 | dependencies = { 16 | "lua >= 5.1", 17 | "compat53", 18 | "ansicolors ~> 1.0", 19 | } 20 | build = { 21 | type = "builtin", 22 | modules = { 23 | tabular = "tabular.lua" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tabular.lua: -------------------------------------------------------------------------------- 1 | local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local math = _tl_compat and _tl_compat.math or math; local os = _tl_compat and _tl_compat.os or os; local pairs = _tl_compat and _tl_compat.pairs or pairs; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local utf8 = _tl_compat and _tl_compat.utf8 or utf8 2 | 3 | 4 | 5 | local tabular = {} 6 | 7 | 8 | 9 | 10 | 11 | local _require = require 12 | local ansicolors = _require("ansicolors") 13 | 14 | local draw = { 15 | NW = "/", 16 | NE = "\\", 17 | SW = "\\", 18 | SE = "/", 19 | N = "+", 20 | S = "+", 21 | E = "+", 22 | W = "+", 23 | V = "|", 24 | H = "-", 25 | X = "+", 26 | } 27 | 28 | local colors = { 29 | ansicolors.noReset("%{cyan}"), 30 | ansicolors.noReset("%{white}"), 31 | } 32 | 33 | local function strlen(s) 34 | s = s:gsub("\27[^m]*m", "") 35 | return #s 36 | end 37 | 38 | local strsub = string.sub 39 | 40 | if (os.getenv("LANG") or ""):upper():match("UTF%-?8") then 41 | draw = { 42 | NW = "┌", 43 | NE = "┐", 44 | SW = "└", 45 | SE = "┘", 46 | N = "┬", 47 | S = "┴", 48 | E = "┤", 49 | W = "├", 50 | V = "│", 51 | H = "─", 52 | X = "┼", 53 | } 54 | 55 | strlen = function(s) 56 | s = s:gsub("\27[^m]*m", "") 57 | return utf8.len(s) or #s 58 | end 59 | 60 | strsub = function(s, i, j) 61 | local uj = utf8.offset(s, j + 1) 62 | if uj then 63 | uj = uj - 1 64 | end 65 | return s:sub(utf8.offset(s, i), uj) 66 | end 67 | 68 | end 69 | 70 | 71 | 72 | 73 | 74 | 75 | local show 76 | local show_as_columns 77 | 78 | local function output_line(out, line) 79 | table.insert(out, line) 80 | out.width = math.max(out.width or 0, strlen(line)) 81 | end 82 | 83 | local function escape_chars(c) 84 | return "\\" .. string.byte(c) 85 | end 86 | 87 | 88 | 89 | local function show_as_list(t, color, seen, ids, skip_array) 90 | local tt = {} 91 | local width = 0 92 | local keys = {} 93 | 94 | for k, v in pairs(t) do 95 | if not skip_array or type(k) ~= "number" then 96 | table.insert(tt, { k, v }) 97 | keys[k] = tostring(k) 98 | width = math.max(width, strlen(keys[k])) 99 | end 100 | end 101 | 102 | table.sort(tt, function(a, b) 103 | if type(a[1]) == "number" and type(b[1]) == "number" then 104 | return a[1] < b[1] 105 | else 106 | return tostring(a[1]) < tostring(b[1]) 107 | end 108 | end) 109 | 110 | for i = 1, #tt do 111 | local k = keys[tt[i][1]] 112 | tt[i][1] = k .. " " .. ("."):rep(width - strlen(k)) .. ":" 113 | end 114 | 115 | return show_as_columns(tt, color, seen, ids, nil, true) 116 | end 117 | 118 | local function show_primitive(t) 119 | local out = {} 120 | local s = tostring(t) 121 | 122 | if utf8.len(s) then 123 | s = s:gsub("[\n\t]", { 124 | ["\n"] = "\\n", 125 | ["\t"] = "\\t", 126 | }) 127 | else 128 | s = s:gsub("[%z-\31\127-\255]", escape_chars) 129 | end 130 | 131 | if strlen(s) > 80 then 132 | 133 | for i = 1, strlen(s), 80 do 134 | output_line(out, strsub(s, i, i + 79)) 135 | end 136 | else 137 | output_line(out, s) 138 | end 139 | 140 | return out 141 | end 142 | 143 | 144 | 145 | 146 | 147 | 148 | show_as_columns = function(t, bgcolor, seen, ids, column_order, skip_header) 149 | local columns = {} 150 | local row_heights = {} 151 | 152 | local column_names 153 | local column_set 154 | 155 | if column_order then 156 | column_names = column_order 157 | column_set = {} 158 | for _, cname in ipairs(column_names) do 159 | column_set[cname] = true 160 | end 161 | end 162 | 163 | for i, row in ipairs(t) do 164 | if type(row) == "table" then 165 | for k, v in pairs(row) do 166 | local sk = tostring(k) 167 | if (not column_set) or column_set[sk] then 168 | if not columns[sk] then 169 | columns[sk] = {} 170 | columns[sk].width = strlen(sk) 171 | end 172 | local sv = show(v, bgcolor and colors[(i % #colors) + 1], seen, ids) 173 | columns[sk][i] = sv 174 | columns[sk].width = math.max(columns[sk].width, sv.width) 175 | row_heights[i] = math.max(row_heights[i] or 0, #sv) 176 | end 177 | end 178 | end 179 | end 180 | 181 | if not column_order then 182 | column_names = {} 183 | column_set = {} 184 | for name, _row in pairs(columns) do 185 | if not column_set[name] then 186 | table.insert(column_names, name) 187 | column_set[name] = true 188 | end 189 | end 190 | table.sort(column_names) 191 | end 192 | 193 | local function output_cell(line, cname, text, color) 194 | local w = columns[cname].width 195 | text = text or "" 196 | if color then 197 | table.insert(line, color) 198 | elseif bgcolor then 199 | table.insert(line, bgcolor) 200 | end 201 | table.insert(line, text .. (" "):rep((w - strlen(text)))) 202 | if color then 203 | table.insert(line, bgcolor) 204 | end 205 | table.insert(line, draw.V) 206 | end 207 | 208 | local out = {} 209 | 210 | local border_top = {} 211 | local border_bot = {} 212 | for i, cname in ipairs(column_names) do 213 | local w = columns[cname].width 214 | table.insert(border_top, draw.H:rep(w)) 215 | table.insert(border_bot, draw.H:rep(w)) 216 | if i < #column_names then 217 | table.insert(border_top, draw.N) 218 | table.insert(border_bot, draw.S) 219 | end 220 | end 221 | table.insert(border_top, 1, draw.NW) 222 | table.insert(border_bot, 1, draw.SW) 223 | table.insert(border_top, draw.NE) 224 | table.insert(border_bot, draw.SE) 225 | 226 | output_line(out, table.concat(border_top)) 227 | if not skip_header then 228 | local line = { draw.V } 229 | local sep = { draw.V } 230 | for _, cname in ipairs(column_names) do 231 | output_cell(line, cname, cname) 232 | output_cell(sep, cname, draw.H:rep(strlen(cname))) 233 | end 234 | output_line(out, table.concat(line)) 235 | output_line(out, table.concat(sep)) 236 | end 237 | 238 | for i = 1, #t do 239 | for h = 1, row_heights[i] or 1 do 240 | local line = { draw.V } 241 | for _, cname in ipairs(column_names) do 242 | local row = columns[cname][i] 243 | output_cell(line, cname, row and row[math.floor(h)] or "", bgcolor and colors[(i % #colors) + 1]) 244 | end 245 | output_line(out, table.concat(line)) 246 | end 247 | end 248 | output_line(out, table.concat(border_bot)) 249 | 250 | local mt = t 251 | for k, _v in pairs(mt) do 252 | if type(k) ~= "number" then 253 | local out2 = show_as_list(mt, bgcolor, seen, ids, true) 254 | for _, line in ipairs(out2) do 255 | output_line(out, line) 256 | end 257 | break 258 | end 259 | end 260 | 261 | return out 262 | end 263 | 264 | show = function(t, color, seen, ids, column_order) 265 | if type(t) == "table" and seen[t] then 266 | local msg = "" 267 | return { msg, width = strlen(msg) } 268 | end 269 | seen[t] = true 270 | 271 | if type(t) == "table" then 272 | local tt = t 273 | if #(tt) > 0 and type(tt[1]) == "table" then 274 | return show_as_columns(tt, color, seen, ids, column_order) 275 | else 276 | return show_as_list(tt, color, seen, ids) 277 | end 278 | else 279 | return show_primitive(t) 280 | end 281 | end 282 | 283 | local function detect_cycles(t, n, seen) 284 | n = n or 0 285 | seen = seen or {} 286 | if type(t) == "table" then 287 | if seen[t] then 288 | return seen 289 | end 290 | n = n + 1 291 | seen[t] = n 292 | for _k, v in pairs(t) do 293 | seen, n = detect_cycles(v, n, seen) 294 | end 295 | end 296 | return seen, n 297 | end 298 | 299 | function tabular.show(t, column_order, color) 300 | local ids = detect_cycles(t) 301 | return table.concat(show(t, color and colors and ansicolors.noReset("%{reset}"), {}, ids, column_order), "\n") 302 | end 303 | 304 | if arg and arg[0]:match("tabular%..*$") then 305 | print(tabular.show(_G, nil, true)) 306 | os.exit(0) 307 | end 308 | 309 | return setmetatable(tabular, { 310 | __call = function(_, ...) 311 | return tabular.show(...) 312 | end, 313 | }) 314 | -------------------------------------------------------------------------------- /tabular.tl: -------------------------------------------------------------------------------- 1 | local record Tabular 2 | show: function(t: any, column_order?: {string}, color?: boolean): string 3 | end 4 | 5 | local tabular: Tabular = {} 6 | 7 | local record AnsiColors 8 | noReset: function(string):string 9 | end 10 | 11 | local _require = require 12 | local ansicolors = _require("ansicolors") as AnsiColors 13 | 14 | local draw = { 15 | NW = "/", 16 | NE = "\\", 17 | SW = "\\", 18 | SE = "/", 19 | N = "+", 20 | S = "+", 21 | E = "+", 22 | W = "+", 23 | V = "|", 24 | H = "-", 25 | X = "+", 26 | } 27 | 28 | local colors: {string} = { 29 | ansicolors.noReset("%{cyan}"), 30 | ansicolors.noReset("%{white}"), 31 | } 32 | 33 | local function strlen(s: string): integer 34 | s = s:gsub("\27[^m]*m", "") 35 | return #s 36 | end 37 | 38 | local strsub = string.sub 39 | 40 | if (os.getenv("LANG") or ""):upper():match("UTF%-?8") then 41 | draw = { 42 | NW = "┌", 43 | NE = "┐", 44 | SW = "└", 45 | SE = "┘", 46 | N = "┬", 47 | S = "┴", 48 | E = "┤", 49 | W = "├", 50 | V = "│", 51 | H = "─", 52 | X = "┼", 53 | } 54 | 55 | strlen = function(s: string): integer 56 | s = s:gsub("\27[^m]*m", "") 57 | return utf8.len(s) as integer or #s 58 | end 59 | 60 | strsub = function(s: string, i: number, j: number): string 61 | local uj: number = utf8.offset(s, j + 1) 62 | if uj then 63 | uj = uj - 1 64 | end 65 | return s:sub(utf8.offset(s, i) as integer, uj as integer) 66 | end 67 | 68 | end 69 | 70 | local record Output 71 | {string} 72 | width: number 73 | end 74 | 75 | local show: function(any, string, {any:boolean}, {any:number}, ?{string}):Output 76 | local show_as_columns: function({{any:any}}, string, {any:boolean}, {any:number}, {string}, ?boolean): Output 77 | 78 | local function output_line(out: Output, line: string) 79 | table.insert(out, line) 80 | out.width = math.max(out.width or 0, strlen(line)) 81 | end 82 | 83 | local function escape_chars(c:string):string 84 | return "\\" .. string.byte(c) 85 | end 86 | 87 | local type Pair = {string, any} 88 | 89 | local function show_as_list(t: {any:any}, color: string, seen: {any:boolean}, ids: {any:number}, skip_array?: boolean): Output 90 | local tt: {Pair} = {} 91 | local width = 0 92 | local keys = {} 93 | 94 | for k, v in pairs(t) do 95 | if not skip_array or type(k) ~= "number" then 96 | table.insert(tt, { k as string, v }) 97 | keys[k] = tostring(k) 98 | width = math.max(width, strlen(keys[k])) 99 | end 100 | end 101 | 102 | table.sort(tt, function(a: Pair, b: Pair): boolean 103 | if type(a[1]) == "number" and type(b[1]) == "number" then 104 | return a[1] < b[1] 105 | else 106 | return tostring(a[1]) < tostring(b[1]) 107 | end 108 | end) 109 | 110 | for i = 1, #tt do 111 | local k = keys[tt[i][1]] 112 | tt[i][1] = k .. " " .. ("."):rep(width - strlen(k)) .. ":" 113 | end 114 | 115 | return show_as_columns(tt as {{any:any}}, color, seen, ids, nil, true) 116 | end 117 | 118 | local function show_primitive(t: any): Output 119 | local out = {} 120 | local s = tostring(t) 121 | 122 | if utf8.len(s) then 123 | s = s:gsub("[\n\t]", { 124 | ["\n"] = "\\n", 125 | ["\t"] = "\\t", 126 | }) 127 | else 128 | s = s:gsub("[%z-\31\127-\255]", escape_chars) 129 | end 130 | 131 | if strlen(s) > 80 then 132 | -- word wrap 133 | for i = 1, strlen(s), 80 do 134 | output_line(out, strsub(s, i, i+79)) 135 | end 136 | else 137 | output_line(out, s) 138 | end 139 | 140 | return out 141 | end 142 | 143 | local record Row 144 | {Output} 145 | width: number 146 | end 147 | 148 | show_as_columns = function(t: {{any:any}}, bgcolor: string, seen: {any:boolean}, ids: {any:number}, column_order: {string}, skip_header: boolean): Output 149 | local columns: {string:Row} = {} 150 | local row_heights: {integer:integer} = {} 151 | 152 | local column_names: {string} 153 | local column_set: {string:boolean} 154 | 155 | if column_order then 156 | column_names = column_order 157 | column_set = {} 158 | for _, cname in ipairs(column_names) do 159 | column_set[cname] = true 160 | end 161 | end 162 | 163 | for i, row in ipairs(t) do 164 | if type(row) == "table" then 165 | for k, v in pairs(row) do 166 | local sk = tostring(k) 167 | if (not column_set) or column_set[sk] then 168 | if not columns[sk] then 169 | columns[sk] = {} 170 | columns[sk].width = strlen(sk) 171 | end 172 | local sv = show(v, bgcolor and colors[(i % #colors) + 1], seen, ids) 173 | columns[sk][i] = sv 174 | columns[sk].width = math.max(columns[sk].width, sv.width) 175 | row_heights[i] = math.max(row_heights[i] or 0, #sv) 176 | end 177 | end 178 | end 179 | end 180 | 181 | if not column_order then 182 | column_names = {} 183 | column_set = {} 184 | for name, _row in pairs(columns) do 185 | if not column_set[name] then 186 | table.insert(column_names, name) 187 | column_set[name] = true 188 | end 189 | end 190 | table.sort(column_names) 191 | end 192 | 193 | local function output_cell(line: {string}, cname: string, text: string, color?: string) 194 | local w = columns[cname].width 195 | text = text or "" 196 | if color then 197 | table.insert(line, color) 198 | elseif bgcolor then 199 | table.insert(line, bgcolor) 200 | end 201 | table.insert(line, text .. (" "):rep((w - strlen(text)) as integer)) 202 | if color then 203 | table.insert(line, bgcolor) 204 | end 205 | table.insert(line, draw.V) 206 | end 207 | 208 | local out = {} 209 | 210 | local border_top = {} 211 | local border_bot = {} 212 | for i, cname in ipairs(column_names) do 213 | local w = columns[cname].width 214 | table.insert(border_top, draw.H:rep(w as integer)) 215 | table.insert(border_bot, draw.H:rep(w as integer)) 216 | if i < #column_names then 217 | table.insert(border_top, draw.N) 218 | table.insert(border_bot, draw.S) 219 | end 220 | end 221 | table.insert(border_top, 1, draw.NW) 222 | table.insert(border_bot, 1, draw.SW) 223 | table.insert(border_top, draw.NE) 224 | table.insert(border_bot, draw.SE) 225 | 226 | output_line(out, table.concat(border_top)) 227 | if not skip_header then 228 | local line = { draw.V } 229 | local sep = { draw.V } 230 | for _, cname in ipairs(column_names) do 231 | output_cell(line, cname, cname) 232 | output_cell(sep, cname, draw.H:rep(strlen(cname))) 233 | end 234 | output_line(out, table.concat(line)) 235 | output_line(out, table.concat(sep)) 236 | end 237 | 238 | for i = 1, #t do 239 | for h = 1, row_heights[i] or 1 do 240 | local line = { draw.V } 241 | for _, cname in ipairs(column_names) do 242 | local row = columns[cname][i] 243 | output_cell(line, cname, row and row[math.floor(h)] or "", bgcolor and colors[(i % #colors) + 1]) 244 | end 245 | output_line(out, table.concat(line)) 246 | end 247 | end 248 | output_line(out, table.concat(border_bot)) 249 | 250 | local mt = t as {any:any} 251 | for k, _v in pairs(mt) do 252 | if type(k) ~= "number" then 253 | local out2 = show_as_list(mt, bgcolor, seen, ids, true) 254 | for _, line in ipairs(out2) do 255 | output_line(out, line) 256 | end 257 | break 258 | end 259 | end 260 | 261 | return out 262 | end 263 | 264 | show = function(t: any, color: string, seen: {any:boolean}, ids: {any:number}, column_order: {string}): Output 265 | if type(t) == "table" and seen[t] then 266 | local msg = "" 267 | return { msg, width = strlen(msg) } 268 | end 269 | seen[t] = true 270 | 271 | if type(t) == "table" then 272 | local tt = t as {any:any} 273 | if #(tt as {any}) > 0 and type(tt[1]) == "table" then 274 | return show_as_columns(tt as {{any:any}}, color, seen, ids, column_order) 275 | else 276 | return show_as_list(tt, color, seen, ids) 277 | end 278 | else 279 | return show_primitive(t) 280 | end 281 | end 282 | 283 | local function detect_cycles(t: any, n?: number, seen?: {any:number}): {any:number}, number 284 | n = n or 0 285 | seen = seen or {} 286 | if type(t) == "table" then 287 | if seen[t] then 288 | return seen 289 | end 290 | n = n + 1 291 | seen[t] = n 292 | for _k, v in pairs(t as {any:any}) do 293 | seen, n = detect_cycles(v, n, seen) 294 | end 295 | end 296 | return seen, n 297 | end 298 | 299 | function tabular.show(t: any, column_order?: {string}, color?: boolean): string 300 | local ids = detect_cycles(t) 301 | return table.concat(show(t, color and colors and ansicolors.noReset("%{reset}"), {}, ids, column_order), "\n") 302 | end 303 | 304 | if arg and arg[0]:match("tabular%..*$") then 305 | print(tabular.show(_G, nil, true)) 306 | os.exit(0) 307 | end 308 | 309 | return setmetatable( tabular, { 310 | __call = function(_: Tabular, ...:any): string 311 | return tabular.show(... as {string}) 312 | end 313 | } ) 314 | --------------------------------------------------------------------------------