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%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%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%s>"):format(field, tostring(sv), field));
+ end
+ add(string.format("%s>", field));
+ end
+ else
+ add(("<%s>%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 ''
+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("" .. caption .. "")
+ 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,'' .. h .. ' | ')
+ empty_header = empty_header and h == ""
+ end
+ if not empty_header then
+ 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('' .. c .. ' | ')
+ end
+ add('
')
+ end
+ add('
')
+ 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