diff --git a/tools/2xep.lua b/tools/2xep.lua new file mode 100644 index 00000000..64ced99e --- /dev/null +++ b/tools/2xep.lua @@ -0,0 +1,408 @@ +-- XEP output format for pandoc +-- +-- Based on the pandoc sample.lua HTML writer +-- +-- Invoke with: pandoc -t 2xep.lua +-- +-- Based on `data/sample.lua` from pandoc. +-- +-- Modifications released under the MIT license. +-- Copyright (C) 2021 Kim Alvefur + +-- luacheck: globals Blocksep Doc Space SoftBreak Str LineBreak Emph Strong Subscript Superscript SmallCaps Strikeout +-- luacheck: globals Link Image Code InlineMath DisplayMath SingleQuoted DoubleQuoted Note Span RawInline Cite Plain +-- luacheck: globals Para Header BlockQuote HorizontalRule LineBlock CodeBlock BulletList OrderedList DefinitionList +-- luacheck: globals CaptionedImage Table RawBlock Div + +local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; +local function escape(s, in_attribute) + return (string.gsub(s, in_attribute and "['&<>\"]" or "[&<>]", escape_table)); +end + +local sectionstack = {}; + +-- 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 + +-- 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 = { [[ + + +%ents; +]> + +
]] + } + local function add(s) + table.insert(buffer, s) + end + local header_schema = [[ + (title , abstract , legal , number , status , lastcall* , + interim* , type , sig , approver* , dependencies , supersedes , + supersededby , shortname , schemaloc* , registry? , discuss? , + expires? , author+ , revision+ , councilnote?) + ]]; + for field, r in string.gmatch(header_schema, "(%w+)(%p?)") do + local v = metadata[field] or variables[field]; + if not v then + if field == "legal" then + add("&LEGALNOTICE;"); + goto next; + elseif field == "supersedes" or field == "supersededby" then + add(("<%s/>"):format(field)); + goto next; + elseif r ~= "*" and r ~= "?" then + error(string.format("Missing required metadata field '%s'", field)); + else + io.stderr:write(string.format("Missing optional metadata field '%s'\n", field)); + goto next; + end + end + if field == "number" then + assert(tonumber(v) or v == "xxxx", "Invalid XEP number"); + if v ~= "xxxx" then + v = string.format("%04d", tonumber(v)); + end + end + if type(v) == "table" then + for sk, sv in pairs(v) do + add(string.format("<%s>", field)); + if type(sk) == "string" then + add(("<%s>%s"):format(sk, tostring(sv), sk)); + elseif field == "author" then + local first, last = sv:match("(%S+)%s+(%S+)"); -- Names are hard + add(("%s"):format(first)); + add(("%s"):format(last)); + -- Why is there HTML in the thing? + for typ, addr in sv:gmatch("%shref='(%a+):([^']+)") do + if typ == "mailto" then + add(("%s"):format(addr)); + elseif typ == "xmpp" then + add(("%s"):format(addr)); + end + end + elseif field == "dependencies" then + add(("%s"):format(tostring(sv))); + elseif field == "revision" then + for rev_field in string.gmatch("( version, date, initials )", "%w+") do + add(("<%s>%s"):format(rev_field, tostring(sv[rev_field]), rev_field)); + end + add(""); + for _, remark in ipairs(sv.remark) do + add(("

%s

"):format(remark)); + end + add("
"); + else + add(("<%s>%s"):format(field, tostring(sv), field)); + end + add(string.format("", field)); + end + else + add(("<%s>%s"):format(field, tostring(v), field)); + end + ::next:: + end + add("
"); + add(body) + for i = 1, #sectionstack do + add(""); + end + add("
\n"); + 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(str) + return escape('"' .. str .. '"'); +end + +function Note(s) + return "" .. s .. ""; +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) + local ret = "" + if sectionstack[1] and sectionstack[#sectionstack] >= lev then + repeat + ret = ret .. "" + until sectionstack[1] == nil or sectionstack[#sectionstack] == lev -1; + end + table.insert(sectionstack, lev); + attr.topic = s; + attr.anchor, attr.id = attr.id, nil; + ret = ret .. "" + return ret; +end + +function BlockQuote(s) + return "
\n" .. s .. "\n
" +end + +function HorizontalRule() + return "
" +end + +function LineBlock(ls) + return '
' .. table.concat(ls, '\n') .. + '
' +end + +local function has(haystack, needle) --> boolean + if type(haystack) == "table" then + for _, v in ipairs(haystack) do + if v == needle then return true; end + end + elseif type(haystack) == "string" then + for v in haystack:gmatch("%S+") do + if v == needle then return true; end + end + else + error("unhandled haystack type "..type(haystack)) + end + return false; +end + +function CodeBlock(s, attr) + if attr and attr.class and has(attr.class, "example") then + return "" + else + return "" + end +end + +function BulletList(items) + local buffer = {} + for _, item in pairs(items) do + table.insert(buffer, "
  • " .. item .. "
  • ") + end + return "
      \n" .. table.concat(buffer, "\n") .. "\n
    " +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. +local 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 not empty_header then + add('') + for _,h in pairs(header_row) do + add(h) + end + add('') + else + -- head = "" -- XXX What is this? + 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) + diff --git a/tools/md-diff.sh b/tools/md-diff.sh new file mode 100755 index 00000000..83cc6bcc --- /dev/null +++ b/tools/md-diff.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# md-diff +# arguments: file commit commit [diff tool with args] + +if [ $# -lt 3 ]; then + echo 'arguments: file commit commit [diff tool with args]' >&2 + exit 1; +fi + +${4:-diff} \ + <(git show "$2:$1" | ${0%/*}/xep2md.sh -) \ + <(git show "$3:$1" | ${0%/*}/xep2md.sh -) diff --git a/tools/xep2md.lua b/tools/xep2md.lua new file mode 100644 index 00000000..0ba8cd5e --- /dev/null +++ b/tools/xep2md.lua @@ -0,0 +1,626 @@ +#!/usr/bin/env lua5.3 +-- XEP to Markdown converter +-- +-- Copyright (C) 2021 Kim Alvefur +-- +-- This file is released under the MIT license. +-- +-- Invoke with: +-- xmllint --nonet --noent --loaddtd "$@" | lua5.3 -lluarocks.loader xep2md.lua + +-- Inlined util.events from Prosody, you may wanna skip ahead ~160 lines +-- or so to the main script. +package.preload["util.events"] = (function() +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + + +local pairs = pairs; +local t_insert = table.insert; +local t_remove = table.remove; +local t_sort = table.sort; +local setmetatable = setmetatable; +local next = next; + +local _ENV = nil; + +local function new() + local handlers = {}; + local global_wrappers; + local wrappers = {}; + local event_map = {}; + local function _rebuild_index(handlers, event) + local _handlers = event_map[event]; + if not _handlers or next(_handlers) == nil then return; end + local index = {}; + for handler in pairs(_handlers) do + t_insert(index, handler); + end + t_sort(index, function(a, b) return _handlers[a] > _handlers[b]; end); + handlers[event] = index; + return index; + end; + setmetatable(handlers, { __index = _rebuild_index }); + local function add_handler(event, handler, priority) + local map = event_map[event]; + if map then + map[handler] = priority or 0; + else + map = {[handler] = priority or 0}; + event_map[event] = map; + end + handlers[event] = nil; + end; + local function remove_handler(event, handler) + local map = event_map[event]; + if map then + map[handler] = nil; + handlers[event] = nil; + if next(map) == nil then + event_map[event] = nil; + end + end + end; + local function get_handlers(event) + return handlers[event]; + end; + local function add_handlers(handlers) + for event, handler in pairs(handlers) do + add_handler(event, handler); + end + end; + local function remove_handlers(handlers) + for event, handler in pairs(handlers) do + remove_handler(event, handler); + end + end; + local function _fire_event(event_name, event_data) + local h = handlers[event_name]; + if h then + for i=1,#h do + local ret = h[i](event_data); + if ret ~= nil then return ret; end + end + end + end; + local function fire_event(event_name, event_data) + local w = wrappers[event_name] or global_wrappers; + if w then + local curr_wrapper = #w; + local function c(event_name, event_data) + curr_wrapper = curr_wrapper - 1; + if curr_wrapper == 0 then + if global_wrappers == nil or w == global_wrappers then + return _fire_event(event_name, event_data); + end + w, curr_wrapper = global_wrappers, #global_wrappers; + return w[curr_wrapper](c, event_name, event_data); + else + return w[curr_wrapper](c, event_name, event_data); + end + end + return w[curr_wrapper](c, event_name, event_data); + end + return _fire_event(event_name, event_data); + end + local function add_wrapper(event_name, wrapper) + local w; + if event_name == false then + w = global_wrappers; + if not w then + w = {}; + global_wrappers = w; + end + else + w = wrappers[event_name]; + if not w then + w = {}; + wrappers[event_name] = w; + end + end + w[#w+1] = wrapper; + end + local function remove_wrapper(event_name, wrapper) + local w; + if event_name == false then + w = global_wrappers; + else + w = wrappers[event_name]; + end + if not w then return; end + for i = #w, 1 do + if w[i] == wrapper then + t_remove(w, i); + end + end + if #w == 0 then + if event_name == nil then + global_wrappers = nil; + else + wrappers[event_name] = nil; + end + end + end + return { + add_handler = add_handler; + remove_handler = remove_handler; + add_handlers = add_handlers; + remove_handlers = remove_handlers; + get_handlers = get_handlers; + wrappers = { + add_handler = add_wrapper; + remove_handler = remove_wrapper; + }; + add_wrapper = add_wrapper; + remove_wrapper = remove_wrapper; + fire_event = fire_event; + _handlers = handlers; + _event_map = event_map; + }; +end + +return { + new = new; +}; +end); + +local lxp = require "lxp"; +local lom = require "lxp.lom"; +local events = require"util.events".new(); +local have_yaml, yaml = pcall(require, "lyaml"); + +local handler = {}; +local stack = {}; + +local meta = {}; + +local no_write = true; +local function output(...) + if no_write then return end + io.write(...); +end + +local function print_empty_line() + output("\n\n"); + return true; +end + +local text_buf; + +-- FIXME LuaExpat claims to not require this hack +local function CharacterDataDone() + if text_buf then + local text = table.concat(text_buf); + if text ~= "" then + events.fire_event("#text", { stack = stack, text = text }); + end + text_buf = nil; + end +end + +function handler:StartElement(tagname, attr) + CharacterDataDone(); + tagname = tagname:gsub("^([^\1]+)\1", "{%1}"); + table.insert(stack, tagname) + events.fire_event(tagname, { stack = stack, attr = attr }); +end + +function handler:CharacterData(data) + if text_buf then + table.insert(text_buf, data) + else + text_buf = { data }; + end +end + +function handler:EndElement() + CharacterDataDone(); + events.fire_event(table.remove(stack) .. "/", { stack = stack }); +end + +-- Oh god oh god we're all gonna die! +local function escape_text(event) + event.text = event.text:gsub("['&<>\"]", "\\%1"):gsub("^%s+", ""):gsub("%s+$", ""):gsub("%s+", " "); +end +events.add_handler("#text", escape_text, 1000); + +events.add_handler("#text", function (event) + local stack = event.stack; + return events.fire_event(stack[#stack].."#text", event); +end, 10); + +events.add_handler("#text", function (event) + if event.text:find"%S" then + output(event.text); + end + return true; +end); + +local header_schema = [[ + (title , abstract , legal , number , status , lastcall* , + interim* , type , sig , approver* , dependencies , supersedes , + supersededby , shortname , schemaloc* , registry? , discuss? , + expires? , author+ , revision+ , councilnote?) +]]; +for field in header_schema:gmatch("%w+") do + events.add_handler(field.."#text", function (event) + meta[field] = event.text:match("%S.*%S"); + return true; + end); +end + +do + local author; + events.add_handler("author", function (event) + author = { }; + return true; + end); + + for _, field in pairs{"firstname", "surname", "email", "jid"} do + events.add_handler(field.."#text", function (event) author[field] = event.text; return true; end); + end + + events.add_handler("author/", function (event) + if author.email and author.jid then + author = string.format("%s %s <%s> ", author.firstname, author.surname, author.email, author.jid); + elseif author.email then + author = string.format("%s %s <%s>", author.firstname, author.surname, author.email); + else + author = string.format("%s %s", author.firstname, author.surname); + end + + local authors = meta.author; + if not authors then + meta.author = { author; }; + else + table.insert(authors, author); + end + + author = nil; + return true; + end); +end + +do + local revision; + for _, field in pairs{"version", "date", "initials"} do + events.add_handler(field.."#text", function (event) + if revision then + revision[field] = event.text; + return true; + end + end); + end + + local function handle_remark(event) + if revision and event.text and event.text:match("%S") then + table.insert(revision.remark, event.text); + end + end + + events.add_handler("remark#text", handle_remark, 100); + + events.add_handler("remark", function (event) + events.add_handler("p#text", handle_remark, 100); + end); + + events.add_handler("remark/", function (event) + events.remove_handler("p#text", handle_remark); + end); + + events.add_handler("revision", function (event) + revision = {remark={}}; + return true; + end); + + local revisions = {}; + events.add_handler("revision/", function (event) + table.insert(revisions, revision); + meta.revision = revisions; + revision = nil; + return true; + end); + +end + +events.add_handler("date#text", function (event) + if meta and not meta.date then + meta.date = event.text; + end +end, 1); + +events.add_handler("spec#text", function (event) + if not meta then return end + + local kind = stack[#stack-1]; + if meta[kind] then + table.insert(meta[kind], event.text); + else + meta[kind] = { event.text }; + end +end); + +events.add_handler("header/", function (event) + no_write = false; + if next(meta) ~= nil then + if meta.title and meta.number then + meta.title = "XEP-"..meta.number..": "..meta.title; + end + if have_yaml and yaml.dump then + output(yaml.dump({meta})); + else + print("% "..meta.title); + if type(meta.author) == "table" then + print("% "..table.concat(meta.author, "; ")); + elseif meta.author then + print("% "..meta.author); + else + print("% "); + end + if meta.date then + print("% "..meta.date); + end + end + end + return true; +end); + +for i = 1, 6 do + events.add_handler("section"..i, function () + output("\n"); + end, 10); + events.add_handler("section"..i.."/", function () + output("\n"); + return true; + end, 10); + + events.add_handler("section"..i, function (event) + assert(event.attr.topic, "no @topic"); + output(string.rep("#", i), " ", event.attr.topic); + if event.attr.anchor and event.attr.anchor ~= "" then + output(" {#", event.attr.anchor, "}\n") + else + output("\n"); + end + return true; + end); +end + +events.add_handler("section1", function (event) + output(event.attr.topic); + if event.attr.anchor and event.attr.anchor ~= "" then + output(" {#", event.attr.anchor, "}"); + end + output("\n", string.rep("=", #event.attr.topic), "\n\n"); + return true; +end, 1); + +events.add_handler("section2", function (event) + output(event.attr.topic); + if event.attr.anchor and event.attr.anchor ~= "" then + output(" {#", event.attr.anchor, "}"); + end + output("\n", string.rep("-", #event.attr.topic), "\n\n"); + return true; +end, 1); + +local function normalize_whitespace(event) + event.text = event.text:gsub("%s+", " ") + -- event.text = event.text:match("^%s*(.-)%s*$") +end +events.add_handler("p#text", normalize_whitespace, 10); +events.add_handler("li#text", normalize_whitespace, 10); +events.add_handler("dt#text", normalize_whitespace, 10); +events.add_handler("dd#text", normalize_whitespace, 10); + +local example_count = 1; + +events.add_handler("example", function (event) + output("\n#### Example ", example_count, ". "); + if event.attr.caption then + output(event.attr.caption, " ") + end + output("{#example-", example_count, " .unnumbered}\n\n") + example_count = example_count + 1; + output("``` {.xml .example}\n"); + events.remove_handler("#text", escape_text); +end); + +events.add_handler("example#text", function (event) + local example_text = event.text:match("^%s*(.-)%s*$"); + output(example_text, "\n"); + return true; +end); + +events.add_handler("example/", function () + events.add_handler("#text", escape_text, 1000); + output("```\n\n"); + return true; +end); + +events.add_handler("note", function (event) + output(" ^["); + return true; +end); + +events.add_handler("note/", function (event) + output("]"); + return true; +end); + +-- TODO magically import citation data +events.add_handler("cite#text", function (event) + output("**", event.text, "**"); + if meta.references then + local refid = event.text:gsub("%W", ""):lower(); + if meta.references[refid] then + output("[@", refid, "]"); + end + end + return true; +end); + +local url; +events.add_handler("link", function (event) + url = event.attr.url; + if url then + output("["); + end + return true; +end); + +events.add_handler("link/", function (event) + if url then + output("](", url, ")"); + url = nil; + end + return true; +end); + + +local list_depth, list_type = 0, "ul"; +events.add_handler("ul", function () + list_depth = list_depth + 1; + list_type = "ul"; +end); + +events.add_handler("ul/", function (event) + local stack = event.stack; + list_depth = list_depth - 1; + for i = #stack, 1, -1 do + local element = stack[i] + if element == "ul" or element == "ol" then + list_type = element; + break; + end + end + return true; +end); + +events.add_handler("li", function (event) + for i = 2, list_depth do + output(" "); + end + if list_type == "ul" then + output("- "); + elseif list_type == "ul" then + output("#. "); + end + return true; +end); + +events.add_handler("li#text", function (event) + local text = event.text:gsub("%s+", " "); + output(text); + return true; +end); + +events.add_handler("dd#text", function (event) + output("\n: "); +end); + +events.add_handler("li/", print_empty_line, 1); +events.add_handler("ul", print_empty_line, 1); +events.add_handler("ul/", print_empty_line, 1); +events.add_handler("ol", print_empty_line, 1); +events.add_handler("ol/", print_empty_line, 1); +events.add_handler("p/", print_empty_line, 1); +events.add_handler("dd/", print_empty_line, 1); + +local function printcell(event) + output("|"); +end +events.add_handler("th", printcell, 1); +events.add_handler("td", printcell, 1); +events.add_handler("tr/", printcell, 3); +events.add_handler("tr", function () output(" ") end, 1); +events.add_handler("tr/", function () output("\n") end, 1); + +local th; +events.add_handler("table", function () th = 0; end); +events.add_handler("table/", function () th = 0; end); +events.add_handler("th", function () if th then th = th + 1; end end); +events.add_handler("tr/", function () if th then output("\n");output(" |"); output(string.rep("---|", th)); th = nil end end, 2); + +-- Non-example code blocks, like schemas +events.add_handler("code", function (event) + output("```xml\n"); + events.remove_handler("#text", escape_text); + return true; +end); + +events.add_handler("code#text", function (event) + local example_text = event.text:match("^%s*(.-)%s*$"); + output(example_text, "\n"); + return true; +end); + +events.add_handler("code/", function () + events.add_handler("#text", escape_text, 1000); + output("```\n\n"); + return true; +end); + +if meta.references then + events.add_handler("xep/", function () + output("\n\n# References {#references}\n\n"); + end); +end + +if arg[1] == "--debug" then + events.add_wrapper(false, function (fire_event, event_name, event_data) + io.stderr:write("D: "..event_name.."\n"); + io.stderr:write("D: /"..table.concat(event_data.stack, "/").."\n"); + return fire_event(event_name, event_data); + end); + setmetatable(handler, { + __index = function (_, missinghandler) + io.stderr:write("D: Missing handler: "..missinghandler.."\n"); + return function (parser, ...) + io.stderr:write("D: ", missinghandler, "("); + local count = select('#', ...); + for i = 1, count do + local arg = select(i, ...); + local arg_t = type(arg); + io.stderr:write(arg_t, ":"); + if arg_t == "string" then + io.stderr:write(string.format("%q", arg)); + else + io.stderr:write(tostring(arg)); + end + if i ~= count then + io.stderr:write(", "); + end + end + io.stderr:write(")\n"); + return ""; + end; + end; + }); +end + +local parser = lxp.new(handler, "\1"); +parser:setbase("."); +local function chunks(file, size) + return function () + return file:read(size); + end +end + +if not have_yaml then + io.stderr:write("lua-yaml missing, header metadata will be incomplete\n"); +end +for chunk in chunks(io.stdin, 4096) do + local ok, err, line, col = parser:parse(chunk); + if not ok then + io.stderr:write("E: "..err.." on line "..line..", col "..col.."\n"); + os.exit(1); + end +end + +parser:close(); diff --git a/tools/xep2md.sh b/tools/xep2md.sh new file mode 100755 index 00000000..4ea24071 --- /dev/null +++ b/tools/xep2md.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +xmllint --nonet --noent --loaddtd "$@" | lua5.3 -lluarocks.loader ${0%/*}/xep2md.lua