local U = require("scholatex-util") local function split_cells(line) local cells, buf, i, n, inmath = {}, {}, 1, #line, false while i <= n do local c = line:sub(i, i) if c == "\\" then buf[#buf+1] = line:sub(i, i+1); i = i + 2 elseif c == "$" then inmath = not inmath; buf[#buf+1] = c; i = i + 1 elseif c == "|" and not inmath then cells[#cells+1] = U.trim(table.concat(buf)) buf = {}; i = i + 1 else buf[#buf+1] = c; i = i + 1 end end cells[#cells+1] = U.trim(table.concat(buf)) return cells end local function parse_cell(cell) if cell == "." then return { absorbed = true, width = 1 } end if not cell:match("^<%s*[cr][%a]*span:") then return { text = cell, width = 1 } end local words, content = cell:match("^<%s*(.-)%s*>%s*{(.*)}%s*$") if not words then return { text = cell, width = 1 } end local c = { width = 1, text = content } for w in words:gmatch("%S+") do local cs = w:match("^colspan:(%d+)$") local rs = w:match("^rowspan:(%d+)$") if cs then if tonumber(cs) < 2 then error("scholatex: colspan must be 2 or more (got " .. cs .. ")") end c.colspan = tonumber(cs); c.width = c.colspan elseif rs then if tonumber(rs) < 2 then error("scholatex: rowspan must be 2 or more (got " .. rs .. ")") end c.rowspan = tonumber(rs) elseif #w == 2 and U.place_code(w) then local v, h = U.place_code(w) c.align = ({left="l", center="c", right="r"})[h] c.valign = ({top="t", center="m", bottom="b"})[v] else error("scholatex: unknown table cell tag: '" .. w .. "'; use colspan:N, " .. "rowspan:N, or a two-letter placement code like tl, mc, br") end end return c end local function row_cells(line) local out = {} for _, raw in ipairs(split_cells(line)) do out[#out+1] = parse_cell(raw) end return out end local function row_width(cells) local w = 0 for _, c in ipairs(cells) do w = w + c.width end return w end local function parse_table_opts(words_str) local o = { cols = nil, borders = false, header = false, gap = nil } local spec = words_str:match("%[(.-)%]") if spec then o.cols = {} for rawfield in (spec .. ","):gmatch("(.-),") do local field = U.trim(rawfield) if field ~= "" then local width, code = field:match("^(%d+):(%a+)$") if not width then code = field:match("^(%a+)$") width = nil end local align, valign if code and #code == 2 then local v, h = U.place_code(code) if v then valign = ({top="t", center="m", bottom="b"})[v] align = ({left="l", center="c", right="r"})[h] end end if not align then error("scholatex: invalid table column '" .. field .. "'; every column needs a two-letter placement code " .. "(vertical t/m/b, then horizontal l/c/r) such as tl, mc or " .. "br, optionally prefixed with a width: 30:mc") end o.cols[#o.cols+1] = { width = width, align = align, valign = valign } end end words_str = words_str:gsub("%[.-%]", " ") end for w in words_str:gmatch("%S+") do if w == "borders" then o.borders = true elseif w == "header" or w == "headers" then o.header = true else local key, val = w:match("^([%a_]+):(.+)$") if key == "gap" then o.gap = val elseif key == "fill" then o.fill = val elseif key == "line" then o.line = val elseif key == "text" then o.text = val elseif key == "headerfill" then o.headerfill = val elseif key == "headertext" then o.headertext = val elseif key == "radius" or key == "title" or key == "width" then error("scholatex: has no '" .. key .. "' option; it applies to a " .. "box, not table cells. Wrap the table in a to frame it.") elseif key then error("scholatex: unknown
option: '" .. w .. "'") else error("scholatex: unknown
option: '" .. w .. "'") end end end return o end local VMAP = {t = "h", m = "m", b = "f"} local function colspec(o, ncols) local parts = {} for ci = 1, ncols do local col = o.cols and o.cols[ci] local align = (col and col.align) or "c" local valign = VMAP[(col and col.valign) or "t"] or "h" local width = col and col.width if width then parts[ci] = "Q[" .. align .. "," .. valign .. "," .. width .. "mm]" else parts[ci] = "X[" .. align .. "," .. valign .. "]" end end return table.concat(parts) end return function(sl) sl.register_block("table", function(api, words_str, inner) local opts = parse_table_opts(words_str or "") local ncols = 1 for _, l in ipairs(inner) do if type(l) == "string" and not l:match("^%s*for%s") and not l:match("^%s*if%s") and not l:match("^%s*}") and not l:match("^%s*else") and l:match("%S") then ncols = row_width(row_cells(l)); break end end local cs = colspec(opts, ncols) local cn = sl.box_color_name local rulecolor = opts.line and cn(opts.line) local fillcolor = opts.fill and cn(opts.fill) local textcolor = opts.text and cn(opts.text) local hfillcolor = opts.headerfill and cn(opts.headerfill) local htextcolor = opts.headertext and cn(opts.headertext) local width = "\\dimexpr\\linewidth-" .. (2 * ncols) .. "\\tabcolsep\\relax" local settings = { "colspec={" .. cs .. "}", "width=" .. width } if opts.gap then settings[#settings+1] = "colsep=" .. opts.gap .. "mm" end if opts.borders then if rulecolor then settings[#settings+1] = "hlines={" .. rulecolor .. "}" settings[#settings+1] = "vlines={" .. rulecolor .. "}" else settings[#settings+1] = "hlines" settings[#settings+1] = "vlines" end end if fillcolor or textcolor then local c = {} if fillcolor then c[#c+1] = "bg=" .. fillcolor end if textcolor then c[#c+1] = "fg=" .. textcolor end settings[#settings+1] = "cells={" .. table.concat(c, ",") .. "}" end if opts.header then local h = { "font=\\bfseries" } if hfillcolor then h[#h+1] = "bg=" .. hfillcolor end if htextcolor then h[#h+1] = "fg=" .. htextcolor end settings[#settings+1] = "row{1}={" .. table.concat(h, ",") .. "}" end local preamble_esc = table.concat(settings, ", "):gsub("\\", "\\\\") api.raw('emit("\\\\begin{tblr}{' .. preamble_esc .. '}")\n') local function emit_cell_text(cellt, color) if color then api.raw('emit("{\\\\color{' .. color .. '}")\n') end api.forward_text(cellt) if color then api.raw('emit("}")\n') end end local rownum = 0 local depth = 0 local held = {} for c = 1, ncols do held[c] = 0 end local ri, rtotal = 1, #inner while ri <= rtotal do local l = inner[ri] if type(l) == "string" and l:match("^%s*}%s*$") then api.raw("end\n"); depth = depth - 1; ri = ri + 1 elseif type(l) == "string" and api.is_control_open(l) then api.raw(api.lua_control(l) .. "\n"); depth = depth + 1; ri = ri + 1 elseif type(l) == "string" and l:match("%S") then rownum = rownum + 1 local cells = row_cells(l) local rw = row_width(cells) if rw ~= ncols then error("scholatex: table row covers " .. rw .. " columns, " .. ncols .. " expected") end local colidx = 1 for ci, cell in ipairs(cells) do if ci > 1 then api.raw('emit(" & ")\n') end if cell.absorbed then -- a rowspan opened above occupies this slot: leave the cell empty elseif cell.colspan then local h = cell.align or "c" api.raw('emit("\\\\SetCell[c=' .. cell.colspan .. ']{' .. h .. '} ")\n') emit_cell_text((cell.text or ""):gsub("\\\\", "\\newline "), nil) for _ = 2, cell.colspan do api.raw('emit(" &")\n') end elseif cell.rowspan then local v = VMAP[cell.valign or "t"] or "h" api.raw('emit("\\\\SetCell[r=' .. cell.rowspan .. ']{' .. v .. '} ")\n') emit_cell_text((cell.text or ""):gsub("\\\\", "\\newline "), nil) held[colidx] = cell.rowspan else emit_cell_text((cell.text or ""):gsub("\\\\", "\\newline "), nil) end colidx = colidx + cell.width end api.raw('emit(" \\\\\\\\ ")\n') for c = 1, ncols do if held[c] > 0 then held[c] = held[c] - 1 end end ri = ri + 1 else ri = ri + 1 end end while depth > 0 do api.raw("end\n"); depth = depth - 1 end api.raw('emit("\\\\end{tblr}")\n') end) end