---@diagnostic disable: undefined-field require "src.utils"; local json = require "src.json"; local function reader(fd, ...) if type(fd) == "string" then fd = assert(io.open(fd, ... or "r")); elseif type(fd) == "function" then return fd; else assert(fd, ...); end return function () if fd == nil then error "File is closed" end local res = fd:read(1024); if res == nil then fd:close(); fd = nil; return nil; else return res; end end; end local function read_to_str(fd, ...) local res = {}; for val in reader(fd, ...) do res[#res + 1] = val; end return table.concat(res); end local flag = {}; function flag.has(name, ...) if name == ... then return true; elseif ... == nil then return false; else return flag.has(name, select(2, ...)); end end function flag.add(name, ...) if flag.has(name, ...) then return ...; else return name, ...; end end local converters = {}; local function sanitize(str) return (str :gsub("<", "<") :gsub(">", ">") ); end local function get_indent(ctx, n) return ("\t"):rep((ctx.indent or 0) + (n or 0)); end local function indent(ctx) ctx.indent = (ctx.indent or 0) + 1; end local function undent(ctx) ctx.indent = (ctx.indent or 1) - 1; end local function convert(data, ctx, ...) local func = converters[data.t]; if func == nil then error(table.concat { "Unknown node '", data.t, "': ", to_readable(data) }); else return func(data.c, ctx, ...); end end local function convert_all(arr, ctx, ...) local res = array {}; local plain = array {}; local inline = true; for i = 1, #arr do local a, b, c = convert(arr[i], ctx, ...); res:append(a); plain:append(b); if not c then inline = false; res:append("\n", get_indent(ctx)); end end return res, plain, inline; end local function make_id(id, readable, ctx) ctx.ids = ctx.ids or {}; local res; if ctx.ids[id] ~= nil then ctx.ids[id] = ctx.ids[id] + 1; res = id .. "-" .. ctx.ids[id]; else ctx.ids[id] = 0; res = id; end if readable then ctx.citables = ctx.citables or {}; ctx.citables[id] = readable; end return res; end local function last_id(id, ctx) ctx.ids = ctx.ids or {}; if ctx.ids[id] ~= nil and ctx.ids[id] > 0 then return id .. "-" .. ctx.ids[id]; else return id; end end local function count_headers(array) local res = 0; for i = 1, #array do if not array[i].ignored then res = res + 1; end end return res; end local function read_attributes(data, readable, ctx, no_id) local res = {}; if data[1] ~= "" then res.id = data[1]; end for i = 1, #data[2] do res[data[2][i]] = true; end for i = 1, #data[3] do local curr = data[3][i]; res[curr[1]] = curr[2]; end if not no_id and res.id then res.id = make_id(res.id, readable, ctx); end return res; end function converters.Table(data, ctx) local options = data[3]; local header = data[4]; local body = data[5][1]; local text = array {}; local function convert_cell(cell, tag, options) local align = options[1].t; indent(ctx); text:push("<", tag); if align == "AlignRight" then text:push [[ class="right"]]; elseif align == "AlignLeft" then text:push [[ class="left"]]; elseif align == "AlignCenter" then text:push [[ class="center"]]; end text:push(">\n", get_indent(ctx)); text:append((convert_all(cell[5], ctx))); undent(ctx); text:push("\n", get_indent(ctx), ""); end local function convert_row(row, tag) text:push(""); for i = 1, #row[2] do local cell = row[2][i]; indent(ctx); text:push("\n", get_indent(ctx)); convert_cell(cell, tag, options[i]); undent(ctx); end text:push("\n", get_indent(ctx), ""); end text:push [[]]; indent(ctx); indent(ctx); text:push("\n", get_indent(ctx, -1), ""); text:push("\n", get_indent(ctx)); convert_row(header[2][1], "th"); text:push("\n", get_indent(ctx, -1), ""); text:push("\n", get_indent(ctx, -1), ""); for i = 1, #body[4] do text:push("\n", get_indent(ctx)); convert_row(body[4][i], "td"); end text:push("\n", get_indent(ctx, -1), ""); undent(ctx); undent(ctx); text:push("\n", get_indent(ctx), "
"); return text, array { "[table]" }; end function converters.BulletList(data, ctx) local text = array { ""); return text, plain; end function converters.OrderedList(data, ctx) local text = array { "
    " }; local plain = array {}; indent(ctx); for i = 1, #data[2] do local el_text, el_plain = convert_all(data[2][i], ctx); text :push("\n", get_indent(ctx), "
  1. ") :append(el_text) :push("
  2. "); plain:push(i .. ") "):append(el_plain):push("\n"); end undent(ctx); text:push("\n", get_indent(ctx), "
"); return text, plain; end function converters.Code(data, ctx) local content = data[2]; return array { "", sanitize(content), "" }, array { content }; end function converters.CodeBlock(data, ctx) local attribs = read_attributes(data[1], nil, ctx); local content = data[2]; local text = array { "
 0 then
		text:push(" class=\"", classes:join " ", "\"");
	end

	text:push(">", sanitize(content), "
"); return text, array { content }; end function converters.Image(data, ctx) local alt, alt_plain = convert_all(data[2], ctx); local url = data[3][1]; local title = data[3][2]; local attribs = read_attributes(data[1], title or alt_plain:join " ", ctx); local classes = array {}; if attribs.small then classes:push("small") end if attribs.big then classes:push("big") end local text = array {} :push("\"") 0 then text:push(" class=\"", classes:join " ", "\""); end text:push("/>"); return text, title and { title } or alt_plain or url or "[picture]"; end function converters.Figure(data, ctx) local chapter_i = #(ctx.headers or {}); ctx.figures_indices = ctx.figures_indices or {}; ctx.figures_indices[chapter_i] = (ctx.figures_indices[chapter_i] or 0) + 1; local figure_i = ctx.figures_indices[chapter_i]; local prefix = ("%s.%s."):format(chapter_i, figure_i); local attribs = read_attributes(data[1], prefix, ctx); local id = attribs.id; indent(ctx); indent(ctx); local name, name_plain = convert_all(data[2][1], ctx); local content, plain = convert_all(data[3], ctx); undent(ctx); undent(ctx); local text = array { ""); text :push("\n", get_indent(ctx, 1), "
", "\n", get_indent(ctx, 2)) :append(content) :push("\n", get_indent(ctx, 1), "
", "\n", get_indent(ctx, 1), "
", "\n", get_indent(ctx, 2)) :push(prefix, " ") :append(name) :push("\n", get_indent(ctx, 1), "
") :push("\n", get_indent(ctx), ""); plain :push(" (", prefix, " ") :append(name_plain) :push(")"); ctx.citables = ctx.citables or {}; if id then ctx.citables[id] = prefix end return text, plain; end function converters.Div(data, ctx) local attribs = read_attributes(data[1], nil, ctx, true); if attribs.figure then local separator_i = data[2]:find_i(function (v) return v.t == "HorizontalRule" end); local content_data, title_data; if separator_i == nil then content_data = array { data[2][1] }; title_data = data[2]:slice(2); else content_data = data[2]:slice(1, separator_i - 1); title_data = data[2]:slice(separator_i + 1); end if #title_data == 1 and title_data[1].t == "Para" then title_data = title_data[1].c end return converters.Figure(array { data[1], array { title_data }, content_data, }, ctx); else error "Divs are not supported"; end end function converters.Cite(data, ctx) local citation = data[1][1]; local id = last_id(citation.citationId, ctx); local function target() local res = (ctx.citables or {})[id]; if res == nil then io.stderr:write("WARNING! Citation '" .. id .. "' doesn't exist!\n"); res = id; end return res; end return array { "", target, "" }, array { target }, true; end function converters.Header(data, ctx) local level = data[1]; local attribs = read_attributes(data[2], nil, ctx); ctx.headers = ctx.headers or array {}; ctx.header_stack = ctx.header_stack or array {}; local parent = ctx.header_stack[level - 1]; if level > 1 and parent == nil then error "Heading hierarchy inconsistent" end local text, plain = convert_all(data[3], ctx); local header = { id = attribs.id, level = level, children = array {}, ignored = attribs.nonum or false, }; local text_prefix, plain_prefix = "", ""; if level == 1 then ctx.headers:push(header); else parent.children:push(header); end if not header.ignored then local prefix = array { count_headers(ctx.headers) }; local text_format, plain_format; for i = 1, level - 1 do --- @diagnostic disable-next-line: undefined-field prefix:push(count_headers(ctx.header_stack[i].children)); end header.prefix = prefix:join "."; if level == 1 then text_format = ctx.chapter_format or "Chapter %s
"; plain_format = ctx.chapter_format_plain or "Chapter %s: "; else text_format = "%s. "; plain_format = "%s. "; end text_prefix = text_format:format(header.prefix); plain_prefix = plain_format:format(header.prefix); end ctx.header_stack = ctx.header_stack:fill(nil, level + 1, #ctx.header_stack); ctx.header_stack[level] = header; header.text = text_prefix .. text:join ""; header.plain = plain_prefix .. plain:join ""; if attribs.id then ctx.citables = ctx.citables or {}; ctx.citables[attribs.id] = header.plain; end return arrays.concat({ (""):format(level, attribs.id), text_prefix }, text, { (""):format(level) }), array { plain_prefix }:append(plain); end function converters.Link(data, ctx) local content, content_plain = convert_all(data[2], ctx); local url = data[3][1]; local attribs = read_attributes(data[1], nil, ctx); local id = attribs.id or data[3][2]; if id == "" then id = nil end if id then ctx.link_i = (ctx.link_i or 0) + 1; local i = ctx.link_i; ctx.citables = ctx.citables or {}; ctx.citables[id] = "[" .. i .. "]"; end local plain = array {}; plain:push(""); plain:append(content); plain:push(""); return plain, content_plain:push(" (", url, ")"), true; end function converters.Para(data, ctx) indent(ctx); local text, plain = convert_all(data, ctx); undent(ctx); return array {} :push "

" :append(text) :append "

", plain; end function converters.Emph(data, ctx) local text, plain = convert_all(data, ctx); return arrays.concat({ "" }, text, { "" }), plain, true; end function converters.Strong(data, ctx) local text, plain = convert_all(data, ctx); return arrays.concat({ "" }, text, { "" }), plain, true; end function converters.Str(data) return array { sanitize(data) }, array { data }, true; end function converters.Plain(data, ctx) return convert_all(data, ctx); end function converters.RawBlock(data) return array {}, array {}, true; end function converters.MetaInlines(data, ctx) return convert_all(data, ctx); end function converters.LineBreak() return array { "
" }, array { " " }, true; end function converters.SoftBreak() return array { "­" }, array { " " }, true; end function converters.HorizontalRule() return array { "
" }, array { " " }; end function converters.Space() return array { " " }, array { " " }, true; end local function parse_meta(meta) local res = {}; for k, v in next, meta do local text, plain = convert(v, {}); res[k] = text:concat ""; end return res; end local function convert_headers(headers, ctx) ctx = ctx or { indent = 0 }; indent(ctx); local res = arrays.concat( { "" } ); undent(ctx); return res; end local function parse(...) local stream, err = io.popen("pandoc --highlight-style kate --from markdown-raw_html --to json -o - " .. array { ... }:join " "); local str = read_to_str(stream, err); local raw = json.parse(str); return { metadata = parse_meta(raw.meta), content = raw.blocks or array {}, }; end local function convert_page(data, ctx) ctx = ctx or {}; ctx.headers = ctx.headers or array {}; local curr_page = array {}; local pages = array {}; for i = 1, #data.content do local curr = data.content[i]; if curr.t == "Header" then if curr.c[1] == 1 then if #pages > 0 or #curr_page > 0 then pages:append(curr_page); end curr_page = array {}; end end local text, _, inline = convert(curr, ctx); curr_page:append(text); if not inline then curr_page:push("\n"); end end pages:append(curr_page); local function mapper(v) if type(v) == "function" then return v(); else return v; end end return { content = pages:map(mapper, true):join "", toc = convert_headers(ctx.headers):map(mapper, true):join "", }; end local function subst(template, variables) local replaces = array {}; for k, v in next, variables do local i = 1; local search = "{{" .. k .. "}}"; while true do local b, e = string.find(template, search, i, true); if b == nil then break end replaces:push { b = b, e = e, v = v }; i = e + 1; end end table.sort(replaces, function (a, b) return a.b < b.b end); local parts = array {}; local prev = 1; for i = 1, #replaces do local curr = replaces[i]; parts:push(template:sub(prev, curr.b - 1), curr.v); prev = curr.e + 1; end parts:push(template:sub(prev)); return parts:join ""; end function build(options) local toc = options.toc; local content = options.content; local ctx = options.ctx or {}; print(table.concat { "Building ", array(options):join ", ", "..." }); local res = convert_page(parse(unpack(options)), ctx); if toc == nil and content == nil then return res.content; end local obj = {}; if toc ~= nil then obj[toc] = res.toc end if content ~= nil then obj[content] = res.content end return obj; end function combine(objects) local res = {}; for i = 1, #objects do for k, v in next, objects[i] do res[k] = v; end end return res; end function template(options) print("Templating into " .. options.template .. "..."); options = combine(array { options }:append(options)); return subst(read_to_str(io.open(options.template, "r")), options); end function emit(values) local f = io.stdout; if values.target then f = assert(io.open(values.target, "w")); print("Emitting to " .. values.target .. "..."); else print("Emitting to stdout..."); end for i = 1, #values do assert(f:write(values[i])); end if f ~= io.stdout then f:close(); end end -- return main;