├── LICENSE ├── README.md ├── expected.md ├── pandoc-list-table.lua ├── pandoc-list-table.moon └── test.md /LICENSE: -------------------------------------------------------------------------------- 1 | This software is Copyright (c) 2020 by Benct Philip Jonsson. 2 | 3 | This is free software, licensed under: 4 | 5 | The MIT (X11) License 6 | 7 | The MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated 11 | documentation files (the "Software"), to deal in the Software 12 | without restriction, including without limitation the rights to 13 | use, copy, modify, merge, publish, distribute, sublicense, 14 | and/or sell copies of the Software, and to permit persons to 15 | whom the Software is furnished to do so, subject to the 16 | following conditions: 17 | 18 | The above copyright notice and this permission notice shall 19 | be included in all copies or substantial portions of the 20 | Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT 23 | WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 24 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR 26 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT 27 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 28 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 30 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 31 | CONNECTION WITH THE SOFTWARE OR THE USE OR 32 | OTHER DEALINGS IN THE SOFTWARE. 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # list-table 2 | 3 | This [Pandoc][] [Lua filter][] allows to convert lists of lists (bullet lists and/or ordered lists) into tables. This makes it easier to type long tables and tables with "complicated" content because you don't need to draw any ASCII art. 4 | 5 | The filter can also convert tables to lists of lists, allowing full roundtripping. 6 | 7 | [Pandoc]: https://pandoc.org 8 | [Lua filter]: https://pandoc.org/MANUAL.html#option--lua-filter 9 | 10 | ## Version 11 | 12 | This document describes filter version 202010012000. 13 | 14 | ## Contributing/hacking 15 | 16 | **Don't modify `pandoc-list-table.lua` directly!** 17 | 18 | The filter is written in [MoonScript][] which must be compiled to Lua with the `moonc` program to be used with Pandoc. If you want to do a pull request, or just hack on the filter, you should edit the [pandoc-list-table.moon](pandoc-list-table.moon) file, install MoonScript, then compile the filter code to Lua with `moonc pandoc-list-table.moon`, then check that your modifications work by running pandoc on suitable input with `pandoc-list-table.lua` specified as a Lua filter. 19 | 20 | [MoonScript]: https://moonscript.org 21 | 22 | ## Compatibility 23 | 24 | With Pandoc version 2.10 Pandoc's internal representation of tables changed from 25 | the earlier simple model to a model which allows ["complex" tables](#a-note-on-terminology) 26 | more similar to HTML tables (colspan/rowspan etc.), so that existing filters 27 | written to work with the old simple Table object don't work with Pandoc 2.10.0 28 | and Pandoc 2.10.1 After Pandoc version 2.10.1 the Lua filter engine supports, as 29 | a compatibility measure, a new SimpleTable object type similar to the old simple 30 | Table object, and provides functions to convert between SimpleTable objects and 31 | the new "complex" Table type. As of Pandoc 2.10.1 Pandoc's Markdown reader and 32 | writer do as yet not have any syntax supporting the new "complex" table 33 | features, and since it is not clear how this filter might support complex tables 34 | no support for the complex Table object type has been implemented in this filter 35 | either. 36 | 37 | Due to the changed table model in Pandoc 2.10 this filter does not work with 38 | Pandoc versions 2.10.0 and 2.10.1 inclusive. As of filter version 20201001 39 | (that's YYYYMMDD!) there is a check in place which uses the [SimpleTable][] 40 | (see also the [SimpleTable pull request][]) 41 | constructor if it exists and throws an error if the [PANDOC_VERSION][] >= 2.10.0 42 | but SimpleTable does not exist. Thus the filter should work both with nightlies 43 | where SimpleTable exists and with the next release of Pandoc. If SimpleTable 44 | exists the `table2lol` implementation now tries to convert complex Table objects 45 | to SimpleTable objects, throwing an error with a hopefully helpful message if 46 | conversion fails, and at the other end the `lol2table` implementation converts a 47 | SimpleTable to a complex Table before returning (and also throws an error with a 48 | hopefully helpful message in the unlikely event that such a conversion fails.) 49 | 50 | This should mean that when Pandoc's readers and writers start to support 51 | the new complex Table features this filter should still work unless you 52 | try to convert tables actually *using* those features to lists. In the future 53 | this filter may come to support at least some complex Table features along the 54 | lines suggested in [Issue #1][issue_1], but don't hold your breath for it! 55 | 56 | [SimpleTable]: https://pandoc.org/lua-filters.html#type-simpletable 57 | [SimpleTable pull request]: https://github.com/jgm/pandoc/pull/6575 58 | [PANDOC_VERSION]: https://pandoc.org/lua-filters.html#global-variables 59 | [issue_1]: https://git.io/JU1XR 60 | 61 | ### A note on terminology 62 | 63 | In this README a distinction is made between the following terms: 64 | 65 | - *complicated content* 66 | 67 | Table cell content which is hard or impossible to represent in the "ASCII 68 | art" table syntaxes of Pandoc's Markdown. 69 | 70 | - *complex Table* 71 | 72 | The new Table object type in the Lua filter engine from Pandoc 2.10 and on, 73 | which supports various HTML-like table features such as colspans and 74 | rowspans. This filted does as of its version 20201001 not support these 75 | features. 76 | 77 | - *simple Table* 78 | 79 | The old Table object type in the Lua filter engine before Pandoc 2.10, 80 | which did not support the new "complex" table features. This filter 81 | supports versions of Pandoc using this older Table object type. 82 | 83 | - *SimpleTable* 84 | 85 | The compatibility SimpleTable object type, similar to the old *simple Table* 86 | object type supported by the Lua filter engine in versions of Pandoc later 87 | than 2.10. This filter uses this object type when available. 88 | 89 | 90 | ## Usage 91 | 92 | Obviously it would be dysfunctional if all lists of lists were converted to tables. Hence you must tell the filter that you want to convert a given list of list to a table by wrapping the list in a div with the class `lol2table` (short for "list-of-lists-to-table": 93 | 94 | ````pandoc 95 | :::lol2table 96 | * - foo 97 | - bar 98 | - baz 99 | * - + tic 100 | + pic 101 | - + tac 102 | + pac 103 | ::: 104 | ```` 105 | 106 | When running this through pandoc with the filter enabled and with markdown as output format it is replaced with this: 107 | 108 | ````pandoc 109 | +---------+---------+-----+ 110 | | foo | bar | baz | 111 | +=========+=========+=====+ 112 | | - tic | - tac | | 113 | | - pic | - pac | | 114 | +---------+---------+-----+ 115 | ```` 116 | 117 | Note how each item in the top level list becomes a table row and each item in the second level list becomes a table cell, while third level list remain lists. Note also that the filter handles the situation where there are only two items in the second second-level list: if any rows are shorter than the longest row they are padded with empty cells towards the end. 118 | 119 | ### Headerless tables 120 | 121 | To turn a list of lists into a headerless table just include the class `no-header` (or `noheader`) on the wrapping div: 122 | 123 | ````pandoc 124 | ::: {.lol2table} 125 | Table with header 126 | 127 | 1. 1. foo 128 | 2. bar 129 | 3. baz 130 | 131 | 2. 1. tic 132 | 2. tac 133 | 3. toc 134 | ::: 135 | 136 | ::: {.lol2table .no-header} 137 | Table without header 138 | 139 | 1. 1. foo 140 | 2. bar 141 | 3. baz 142 | 143 | 2. 1. tic 144 | 2. tac 145 | 3. toc 146 | ::: 147 | ```` 148 | 149 | ````pandoc 150 | foo bar baz 151 | ----- ----- ----- 152 | tic tac toc 153 | 154 | : Table with header 155 | 156 | ----- ----- ----- 157 | foo bar baz 158 | tic tac toc 159 | ----- ----- ----- 160 | 161 | : Table without header 162 | ```` 163 | 164 | ### Captions 165 | 166 | The previous example also shows how to set a caption on the table: just include a paragraph with the caption text inside the div. 167 | 168 | ### Custom alignments and custom column widths 169 | 170 | To specify the alignment of the table columns set an attribute `align` on the div. Its value should be a comma separated "list" (and I mean *comma* separated, not comma-and-whitespace!) of any of the letters `d l c r` (for `AlignDefault`, `AlignLeft`, `AlignCenter`, `AlignRight` respectively): 171 | 172 | ````pandoc 173 | ::: {.lol2table align="l,c,r"} 174 | * - foo 175 | - bar 176 | - baz 177 | * - + tic 178 | + pic 179 | - + tac 180 | + pac 181 | ::: 182 | ```` 183 | 184 | ````pandoc 185 | +---------+---------+-----+ 186 | | foo | bar | baz | 187 | +:========+:=======:+====:+ 188 | | - tic | - tac | | 189 | | - pic | - pac | | 190 | +---------+---------+-----+ 191 | ```` 192 | 193 | Likewise to specify the relative width of columns include an attribute `widths` on the div. Its value should be a comma-separated (again really *comma* separated!) "list" of integers between 0 and 100, where each integer is the percentage of the available total width which should be the width of the respective column: 194 | 195 | ````pandoc 196 | ::: {.lol2table widths="20,40,10"} 197 | * - foo 198 | - bar 199 | - baz 200 | * - + tic 201 | + pic 202 | - + tac 203 | + pac 204 | ::: 205 | ```` 206 | 207 | ````pandoc 208 | +-------------+---------------------------+------+ 209 | | foo | bar | baz | 210 | +=============+===========================+======+ 211 | | - tic | - tac | | 212 | | - pic | - pac | | 213 | +-------------+---------------------------+------+ 214 | ```` 215 | 216 | Naturally you can combine the two: 217 | 218 | ````pandoc 219 | ::: {.lol2table align="l,c,r" widths="20,40,10"} 220 | * - foo 221 | - bar 222 | - baz 223 | * - tic 224 | - tac 225 | ::: 226 | ```` 227 | 228 | ````pandoc 229 | --------------------------------------------------- 230 | foo bar baz 231 | -------------- ---------------------------- ------- 232 | tic tac 233 | 234 | --------------------------------------------------- 235 | ```` 236 | 237 | If you specify more alignments or widths than there are columns the extra alignments/widths will be ignored. 238 | 239 | If you specify fewer alignments than there are columns the list of alignments is padded to the right length with copies of the rightmost alignment actually specified. If you specify fewer widths than there are columns the list of widths is padded to the right length with zeroes. This should cause Pandoc to distribute the remaining width between them. 240 | 241 | ## Roundtripping 242 | 243 | To convert a table into a list of lists you wrap it in a div with the class `table2lol`: 244 | 245 | ````pandoc 246 | :::table2lol 247 | 248 | |foo|bar|baz 249 | |---|---|--- 250 | |tic|tac|toc 251 | 252 | ::: 253 | ```` 254 | 255 | ````pandoc 256 | 0. 1. foo 257 | 2. bar 258 | 3. baz 259 | 260 | 1. 1. tic 261 | 2. tac 262 | 3. toc 263 | ```` 264 | 265 | Note that the resulting lists always are numbered lists and that if there was a header row the numbering of the top-level list starts at zero. 266 | 267 | ### Keeping the div 268 | 269 | If you include a class `keep-div` (or `keepdiv`) on the div the result will also be wrapped in a div, designed to make roundtripping easier: 270 | 271 | 272 | ````pandoc 273 | ::: {#alpha .lol2table .keep-div} 274 | 1. 1. foo 275 | 2. bar 276 | 3. baz 277 | 2. 1. tic 278 | 2. tac 279 | 3. toc 280 | ::: 281 | 282 | ::: {#beta .table2lol .keep-div} 283 | foo bar baz 284 | ----- ----- ----- 285 | tic tac toc 286 | ::: 287 | ```` 288 | 289 | ````pandoc 290 | ::: {#alpha .maybe-table2lol .keep-div} 291 | foo bar baz 292 | ----- ----- ----- 293 | tic tac toc 294 | ::: 295 | 296 | ::: {#beta .maybe-lol2table .keep-div align="l,l,l" widths="0,0,0"} 297 | 0. 1. foo 298 | 2. bar 299 | 3. baz 300 | 301 | 1. 1. tic 302 | 2. tac 303 | 3. toc 304 | ::: 305 | ```` 306 | 307 | ## Author 308 | 309 | Benct Philip Jonsson `bpjonsson+pandoc@gmail.com` 310 | 311 | ## Copyright and license 312 | 313 | This software is Copyright (c) 2020 by Benct Philip Jonsson. 314 | 315 | This is free software, licensed under: 316 | 317 | The MIT (X11) License 318 | 319 | http://www.opensource.org/licenses/mit-license.php 320 | 321 | 322 | -------------------------------------------------------------------------------- /expected.md: -------------------------------------------------------------------------------- 1 | ::: {.maybe-table2lol .keep-div} 2 | +-------------+---------------------------+---------------------------+ 3 | | foo | bar | baz | 4 | +=============+===========================+===========================+ 5 | | - tic | - tac | - toc | 6 | | - pic | - pac | - poc | 7 | +-------------+---------------------------+---------------------------+ 8 | 9 | : A table of stuff. 10 | ::: 11 | 12 | ::: {.maybe-lol2table .keep-div widths="0,0,0" align="d,d,d"} 13 | 0. 1. foo 14 | 2. bar 15 | 3. baz 16 | 17 | 1. 1. tic 18 | 2. tac 19 | 3. toc 20 | ::: 21 | 22 | ::: {.maybe-lol2table .no-header .keep-div widths="0,0,0" align="d,d,d"} 23 | 1. 1. foo 24 | 2. bar 25 | 3. baz 26 | 27 | 2. 1. tic 28 | 2. tac 29 | 3. toc 30 | 31 | Headerless table 32 | ::: 33 | -------------------------------------------------------------------------------- /pandoc-list-table.lua: -------------------------------------------------------------------------------- 1 | local filter_info = [==========[This is filter version 202010012000 2 | 3 | This software is Copyright (c) 2020 by Benct Philip Jonsson. 4 | 5 | This is free software, licensed under: 6 | 7 | The MIT (X11) License 8 | 9 | http://www.opensource.org/licenses/mit-license.php 10 | ]==========] 11 | local concat, insert, remove, pack, unpack 12 | do 13 | local _obj_0 = table 14 | concat, insert, remove, pack, unpack = _obj_0.concat, _obj_0.insert, _obj_0.remove, _obj_0.pack, _obj_0.unpack 15 | end 16 | local floor 17 | floor = math.floor 18 | local assertion 19 | assertion = function(msg, val) 20 | return assert(val, msg) 21 | end 22 | local SimpleTable = pandoc.SimpleTable 23 | local Table = SimpleTable or pandoc.Table 24 | if not ('function' == type(SimpleTable)) then 25 | if pandoc.types and PANDOC_VERSION then 26 | local Version = pandoc.types.Version 27 | if 'function' == type(Version) then 28 | assertion("The pandoc-list-table filter does not work with Pandoc " .. tostring(PANDOC_VERSION), PANDOC_VERSION < Version('2.10.0')) 29 | end 30 | end 31 | end 32 | local call_func 33 | call_func = function(id, ...) 34 | local res = pack(pcall(...)) 35 | assert(res[1], "Error " .. tostring(id) .. ": " .. tostring(res[2])) 36 | remove(res, 1) 37 | return unpack(res) 38 | end 39 | local contains_any 40 | contains_any = function(...) 41 | local wanted 42 | do 43 | local _tbl_0 = { } 44 | local _list_0 = pack(...) 45 | for _index_0 = 1, #_list_0 do 46 | local w = _list_0[_index_0] 47 | _tbl_0[w] = true 48 | end 49 | wanted = _tbl_0 50 | end 51 | return function(list) 52 | local _exp_0 = type(list) 53 | if 'table' == _exp_0 then 54 | for _index_0 = 1, #list do 55 | local v = list[_index_0] 56 | if wanted[v] then 57 | return true 58 | end 59 | end 60 | else 61 | return nil 62 | end 63 | return false 64 | end 65 | end 66 | local is_elem 67 | is_elem = function(x, ...) 68 | local _exp_0 = type(x) 69 | if 'table' == _exp_0 then 70 | local tag = x.tag 71 | local _exp_1 = type(tag) 72 | if 'string' == _exp_1 then 73 | local tags = pack(...) 74 | if #tags > 0 then 75 | for _index_0 = 1, #tags do 76 | local t = tags[_index_0] 77 | if t == tag then 78 | return tag 79 | end 80 | end 81 | return nil 82 | end 83 | return true 84 | end 85 | return false 86 | end 87 | end 88 | local get_div_id 89 | get_div_id = function(cls, div, div_count) 90 | if div_count == nil then 91 | div_count = "" 92 | end 93 | local div_id = div.identifier or "" 94 | if "" == div_id then 95 | div_id = div_count 96 | end 97 | return tostring(cls) .. " div #" .. tostring(div_id) 98 | end 99 | local letter2align = { 100 | d = 'AlignDefault', 101 | l = 'AlignLeft', 102 | c = 'AlignCenter', 103 | r = 'AlignRight' 104 | } 105 | local align2letter 106 | do 107 | local _tbl_0 = { } 108 | for k, v in pairs(letter2align) do 109 | _tbl_0[v] = k 110 | end 111 | align2letter = _tbl_0 112 | end 113 | local contains_no_header = contains_any('no-header', 'noheader') 114 | local contains_keep_div = contains_any('keep-div', 'keepdiv') 115 | local lol2table 116 | do 117 | local div_count = 0 118 | lol2table = function(div) 119 | div_count = div_count + 1 120 | local div_id = get_div_id('lol2table', div, div_count) 121 | local lol, caption = nil, nil 122 | local _list_0 = div.content 123 | for _index_0 = 1, #_list_0 do 124 | local _continue_0 = false 125 | repeat 126 | local item = _list_0[_index_0] 127 | if not (is_elem(item)) then 128 | _continue_0 = true 129 | break 130 | end 131 | local _exp_0 = item.tag 132 | if 'BulletList' == _exp_0 or 'OrderedList' == _exp_0 then 133 | if lol then 134 | error("Expected only one list in " .. tostring(div_id), 2) 135 | end 136 | lol = item 137 | elseif 'Para' == _exp_0 or 'Plain' == _exp_0 then 138 | if caption then 139 | error("Expected only one caption paragraph in " .. tostring(div_id), 2) 140 | end 141 | caption = item.content 142 | else 143 | error("Didn't expect " .. tostring(item.tag) .. " in " .. tostring(div_id), 2) 144 | end 145 | _continue_0 = true 146 | until true 147 | if not _continue_0 then 148 | break 149 | end 150 | end 151 | if not (lol) then 152 | return nil 153 | end 154 | caption = caption or { } 155 | if not (is_elem(lol, 'BulletList', 'OrderedList')) then 156 | return nil 157 | end 158 | local header = not (contains_no_header(div.classes)) 159 | local rows = { } 160 | local col_count = 0 161 | local _list_1 = lol.content 162 | for _index_0 = 1, #_list_1 do 163 | local item = _list_1[_index_0] 164 | assertion("Expected list in " .. tostring(div_id) .. " to be list of lists", #item == 1 and is_elem(item[1], 'BulletList', 'OrderedList')) 165 | local row = item[1].content 166 | if #row > col_count then 167 | col_count = #row 168 | end 169 | rows[#rows + 1] = row 170 | end 171 | for _index_0 = 1, #rows do 172 | local row = rows[_index_0] 173 | while #row < col_count do 174 | row[#row + 1] = { } 175 | end 176 | end 177 | local headers 178 | if header then 179 | headers = remove(rows, 1) 180 | else 181 | headers = { } 182 | end 183 | local aligns = { } 184 | local align = (div.attributes.align or ""):lower() 185 | if "" == align then 186 | align = 'd' 187 | end 188 | for a in align:gmatch('[^,]+') do 189 | aligns[#aligns + 1] = assertion("Unknown column alignment in " .. tostring(div_id) .. ": '" .. tostring(a) .. "'", letter2align[a]) 190 | if #aligns == col_count then 191 | break 192 | end 193 | end 194 | while #aligns < col_count do 195 | aligns[#aligns + 1] = aligns[#aligns] 196 | end 197 | local widths = { } 198 | local width = div.attributes.widths or "" 199 | if "" == width then 200 | width = '0' 201 | end 202 | for w in width:gmatch('[^,]+') do 203 | assertion("Expected column width in " .. tostring(div_id) .. " to be percentage, not '" .. tostring(w) .. "'", w:match('^[01]?%d?%d$')) 204 | widths[#widths + 1] = tonumber(w, 10) / 100 205 | if #widths == col_count then 206 | break 207 | end 208 | end 209 | while #widths < col_count do 210 | widths[#widths + 1] = 0 211 | end 212 | local tab = call_func("converting list to table in " .. tostring(div_id), Table, caption, aligns, widths, headers, rows) 213 | if SimpleTable and 'SimpleTable' == tab.tag then 214 | tab = call_func("converting SimpleTable to Table in " .. tostring(div_id), pandoc.utils.from_simple_table, tab) 215 | end 216 | if contains_keep_div(div.classes) then 217 | local attr = div.attr 218 | local _list_2 = { 219 | 'align', 220 | 'widths' 221 | } 222 | for _index_0 = 1, #_list_2 do 223 | local key = _list_2[_index_0] 224 | attr.attributes[key] = nil 225 | end 226 | do 227 | local _accum_0 = { } 228 | local _len_0 = 1 229 | local _list_3 = div.classes 230 | for _index_0 = 1, #_list_3 do 231 | local c = _list_3[_index_0] 232 | if 'lol2table' ~= c then 233 | _accum_0[_len_0] = c 234 | _len_0 = _len_0 + 1 235 | end 236 | end 237 | attr.classes = _accum_0 238 | end 239 | insert(attr.classes, 1, 'maybe-table2lol') 240 | return pandoc.Div({ 241 | tab 242 | }, attr) 243 | end 244 | return tab 245 | end 246 | end 247 | local table2lol 248 | do 249 | local no_class = { 250 | table2lol = true, 251 | ['no-header'] = true, 252 | noheader = true 253 | } 254 | local div_count = 0 255 | table2lol = function(div) 256 | div_count = div_count + 1 257 | if #div.content == 0 then 258 | return nil 259 | end 260 | local div_id = get_div_id('table2lol', div, div_count) 261 | assertion("Expected " .. tostring(div_id) .. " to contain only a table", #div.content == 1 and is_elem(div.content[1], 'Table', 'SimpleTable')) 262 | local tab = div.content[1] 263 | if SimpleTable and 'SimpleTable' ~= tab.tag then 264 | tab = call_func("converting Table to SimpleTable in " .. tostring(div_id), pandoc.utils.to_simple_table, tab) 265 | end 266 | local caption, headers, rows = tab.caption, tab.headers, tab.rows 267 | local header = false 268 | for _index_0 = 1, #headers do 269 | local h = headers[_index_0] 270 | if #h > 0 then 271 | header = true 272 | end 273 | end 274 | local lol 275 | do 276 | local _accum_0 = { } 277 | local _len_0 = 1 278 | for _index_0 = 1, #rows do 279 | local row = rows[_index_0] 280 | _accum_0[_len_0] = { 281 | pandoc.OrderedList(row) 282 | } 283 | _len_0 = _len_0 + 1 284 | end 285 | lol = _accum_0 286 | end 287 | local list_attr = pandoc.ListAttributes() 288 | if header then 289 | insert(lol, 1, { 290 | pandoc.OrderedList(headers) 291 | }) 292 | list_attr.start = 0 293 | end 294 | lol = pandoc.OrderedList(lol, list_attr) 295 | if contains_keep_div(div.classes) then 296 | local cols = { 297 | align = (function() 298 | local _accum_0 = { } 299 | local _len_0 = 1 300 | local _list_0 = tab.aligns 301 | for _index_0 = 1, #_list_0 do 302 | local a = _list_0[_index_0] 303 | _accum_0[_len_0] = align2letter[a] 304 | _len_0 = _len_0 + 1 305 | end 306 | return _accum_0 307 | end)(), 308 | widths = (function() 309 | local _accum_0 = { } 310 | local _len_0 = 1 311 | local _list_0 = tab.widths 312 | for _index_0 = 1, #_list_0 do 313 | local w = _list_0[_index_0] 314 | _accum_0[_len_0] = floor(w * 100) 315 | _len_0 = _len_0 + 1 316 | end 317 | return _accum_0 318 | end)() 319 | } 320 | local classes 321 | do 322 | local _accum_0 = { } 323 | local _len_0 = 1 324 | local _list_0 = div.classes 325 | for _index_0 = 1, #_list_0 do 326 | local c = _list_0[_index_0] 327 | if not no_class[c] then 328 | _accum_0[_len_0] = c 329 | _len_0 = _len_0 + 1 330 | end 331 | end 332 | classes = _accum_0 333 | end 334 | if #caption > 0 then 335 | caption = pandoc.Para(caption) 336 | else 337 | caption = pandoc.Null() 338 | end 339 | if not (header) then 340 | insert(classes, 1, 'no-header') 341 | end 342 | insert(classes, 1, 'maybe-lol2table') 343 | local attr = div.attr 344 | attr.classes = classes 345 | for key, list in pairs(cols) do 346 | attr.attributes[key] = concat(list, ",") 347 | end 348 | return pandoc.Div({ 349 | lol, 350 | caption 351 | }, attr) 352 | end 353 | return lol 354 | end 355 | end 356 | do 357 | local div_count = 0 358 | return { 359 | { 360 | Div = function(self) 361 | div_count = div_count + 1 362 | local is_lol2table = self.classes:includes('lol2table') 363 | local is_table2lol = self.classes:includes('table2lol') 364 | if is_lol2table and is_table2lol then 365 | local div_id = get_div_id("", self, div_count) 366 | error("Expected" .. tostring(div_id) .. " to have class .lol2table or class .table2lol, not both") 367 | elseif is_lol2table then 368 | return lol2table(self) 369 | elseif is_table2lol then 370 | return table2lol(self) 371 | end 372 | return nil 373 | end 374 | } 375 | } 376 | end 377 | -------------------------------------------------------------------------------- /pandoc-list-table.moon: -------------------------------------------------------------------------------- 1 | 2 | filter_info = [==========[ 3 | This is filter version 202010012000 4 | 5 | This software is Copyright (c) 2020 by Benct Philip Jonsson. 6 | 7 | This is free software, licensed under: 8 | 9 | The MIT (X11) License 10 | 11 | http://www.opensource.org/licenses/mit-license.php 12 | ]==========] 13 | 14 | import concat, insert, remove, pack, unpack from table 15 | import floor from math 16 | 17 | assertion = (msg, val) -> assert val, msg 18 | 19 | -- Check if we have SimpleTable 20 | SimpleTable = pandoc.SimpleTable 21 | Table = SimpleTable or pandoc.Table 22 | 23 | unless 'function' == type SimpleTable 24 | if pandoc.types and PANDOC_VERSION 25 | Version = pandoc.types.Version 26 | -- If Version isn't a function Pandoc is surely less than 2.10 27 | if 'function' == type Version 28 | -- We know we haven't got SimpleTable, so now check if Pandoc < 2.10 29 | assertion "The pandoc-list-table filter does not work with Pandoc #{PANDOC_VERSION}", 30 | PANDOC_VERSION < Version '2.10.0' 31 | 32 | -- pcall with less boilerplate 33 | call_func = (id, ...) -> 34 | res = pack pcall ... 35 | assert res[1], "Error #{id}: #{res[2]}" 36 | remove res, 1 37 | return unpack res 38 | 39 | -- contains_any(val1 [, val2, ...]) 40 | -- returns a closure such that closure(x) returns 41 | -- * nil if x is not a table 42 | -- * true if x is an array and contains a value 43 | -- which is equal to one of val1, ... 44 | -- * false otherwise 45 | contains_any = (...) -> 46 | wanted = {w, true for w in *pack ...} 47 | return (list) -> 48 | switch type list 49 | when 'table' 50 | for v in *list 51 | if wanted[v] 52 | return true 53 | else 54 | return nil 55 | return false 56 | 57 | -- is_elem(val, tag1 [, tag2, ...]) 58 | -- returns 59 | -- * false if x is not a table 60 | -- * false if x.tag is not a string 61 | -- * x.tag if x.tag equals one of tag1, ... 62 | -- * nil otherwise 63 | -- is_elem(x) 64 | -- returns 65 | -- * false if x is not a table 66 | -- * false if x.tag is not a string 67 | -- * true otherwise 68 | is_elem = (x, ...) -> 69 | switch type x 70 | when 'table' 71 | tag = x.tag 72 | switch type tag 73 | when 'string' 74 | tags = pack ... 75 | if #tags > 0 76 | for t in *tags 77 | if t == tag 78 | return tag 79 | return nil 80 | return true 81 | return false 82 | 83 | -- get_div_id(cls, div [, div_count]) 84 | -- 85 | -- Takes the following arguments: 86 | -- 87 | -- 1. A string. May be a class name, something else which 88 | -- migh serve as a "div type", or an empty string. 89 | -- 90 | -- 2. An actual Pandoc Div object. 91 | -- 92 | -- 3. An optional number, assumed to be the number of divs of 93 | -- the same "type" already seen, including the current 94 | -- one. 95 | -- 96 | -- Returns a string of the form ` div #`, where 97 | -- `` is either the id attribute of `div`, or if 98 | -- that is empty the `div_count`. 99 | 100 | get_div_id = (cls, div, div_count="") -> 101 | div_id = div.identifier or "" 102 | div_id = div_count if "" == div_id 103 | return "#{cls} div ##{div_id}" 104 | 105 | -- Map one-letter abbreviations to full alignment type names. 106 | letter2align = { 107 | d: 'AlignDefault' 108 | l: 'AlignLeft' 109 | c: 'AlignCenter' 110 | r: 'AlignRight' 111 | } 112 | -- Map full alignment type names to one-letter abbreviations. 113 | align2letter = {v,k for k,v in pairs letter2align} 114 | 115 | -- Functions to look for variants of the 'magic' classes. 116 | contains_no_header = contains_any 'no-header', 'noheader' 117 | contains_keep_div = contains_any 'keep-div', 'keepdiv' 118 | 119 | -- Function to convert a list of lists to a table 120 | lol2table = do 121 | -- Keep track of how many lol2table divs we have seen. 122 | div_count = 0 123 | -- The function receives the enclosing div as argument 124 | (div) -> 125 | -- Increment the count 126 | div_count += 1 127 | -- Get a moniker for this div 128 | div_id = get_div_id 'lol2table', div, div_count 129 | -- Now look up the LoL and the caption paragraph if any. 130 | -- Start by declaring the variables 131 | lol, caption = nil, nil 132 | -- Now loop through the children of the div: 133 | for item in *div.content 134 | continue unless is_elem item -- can't happen! 135 | -- See what kind of element we got 136 | switch item.tag 137 | when 'BulletList', 'OrderedList' 138 | -- Complain if we already saw a list 139 | if lol 140 | error "Expected only one list in #{div_id}", 2 141 | lol = item 142 | when 'Para', 'Plain' 143 | -- Complain if we already saw a paragraph 144 | if caption 145 | error "Expected only one caption paragraph in #{div_id}", 2 146 | caption = item.content 147 | else 148 | -- Complain if we see something other than a list or para 149 | error "Didn't expect #{item.tag} in #{div_id}", 2 150 | -- Abort if we didn't see any list 151 | return nil unless lol 152 | -- The caption defaults to an empty list 153 | caption or= {} 154 | -- This can't really happen, so why is this check there? 155 | unless is_elem lol, 'BulletList', 'OrderedList' 156 | return nil 157 | -- Do we want a table with a header? 158 | header = not( contains_no_header div.classes ) 159 | -- Init the array of rows 160 | rows = {} 161 | -- Init the column count 162 | col_count = 0 163 | -- Loop through the list items 164 | for item in *lol.content 165 | -- Check that the item contains a list and nothing else 166 | assertion "Expected list in #{div_id} to be list of lists", 167 | #item == 1 and is_elem item[1], 'BulletList', 'OrderedList' 168 | -- The items of the inner list are the next table row 169 | row = item[1].content 170 | -- If this row is longer than any seen before 171 | -- we update the column count 172 | if #row > col_count 173 | col_count = #row 174 | rows[#rows+1] = row 175 | -- Make sure all rows are the same length by adding empty 176 | -- cells until they are the same length as the longest row 177 | for row in *rows 178 | while #row < col_count 179 | row[#row+1] = {} 180 | -- If we want a header use the first row, 181 | -- else set the headers to an empty list 182 | headers = if header 183 | remove rows, 1 184 | else 185 | {} 186 | -- Init the list of aligns 187 | aligns = {} 188 | -- Get the align attribute if any and coerce it to lowercase 189 | align = (div.attributes.align or "")\lower! 190 | -- If the align attr is empty it defaults to a single d 191 | align = 'd' if "" == align 192 | -- Now step through the comma-separated "items" 193 | for a in align\gmatch '[^,]+' 194 | -- Check that we have a valid "align-letter" and 195 | -- append its expansion to the list of aligns 196 | aligns[#aligns+1] = assertion "Unknown column alignment in #{div_id}: '#{a}'", 197 | letter2align[a] 198 | -- Don't look any further if we got the right number of aligns 199 | if #aligns == col_count 200 | break 201 | -- If we got too few aligns pad out with copies of the last 202 | while #aligns < col_count 203 | aligns[#aligns+1] = aligns[#aligns] 204 | -- Now do the same with widths 205 | widths = {} 206 | width = div.attributes.widths or "" 207 | -- Widths default to automatic widths 208 | width = '0' if "" == width 209 | for w in width\gmatch '[^,]+' 210 | -- A width is a percentage of the available total width, 211 | -- tableso an integer with up to three digits 212 | assertion "Expected column width in #{div_id} to be percentage, not '#{w}'", 213 | w\match '^[01]?%d?%d$' 214 | -- Convert it to a float and append it to the 215 | -- list of widths 216 | widths[#widths+1] = tonumber(w, 10) / 100 217 | if #widths == col_count 218 | break 219 | while #widths < col_count 220 | -- Pad with auto widths if we got too few widths 221 | widths[#widths+1] = 0 222 | -- See if we can create a table 223 | -- and give a nice error message if we fail 224 | tab = call_func "converting list to table in #{div_id}", Table, caption, aligns, widths, headers, rows 225 | if SimpleTable and 'SimpleTable' == tab.tag 226 | tab = call_func "converting SimpleTable to Table in #{div_id}", 227 | pandoc.utils.from_simple_table, tab 228 | -- Do we want to keep the div? 229 | if contains_keep_div div.classes 230 | -- Reuse the attrs of the old div as far as possible! 231 | attr = div.attr 232 | -- Remove any old align/widths attrs since they may 233 | -- become inaccurate if the table is altered. 234 | for key in *{'align', 'widths'} 235 | attr.attributes[key] = nil 236 | -- Remove the lol2table class which certainly is 237 | -- wrong now 238 | attr.classes = [c for c in *div.classes when 'lol2table' != c] 239 | -- but make it easy for the user to revert to a LoL 240 | -- should they want to! 241 | insert attr.classes, 1, 'maybe-table2lol' 242 | -- Return a div with the table and the attributes 243 | return pandoc.Div {tab}, attr 244 | -- Else don't keep the div, just return the table! 245 | return tab 246 | 247 | table2lol = do 248 | no_class = table2lol: true, 'no-header': true, noheader: true 249 | div_count = 0 250 | (div) -> 251 | div_count += 1 252 | return nil if #div.content == 0 253 | div_id = get_div_id 'table2lol', div, div_count 254 | assertion "Expected #{div_id} to contain only a table", 255 | #div.content == 1 and is_elem div.content[1], 'Table', 'SimpleTable' 256 | tab = div.content[1] 257 | if SimpleTable and 'SimpleTable' ~= tab.tag 258 | tab = call_func "converting Table to SimpleTable in #{div_id}", 259 | pandoc.utils.to_simple_table, tab 260 | caption, headers, rows = tab.caption, tab.headers, tab.rows 261 | header = false 262 | for h in *headers 263 | header = true if #h > 0 264 | lol = [ {pandoc.OrderedList(row)} for row in *rows ] 265 | list_attr = pandoc.ListAttributes! 266 | if header 267 | insert lol, 1, {pandoc.OrderedList(headers)} 268 | list_attr.start = 0 269 | lol = pandoc.OrderedList lol, list_attr 270 | if contains_keep_div div.classes 271 | cols = { 272 | align: [align2letter[a] for a in *tab.aligns] 273 | widths: [floor(w * 100) for w in *tab.widths] 274 | } 275 | classes = [ c for c in *div.classes when not no_class[c] ] 276 | caption = if #caption > 0 277 | pandoc.Para caption 278 | else 279 | pandoc.Null! 280 | unless header 281 | insert classes, 1, 'no-header' 282 | insert classes, 1, 'maybe-lol2table' 283 | attr = div.attr 284 | attr.classes = classes 285 | for key, list in pairs cols 286 | attr.attributes[key] = concat list, "," 287 | return pandoc.Div {lol, caption}, attr 288 | return lol 289 | 290 | 291 | 292 | 293 | 294 | return do 295 | div_count = 0 296 | { 297 | { 298 | Div: => 299 | div_count += 1 300 | is_lol2table = @classes\includes 'lol2table' 301 | is_table2lol = @classes\includes 'table2lol' 302 | if is_lol2table and is_table2lol 303 | div_id = get_div_id "", @, div_count 304 | error "Expected#{div_id} to have class .lol2table or class .table2lol, not both" 305 | elseif is_lol2table 306 | return lol2table @ 307 | elseif is_table2lol 308 | return table2lol @ 309 | return nil 310 | } 311 | } 312 | 313 | -------------------------------------------------------------------------------- /test.md: -------------------------------------------------------------------------------- 1 | ::: {.lol2table .keep-div widths="20,40,40" } 2 | 3 | A table of stuff. 4 | 5 | - - foo 6 | - bar 7 | - baz 8 | - - - tic 9 | - pic 10 | - - tac 11 | - pac 12 | - - toc 13 | - poc 14 | ::: 15 | 16 | ::: {.table2lol .keep-div} 17 | 18 | |foo|bar|baz 19 | |---|---|--- 20 | |tic|tac|toc 21 | 22 | ::: 23 | 24 | ::: {.table2lol .keep-div} 25 | 26 | | 27 | |---|---|--- 28 | |foo|bar|baz 29 | |tic|tac|toc 30 | 31 | : Headerless table 32 | 33 | ::: 34 | 35 | --------------------------------------------------------------------------------