-- ChartSS.lua -- Deltas from the boilerplate pandoc filter file are Copyright Andrew Marble, 2020 -- -- Invoke with: pandoc -t ChartSS.lua -- -- Note: you need not have lua installed on your system to use this -- custom writer. However, if you do have lua installed, you can -- use it to test changes to the script. 'lua sample.lua' will -- produce informative error messages if your code contains -- syntax errors. local pipe = pandoc.pipe local stringify = (require "pandoc.utils").stringify -- The global variable PANDOC_DOCUMENT contains the full AST of -- the document which is going to be written. It can be used to -- configure the writer. local meta = PANDOC_DOCUMENT.meta -- Chose the image format based on the value of the -- `image_format` meta value. local image_format = meta.image_format and stringify(meta.image_format) or "png" local image_mime_type = ({ jpeg = "image/jpeg", jpg = "image/jpeg", gif = "image/gif", png = "image/png", svg = "image/svg+xml", })[image_format] or error("unsupported image format `" .. img_format .. "`") -- Character escaping local function escape(s, in_attribute) return s:gsub("[<>&\"']", function(x) if x == '<' then return '<' elseif x == '>' then return '>' elseif x == '&' then return '&' elseif x == '"' then return '"' elseif x == "'" then return ''' else return x end end) end -- Helper function to convert an attributes table into -- a string that can be put into HTML tags. local function attributes(attr) local attr_table = {} for x,y in pairs(attr) do if y and y ~= "" then table.insert(attr_table, ' ' .. x .. '="' .. escape(y,true) .. '"') end end return table.concat(attr_table) end -- Table to store footnotes, so they can be included at the end. local notes = {} -- Blocksep is used to separate block elements. function Blocksep() return "\n\n" end -- This function is called once for the whole document. Parameters: -- body is a string, metadata is a table, variables is a table. -- This gives you a fragment. You could use the metadata table to -- fill variables in a custom lua template. Or, pass `--template=...` -- to pandoc, and pandoc will add do the template processing as -- usual. function Doc(body, metadata, variables) local buffer = {} local function add(s) table.insert(buffer, s) end add('') add('') add('') add('') -- a default good style add('') add('') add('') add(body) if #notes > 0 then add('
    ') for _,note in pairs(notes) do add(note) end add('
') end add('') add('') return table.concat(buffer,'\n') .. '\n' end -- The functions that follow render corresponding pandoc elements. -- s is always a string, attr is always a table of attributes, and -- items is always an array of strings (the items in a list). -- Comments indicate the types of other variables. function Str(s) return escape(s) end function Space() return " " end function SoftBreak() return "\n" end function LineBreak() return "
" end function Emph(s) return "" .. s .. "" end function Strong(s) return "" .. s .. "" end function Subscript(s) return "" .. s .. "" end function Superscript(s) return "" .. s .. "" end function SmallCaps(s) return '' .. s .. '' end function Strikeout(s) return '' .. s .. '' end function Link(s, src, tit, attr) return "" .. s .. "" end function Image(s, src, tit, attr) return "" end function Code(s, attr) return "" .. escape(s) .. "" end function InlineMath(s) return "\\(" .. escape(s) .. "\\)" end function DisplayMath(s) return "\\[" .. escape(s) .. "\\]" end function SingleQuoted(s) return "‘" .. s .. "’" end function DoubleQuoted(s) return "“" .. s .. "”" end function Note(s) local num = #notes + 1 -- insert the back reference right before the final closing tag. s = string.gsub(s, '(.*)' .. s .. '') -- return the footnote reference, linked to the note. return '' .. num .. '' end function Span(s, attr) return "" .. s .. "" end function RawInline(format, str) if format == "html" then return str else return '' end end function Cite(s, cs) local ids = {} for _,cit in ipairs(cs) do table.insert(ids, cit.citationId) end return "" .. s .. "" end function Plain(s) return s end function Para(s) return "

" .. s .. "

" end -- lev is an integer, the header level. function Header(lev, s, attr) return "" .. s .. "" end function BlockQuote(s) return "
\n" .. s .. "\n
" end function HorizontalRule() return "
" end function LineBlock(ls) return '
' .. table.concat(ls, '\n') .. '
' end function CodeBlock(s, attr) -- If code block has class 'dot', pipe the contents through dot -- and base64, and include the base64-encoded png as a data: URL. if attr.class and string.match(' ' .. attr.class .. ' ',' dot ') then local img = pipe("base64", {}, pipe("dot", {"-T" .. image_format}, s)) return '' -- otherwise treat as code (one could pipe through a highlighter) else return "
" .. escape(s) ..
           "
" end end function mysplit (inputstr, sep) if sep == nil then sep = "%s" end local t={} for str in string.gmatch(inputstr, "([^"..sep.."]+)") do table.insert(t, str) end return t end function trim5(s) return s:match'^%s*(.*%S)' or '' end function BarChart(items) local buffer2 = {} local nums = {} for _, item in pairs(items) do local o = mysplit(item,":") if # o == 2 then if tonumber(o[2]) ~= nil then table.insert(nums,tonumber(o[2])) else return nil end else return nil end end table.sort(nums) local m = nums[#nums] for _, item in pairs(items) do local o = mysplit(item,":") table.insert(buffer2, "
  • " .. trim5(o[1]) .. " : " .. trim5(o[2]) .. "
  • ") end table.insert(buffer2, "
  • ") -- for loop from 0 to m/0.9 in five steps: for step=0,4 do table.insert(buffer2,"") end return "
      \n" .. table.concat(buffer2, "\n") .. "\n
    " end function LineChart(items) local buffer2 = {} local nx = {} local ny = {} local sx = {} local sy = {} for _, item in pairs(items) do local o = mysplit(item,":") if # o == 2 then if tonumber(o[2]) ~= nil and tonumber (o[1]) ~= nil then -- print (o[1],o[2]) table.insert(nx,tonumber(o[1])) table.insert(ny,tonumber(o[2])) table.insert(sx,tonumber(o[1])) table.insert(sy,tonumber(o[2])) else return nil end else return nil end end -- only works for a constant gap if #nx < 2 then return nil end local gap = nx[2]-nx[1] for t=2,#nx do if (nx[t]-nx[t-1] ) ~= gap then return nil end end table.sort(sx) table.sort(sy) local maxx = sx[#sx] local minx = sx[1] local maxy = sy[#sy] local miny = sy[1] -- pad a bit local padx = (maxx-minx)*.05 local pady = (maxy-miny)*.05 maxx = maxx + padx minx = minx -padx maxy = maxy + padx miny = miny -padx -- now draw it table.insert(buffer2,"
    ") -- add y ticks in descending order table.insert(buffer2,"
    ") if maxy==miny then table.insert(buffer2,"") else for i=1,6 do local t = maxy - (i-1)*(maxy-miny)/5 table.insert(buffer2,"") end end table.insert(buffer2,"
    ") table.insert(buffer2,"
    ") table.insert(buffer2,"
  • " .. nx[1] .. " : " .. ny[1] .. "
  • ") for i = 2,#nx do local yf = string.format("%.1f",(ny[i-1]-miny)/(maxy-miny)*100) local yt = string.format("%.1f",(ny[i]-miny)/(maxy-miny)*100) -- has to start at the lowest local dir = "to top left" if (ny[i-1]>ny[i]) then local tmep = yt yt=yf yf=tmep dir="to top right" end if yf==yt then table.insert(buffer2,"
  • " .. nx[i] .. " : " .. ny[i] .. "
  • ") else table.insert(buffer2,"
  • " .. nx[i] .. " : " .. ny[i] .. "
  • ") end end -- add x ticks, inside the flex-column table.insert(buffer2,"
    ") if maxx==minx then table.insert(buffer2,"") else for i=1,6 do local t = minx+(i-1)*(maxx-minx)/5 table.insert(buffer2,"") end end table.insert(buffer2,"
    ") return table.concat(buffer2,"\n") end function ScatterPlot(items) local buffer2 = {} local nx = {} local ny = {} local sx = {} local sy = {} for _, item in pairs(items) do local o,p = item:gmatch "([+-]?%d*%.?%d+)%s*,%s*([+-]?%d*%.?%d+)"() if tonumber(o)==nil or tonumber(p)==nil then return nil end table.insert(nx,tonumber(o)) table.insert(sx,tonumber(o)) table.insert(ny,tonumber(p)) table.insert(sy,tonumber(p)) end table.sort(sx) table.sort(sy) local maxx = sx[#sx] local minx = sx[1] local maxy = sy[#sy] local miny = sy[1] -- pad a bit local padx = (maxx-minx)*.05 local pady = (maxy-miny)*.05 maxx = maxx + padx minx = minx -padx maxy = maxy + padx miny = miny -padx table.insert(buffer2,"
    ") -- add y ticks in descending order table.insert(buffer2,"
    ") if maxy==miny then table.insert(buffer2,"") else for i=1,6 do local t = maxy - (i-1)*(maxy-miny)/5 table.insert(buffer2,"") end end table.insert(buffer2,"
    ") table.insert(buffer2,"
    ") for i=1,#nx do local xp=nil local yp if maxx==minx then xp=50 else xp = string.format("%.0f",(nx[i]-minx)/(maxx-minx)*100) end if maxy==miny then yp=50 else yp= string.format("%.0f",(ny[i]-miny)/(maxy-miny)*100) end table.insert(buffer2,"
  • (" .. nx[i] .. "," .. ny[i] .. ")
  • " ) end -- add x ticks, inside the flex-column table.insert(buffer2,"
    ") if maxx==minx then table.insert(buffer2,"") else for i=1,6 do local t = minx+(i-1)*(maxx-minx)/5 table.insert(buffer2,"") end end table.insert(buffer2,"
    ") return table.concat(buffer2,"\n") end function StackedBar (items) local buffer2 = {} local nums = {} for _, item in pairs(items) do if item:sub(#item,#item) == '+' then item = item:sub(1,#item-1) else return nil end local o = mysplit(item,":") if # o == 2 then if tonumber(o[2]) ~= nil then table.insert(nums,tonumber(o[2])) else return nil end else return nil end end local maxy=0 for _,ba in ipairs(nums) do maxy=maxy+ba end local maxy = maxy*1.05 -- shouldnt hard code table.insert(buffer2,"
    ") -- add y ticks in descending order table.insert(buffer2,"
    ") if maxy==0 then return nil else for i=1,6 do local t = maxy - (i-1)*(maxy)/5 table.insert(buffer2,"") end end table.insert(buffer2,"
    \n
    ") for _, item in pairs(items) do item = item:sub(1,#item-1) local o = mysplit(item,":") table.insert(buffer2,"
  • " .. trim5(o[1]) .. ": " .. trim5(o[2]) .. "
  • ") end table.insert(buffer2,"
    ") return "" end function Waterfall(items) local buffer2 = {} local nums = {} for i =2,#items do local item=items[i] if item:sub(#item,#item) == '+' then item = item:sub(1,#item-1) else return nil end local o = mysplit(item,":") if # o == 2 then if tonumber(o[2]) ~= nil then table.insert(nums,tonumber(o[2])) else return nil end else return nil end end local item=items[1] item = item:sub(1,#item-1) local maxy = mysplit(item,":")[2] if tonumber(maxy) == nil then return nil end maxy = maxy*1.05 -- shouldnt hard code -- now draw it table.insert(buffer2,"
    ") -- add y ticks in descending order table.insert(buffer2,"
    ") if maxy==0 then return nil else for i=1,6 do local t = maxy - (i-1)*(maxy)/5 table.insert(buffer2,"") end end table.insert(buffer2,"
    ") table.insert(buffer2,"
    ") local last_hp=1/1.05*100 for _,item in pairs(items) do local item = item:sub(1,#item-1) local label = trim5(mysplit(item,":")[1]) local value = tonumber(mysplit(item,":")[2]) local height_percent = string.format("%.1f",value/maxy*100) local yf = string.format("%.1f",last_hp-(value/maxy)*100) -- hard code if _ > 1 then last_hp =last_hp - value/maxy*100 end table.insert(buffer2,"
  • " .. item .. "
  • ") end -- add x ticks, inside the flex-column table.insert(buffer2,"
    ") table.insert(buffer2,"
    ") return table.concat(buffer2,"\n") end function DefaultBulletList(items) local buffer = {} for _, item in pairs(items) do table.insert(buffer, "
  • " .. item .. "
  • ") end return "" end function BulletList(items) -- Check and see if it is a list that can be parsed into : local l1 = trim5(items[1]) local la=nil local lb=nil la,lb= l1:gmatch "([+-]?%d*%.?%d+)%s*,%s*([+-]?%d*%.?%d+)"() -- see if items[1] matches one of the data point formats local buffer = nil if # mysplit(l1,":")==2 and tonumber(mysplit(l1,":")[2]) ~= nil then if tonumber(mysplit(l1,":")[1]) ~= nil then buffer = LineChart(items) end if buffer==nil then buffer = BarChart(items) end elseif l1:sub(#l1,#l1)=='+' then -- try to parse it like a stacked bar buffer = StackedBar(items) elseif l1:sub(#l1,#l1)=='=' then -- try to parse it like a waterfall buffer = Waterfall(items) elseif tonumber(la)~=nil and tonumber(lb)~=nil then buffer = ScatterPlot(items) end if buffer ~= nil then return buffer else return DefaultBulletList(items) end end function OrderedList(items) local buffer = {} for _, item in pairs(items) do table.insert(buffer, "
  • " .. item .. "
  • ") end return "
      \n" .. table.concat(buffer, "\n") .. "\n
    " end function DefinitionList(items) local buffer = {} for _,item in pairs(items) do local k, v = next(item) table.insert(buffer, "
    " .. k .. "
    \n
    " .. table.concat(v, "
    \n
    ") .. "
    ") end return "
    \n" .. table.concat(buffer, "\n") .. "\n
    " end -- Convert pandoc alignment to something HTML can use. -- align is AlignLeft, AlignRight, AlignCenter, or AlignDefault. function html_align(align) if align == 'AlignLeft' then return 'left' elseif align == 'AlignRight' then return 'right' elseif align == 'AlignCenter' then return 'center' else return 'left' end end function CaptionedImage(src, tit, caption, attr) return '
    \n\n' .. '

    ' .. caption .. '

    \n
    ' end -- Caption is a string, aligns is an array of strings, -- widths is an array of floats, headers is an array of -- strings, rows is an array of arrays of strings. function Table(caption, aligns, widths, headers, rows) local buffer = {} local function add(s) table.insert(buffer, s) end add("") if caption ~= "" then add("") end if widths and widths[1] ~= 0 then for _, w in pairs(widths) do add('') end end local header_row = {} local empty_header = true for i, h in pairs(headers) do local align = html_align(aligns[i]) table.insert(header_row,'') empty_header = empty_header and h == "" end if empty_header then head = "" else add('') for _,h in pairs(header_row) do add(h) end add('') end local class = "even" for _, row in pairs(rows) do class = (class == "even" and "odd") or "even" add('') for i,c in pairs(row) do add('') end add('') end add('
    " .. caption .. "
    ' .. h .. '
    ' .. c .. '
    ') return table.concat(buffer,'\n') end function RawBlock(format, str) if format == "html" then return str else return '' end end function Div(s, attr) return "\n" .. s .. "" end -- The following code will produce runtime warnings when you haven't defined -- all of the functions you need for the custom writer, so it's useful -- to include when you're working on a writer. local meta = {} meta.__index = function(_, key) io.stderr:write(string.format("WARNING: Undefined function '%s'\n",key)) return function() return "" end end setmetatable(_G, meta)