--[=[
	This module contains functions to implement {{form of/*doc}} templates.
	The module contains the actual implementation, meant to be called from other
	Lua code. See [[Module:form of doc/templates]] for the function meant to be
	called directly from templates.

	Author: Benwing2
]=]

local export = {}

local m_languages = require("Module:languages")
local m_table = require("Module:table")
local form_of_module = "Module:form of"
local m_form_of = require(form_of_module)
local labels_module = "Module:labels"
local strutils = require("Module:string utilities")

local usub = mw.ustring.sub
local uupper = mw.ustring.upper
local rfind = mw.ustring.find
local rsubn = mw.ustring.gsub
local template_link = require("Module:template parser").templateLink


-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
	local retval = rsubn(term, foo, bar)
	return retval
end


local function lang_name(langcode, param)
	local lang = m_languages.getByCode(langcode, param)
	return lang:getCanonicalName()
end


local function ucfirst(text)
	return uupper(usub(text, 1, 1)) .. usub(text, 2)
end


function link_box(content)
	return "<div class=\"noprint plainlinks\" style=\"float: right; clear: both; margin: 0 0 .5em 1em; background: #f9f9f9; border: 1px #aaaaaa solid; margin-top: -1px; padding: 5px; font-weight: bold; font-size: small;\">"
		.. content .. "</div>"
end


function show_editlink(page)
	return link_box("[" .. tostring(mw.uri.fullUrl(page, "action=edit")) .. " Edit]")
end


local function template_name(preserve_lang_code)
	-- Fetch the template name, minus the '/documentation' suffix that may follow
	-- and without any language-specific prefixes (e.g. 'el-' or 'ine-bsl-pro-')
	-- (unless `preserve_lang_code` is given).
	local PAGENAME =  mw.title.getCurrentTitle().text
	local tempname = rsub(PAGENAME, "/documentation$", "")
	if not preserve_lang_code then
		while true do
			-- Repeatedly strip off language code prefixes, in case there are multiple.
			local newname = rsub(tempname, "^[a-z][a-z][a-z]?%-", "")
			if newname == tempname then
				break
			end
			tempname = newname
		end
	end
	return tempname
end


function export.introdoc(args)
	local langname = args.lang and lang_name(args.lang, "lang")
	local exlangnames = {}
	for _, exlang in ipairs(args.exlang) do
		table.insert(exlangnames, lang_name(exlang, "exlang"))
	end
	parts = {}
	table.insert(parts, mw.getCurrentFrame():expandTemplate {title="uses lua", args={form_of_module .. "/templates"}})
	table.insert(parts, "This template creates a definition line for ")
	table.insert(parts, args.pldesc or rsub(template_name(), " of$", "") .. "s")
	table.insert(parts, " ")
	table.insert(parts, args.primaryentrytext or "of primary entries")
	if args.lang then
		table.insert(parts, " in " .. langname)
	elseif #args.exlang > 0 then
		table.insert(parts, ", e.g. in " .. m_table.serialCommaJoin(exlangnames, {conj = "or"}))
	end
	table.insert(parts, ".")
	if #args.cat > 0 then
		table.insert(parts, " It also categorizes the page into ")
		local catparts = {}
		for _, cat in ipairs(args.cat) do
			if args.lang then
				table.insert(catparts, "[[:Category:" .. langname .. " " .. cat .. "]]")
			else
				table.insert(catparts, "the proper language-specific subcategory of [[:Category:" .. ucfirst(cat) .. " by language]] (e.g. [[:Category:" .. (exlangnames[1] or "English") .. " " .. cat .. "]])")
			end
		end
		table.insert(parts, m_table.serialCommaJoin(catparts))
		table.insert(parts, ".")
	end
	if args.addlintrotext then
		table.insert(parts, " ")
		table.insert(parts, args.addlintrotext)
	end
	table.insert(parts, "\n")
	if args.withcap and args.withdot then
		table.insert(parts, [===[

By default, this template displays its output as a full sentence, with an initial capital letter and a trailing period (full stop). This can be overridden using <code>|nocap=1</code> and/or <code>|nodot=1</code> (see below).
]===])
	elseif args.withcap then
		table.insert(parts, [===[

By default, this template displays its output with an initial capital letter. This can be overridden using <code>|nocap=1</code> (see below).
]===])
	end
	table.insert(parts, [===[

This template is '''not''' meant to be used in etymology sections.]===])
	if args.etymtemp then
		table.insert(parts, " For those sections, use <code>{{[[Template:" .. args.etymtemp .. "|" .. args.etymtemp .. "]]}}</code> instead.")
	end
	table.insert(parts, [===[


Note that users can customize how the output of this template displays by modifying their Custom CSS files. See [[:Category:Form-of templates|“Form of” templates]] for details.
]===])
	return table.concat(parts)
end


local function param(params, list, required)
	local paramparts = {}
	if type(params) ~= "table" then
		params = {params}
	end
	for _, p in ipairs(params) do
		local listparts = {}
		table.insert(listparts, "<code>|" .. p .. "=</code>")
		if list then
			table.insert(listparts, ", <code>|" .. p .. "2=</code>")
			table.insert(listparts, ", <code>|" .. p .. "3=</code>")
			table.insert(listparts, ", etc.")
		end
		table.insert(paramparts, table.concat(listparts))
	end
	local reqtext = required and "'''(required)'''" or "''(optional)''"
	return table.concat(paramparts, " or ") .. " " .. reqtext
end


function export.paramdoc(args)
	local parts = {}

	local function param_and_doc(params, list, required, doc)
		table.insert(parts, "; ")
		table.insert(parts, param(params, list, required))
		table.insert(parts, "\n")
		table.insert(parts, ": ")
		table.insert(parts, doc)
		table.insert(parts, "\n")
	end

	local tempname = template_name()
	local art = args.art or require("Module:string utilities").get_indefinite_article(tempname)
	local sgdescof = args.sgdescof or art .. " " .. tempname
	table.insert(parts, "''Positional (unnamed) parameters:''\n")
	if args.lang then
		param_and_doc("1", false, true, "The term to link to (which this page is " .. sgdescof .. "). This should include any needed diacritics as appropriate to " .. lang_name(args.lang, "lang") .. ". These diacritics will automatically be stripped out in the appropriate fashion in order to create the link to the page.")
		param_and_doc("2", false, false, "The text to be shown in the link to the term. If empty or omitted, the term specified by the first parameter will be used. This parameter is normally not necessary, and should not be used solely to indicate diacritics; instead, put the diacritics in the first parameter.")
	else
		param_and_doc("1", false, true, "The [[WT:LANGCODE|language code]] of the term linked to (which this page is " .. sgdescof .. "). See [[Wiktionary:List of languages]]. <small>The parameter <code>|lang=</code> is a deprecated synonym; please do not use. If this is used, all numbered parameters move down by one.</small>")
		param_and_doc("2", false, true, "The term to link to (which this page is " .. sgdescof .. "). This should include diacritics as appropriate to the language (e.g. accents in Russian to mark the stress, vowel diacritics in Arabic, macrons in Latin to indicate vowel length, etc.). These diacritics will automatically be stripped out in a language-specific fashion in order to create the link to the page.")
		param_and_doc("3", false, false, "The text to be shown in the link to the term. If empty or omitted, the term specified by the second parameter will be used. This parameter is normally not necessary, and should not be used solely to indicate diacritics; instead, put the diacritics in the second parameter.")
	end
	table.insert(parts, "''Named parameters:''\n")
	if args.etymtemp == 'contraction' then
		param_and_doc("mandatory", false, false, "If <code>|mandatory=1</code>, indicates that the contraction is mandatory.")
		param_and_doc("optional", false, false, "If <code>|optional=1</code>, indicates that the contraction is optional.")
	end
	param_and_doc({"t", args.lang and "3" or "4"}, false, false, "A gloss or short translation of the term linked to. <small>The parameter <code>|gloss=</code> is a deprecated synonym; please do not use.</small>")
	param_and_doc("tr", false, false, "Transliteration for non-Latin-script terms, if different from the automatically-generated one.")
	param_and_doc("ts", false, false, "Transcription for non-Latin-script terms whose transliteration is markedly different from the actual pronunciation. Should not be used for IPA pronunciations.")
	param_and_doc("sc", false, false, "Script code to use, if script detection does not work. See [[Wiktionary:Scripts]].")
	if args.withfrom then
		param_and_doc("from", true, false, "A label (see " .. template_link("label") .. ") that gives additional information on the dialect that the term belongs to, the place that it originates from, or something similar.")
	end
	if args.withdot then
		param_and_doc("dot", false, false, "A character to replace the final dot that is normally shown automatically.")
		param_and_doc("nodot", false, false, "If <code>|nodot=1</code>, then no automatic dot will be shown.")
	end
	if args.withcap then
		param_and_doc("nocap", false, false, "If <code>|nocap=1</code>, then the first letter will be in lowercase.")
	end
	param_and_doc("id", false, false, "A sense id for the term, which links to anchors on the page set by the " .. template_link("senseid") .. " template.")
	return table.concat(parts)
end


function export.usagedoc(args)
	local exlangs = {}
	for _, exlang in ipairs(args.exlang) do
		table.insert(exlangs, exlang)
	end
	table.insert(exlangs, 'en')
	table.insert(exlangs, 'de')
	table.insert(exlangs, 'ja')
	exlangs = m_table.removeDuplicates(exlangs)
	local sub = {}
	local langparts = {}
	for i, langcode in ipairs(exlangs) do
		table.insert(langparts, '<code>' .. langcode .. '</code> for ' .. lang_name(langcode, "exlang"))
	end
	sub.exlangs = m_table.serialCommaJoin(langparts, {conj = "or"})
	sub.tempname = template_name("preserve lang code")

	if args.lang then
		return strutils.format([===[
==Usage==
Use in the definition line, most commonly as follows:
 # {\op}{\op}{tempname}|<var><primary entry goes here></var>{\cl}{\cl}

===Parameters===
]===], sub) .. export.paramdoc(args)
	else
		return strutils.format([===[
==Usage==
Use in the definition line, most commonly as follows:
 # {\op}{\op}{tempname}|<var><langcode></var>|<var><primary entry goes here></var>{\cl}{\cl}
where <code><var><langcode></var></code> is the [[Wiktionary:Languages|language code]], e.g. {exlangs}.

===Parameters===
]===], sub) .. export.paramdoc(args)
	end
end


function export.fulldoc(args)
	local docsubpage = mw.getCurrentFrame():expandTemplate{title="documentation subpage", args={}}
	local shortcuts = #args.shortcut > 0 and require("Module:shortcut box").format_shortcuts(args.shortcut) or ""
	local introdoc = export.introdoc(args)
	local usagedoc = export.usagedoc(args)
	return docsubpage .. "\n" .. shortcuts .. introdoc .. "\n" .. usagedoc
end


function export.infldoc(args)
	args = m_table.shallowcopy(args)
	args.sgdesc = args.sgdesc or (args.art or "the") .. " " ..
		rsub(template_name(), " of$", "") .. (args.form and " " .. args.form or "")
	args.pldesc = args.sgdesc
	args.sgdescof = args.sgdescof or args.sgdesc .. " of"
	args.primaryentrytext = args.primaryentrytext or "of a primary entry"
	return export.fulldoc(args)
end


local tag_type_to_description = {
	-- If not listed, we just capitalize the first letter
	["tense-aspect"] = "Tense/aspect",
	["voice-valence"] = "Voice/valence",
	["comparison"] = "Degrees of comparison",
	["class"] = "Inflectional class",
	["sound change"] = "Sound changes",
	["grammar"] = "Misc grammar",
	["other"] = "Other tags",
}


local tag_type_order = {
	"person",
	"number",
	"gender",
	"animacy",
	"tense-aspect",
	"mood",
	"voice-valence",
	"non-finite",
	"case",
	"state",
	"comparison",
	"register",
	"deixis",
	"clusivity",
	"class",
	"attitude",
	"sound change",
	"grammar",
	"other",
}


local function tag_type_desc(tag_type)
	return tag_type_to_description[tag_type] or strutils.ucfirst(tag_type)
end


local function sort_by_first(namedata1, namedata2)
	return namedata1[1] < namedata2[1]
end


local function get_display_form(tag_set, lang)
	local norm_tag_sets = m_form_of.normalize_tag_set(tag_set, lang)
	if #norm_tag_sets == 1 then
		return m_form_of.get_tag_set_display_form(norm_tag_sets[1], lang)
	end
	-- If we have a conjoined shortcut that expands to multiple tag sets, display them using a numbered list.
	-- In order to do that inside a table we need a newline before the list.
	local display_forms = {}
	for _, norm_tag_set in ipairs(norm_tag_sets) do
		table.insert(display_forms, "\n# " .. m_form_of.get_tag_set_display_form(norm_tag_set, lang))
	end
	return table.concat(display_forms)
end

local function organize_tag_data(data_module)
	local tab = {}
	for name, data in pairs(data_module.tags) do
		local tag_type = data[m_form_of.TAG_TYPE]
		if not tag_type then
			-- Throw an error because hopefully it will get noticed and fixed. If we just skip it, it may never get
			-- fixed.
			error("Tag '" .. name .. "' has no tag_type")
		end
		if not tab[tag_type] then
			tab[tag_type] = {}
		end
		table.insert(tab[tag_type], {name, data})
	end
	local tag_type_order_set = m_table.listToSet(tag_type_order)
	for tag_type, tags_of_type in pairs(tab) do
		if not tag_type_order_set[tag_type] then
			-- See justification above for throwing an error.
			error("Tag type '" .. tag_type .. "' not listed in tag_type_order")
		end
		table.sort(tags_of_type, sort_by_first)
	end

	return tab
end

function export.tagtable()
	local data_tab = organize_tag_data(mw.loadData(m_form_of.form_of_data_module))
	local data2_tab = organize_tag_data(mw.loadData(m_form_of.form_of_data2_module))

	local parts = {}

	local function ins(text)
		table.insert(parts, text)
	end

	local function insert_group(group)
		for _, namedata in ipairs(group) do
			local sparts = {}
			local name, data = unpack(namedata)
			table.insert(sparts, "| <code>" .. name .. "</code> || ")
			local shortcuts = data[m_form_of.SHORTCUTS]
			if shortcuts then
				local ssparts = {}
				if type(shortcuts) == "string" then
					shortcuts = {shortcuts}
				end
				for _, shortcut in ipairs(shortcuts) do
					table.insert(ssparts, "<code>" .. shortcut .. "</code>")
				end
				table.insert(sparts, table.concat(ssparts, ", ") .. " ")
			end
			table.insert(sparts, "|| " .. m_form_of.get_tag_display_form(name))
			ins("|-")
			ins(table.concat(sparts))
		end
	end

	ins('{|class="wikitable"')
	ins("! Canonical tag !! Shortcut(s) !! Display form")
	for _, tag_type in ipairs(tag_type_order) do
		local group_tab = data_tab[tag_type]
		if group_tab then
			ins("|-")
			ins('! colspan="3" style="text-align: center; background: #dddddd;" | ' ..  tag_type_desc(tag_type) ..
				" (more common)")
			insert_group(group_tab)
		end
		group_tab = data2_tab[tag_type]
		if group_tab then
			ins("|-")
			ins('! colspan="3" style="text-align: center; background: #dddddd;" | ' ..  tag_type_desc(tag_type) ..
				" (less common)")
			insert_group(group_tab)
		end
	end
	ins("|}")

	return table.concat(parts, "\n")
end

local function organize_non_alias_shortcut_data(data_module, lang)
	local non_alias_shortcuts = {}
	for shortcut, full in pairs(data_module.shortcuts) do
		if type(full) == "table" or m_form_of.is_link_or_html(full) or full:find("//") or full:find(":") then
			table.insert(non_alias_shortcuts, {shortcut, full, get_display_form({shortcut}, lang)})
		end
	end

	table.sort(non_alias_shortcuts, sort_by_first)

	return non_alias_shortcuts
end

function export.non_alias_shortcut_table()
	local non_alias_shortcuts = organize_non_alias_shortcut_data(mw.loadData(m_form_of.form_of_data_module))
	local non_alias_shortcuts2 = organize_non_alias_shortcut_data(mw.loadData(m_form_of.form_of_data2_module))

	local parts = {}

	local function ins(text)
		table.insert(parts, text)
	end

	local function insert_shortcut_group(shortcuts)
		for _, spec in ipairs(shortcuts) do
			local shortcut, full, display = unpack(spec)
			ins("|-")
			if type(full) == "table" then
				-- Grrr. table.concat() doesn't work on a table that comes from loadData(); use shallowcopy() to convert to
				-- regular table.
				full = "{" .. table.concat(m_table.shallowcopy(full), " ") .. "}"
			end
			ins(("| <code>%s</code> || <code>%s</code> || %s"):format(shortcut, full, display))
		end
	end

	ins('{|class="wikitable"')
	ins("! Shortcut !! Expansion !! Display form")
	if #non_alias_shortcuts > 0 then
		ins("|-")
		ins('! colspan="3" style="text-align: center; background: #dddddd;" | More common:')
		insert_shortcut_group(non_alias_shortcuts)
	end
	if #non_alias_shortcuts2 > 0 then
		ins("|-")
		ins('! colspan="3" style="text-align: center; background: #dddddd;" | Less common:')
		insert_shortcut_group(non_alias_shortcuts2)
	end
	ins("|}")

	return table.concat(parts, "\n")
end


local function find_categories_and_labels(catstruct)
	local cats = {}
	local labels = {}

	local function process_spec(spec)
		if type(spec) == "string" then
			table.insert(cats, spec)
			return
		elseif not spec or spec == true then
			-- Ignore labels, etc.
			return
		elseif type(spec) ~= "table" then
			error("Wrong type of condition " .. spec .. ": " .. type(spec))
		elseif spec.labels then
			m_table.extendList(labels, spec.labels)
			return
		end
		local predicate = spec[1]
		if predicate == "multi" or predicate == "cond" then
			-- WARNING! #spec doesn't work for objects loaded from loadData()
			for i, sp in ipairs(spec) do
				if i > 1 then
					process_spec(sp)
				end
			end
		elseif predicate == "pexists" then
			process_spec(spec[2])
			process_spec(spec[3])
		elseif predicate == "has" or predicate == "hasall" or predicate == "hasany" or
			predicate == "tags=" or predicate == "p=" or predicate == "pany" or
			predicate == "not" then
			process_spec(spec[3])
			process_spec(spec[4])
		elseif predicate == "and" or predicate == "or" then
			process_spec(spec[3])
			process_spec(spec[4])
		elseif predicate == "call" then
			return
		else
			error("Unrecognized predicate: " .. predicate)
		end
	end

	for _, spec in ipairs(catstruct) do
		process_spec(spec)
	end
	return cats, labels
end


local function construct_category_table(cats)
	local category_parts = {}
	local function ins(text)
		table.insert(category_parts, text)
	end
	ins('{|class="wikitable"')
	ins("! Category")
	for _, cat in ipairs(cats) do
		ins("|-")
		ins("| <code>" .. cat .. "</code>")
	end
	ins("|}")
	return table.concat(category_parts, "\n")
end


local function construct_label_table(labels, lang, replace_und)
	local label_parts = {}
	local function ins(text)
		table.insert(label_parts, text)
	end
	ins('{|class="wikitable"')
	ins("! Label !! Display form !! Associated categories")
	for _, label in ipairs(labels) do
		ins("|-")
		local label_data = require(labels_module).get_label_info {
			label = label,
			lang = lang,
		}
		local coded_categories = {}
		for _, cat in ipairs(label_data.categories) do
			if replace_und then
				cat = cat:gsub("^und:", "LANGCODE:")
				cat = cat:gsub("^Undetermined ", "LANG ")
			end
			table.insert(coded_categories, "<code>" .. cat .. "</code>")
		end
		ins(("| <code>%s</code> || %s || %s"):format(label, label_data.label,
			table.concat(coded_categories, ",")))
	end
	ins("|}")
	return table.concat(label_parts, "\n")
end


function export.lang_specific_tables()
	local m_cats = mw.loadData(m_form_of.form_of_cats_module)
	local parts = {}

	local function ins(text)
		table.insert(parts, text)
	end

	local data_by_lang = {}

	local langs_with_lang_specific_data = mw.clone(m_form_of.langs_with_lang_specific_tags)
	for langcode, _ in pairs(m_cats) do
		if langcode ~= "und" then
			langs_with_lang_specific_data[langcode] = true
		end
	end

	for langcode, _ in pairs(langs_with_lang_specific_data) do
		local lang = m_languages.getByCode(langcode, true)
		local data_module_name = m_form_of.form_of_lang_data_module_prefix .. langcode
		local data_module = m_form_of.langs_with_lang_specific_tags[langcode] and mw.loadData(data_module_name) or nil

		-- First do base-lemma params.
		local base_lemma_param_table
		-- Can't just call #data_module.base_lemma_params as this comes from loadData().
		if data_module and data_module.base_lemma_params and data_module.base_lemma_params[1] then
			local base_lemma_param_parts = {}
			local function ins(text)
				table.insert(base_lemma_param_parts, text)
			end
			ins('{|class="wikitable"')
			ins("! Parameter !! Display form")
			for _, base_lemma_param in ipairs(data_module.base_lemma_params) do
				ins("|-")
				ins(("| <code>%s</code> || %s"):format(base_lemma_param.param,
					get_display_form(base_lemma_param.tags, lang)))
			end
			ins("|}")
			base_lemma_param_table = table.concat(base_lemma_param_parts, "\n")
		end


		-- Then do inflection tags.
		local data_tab = data_module and organize_tag_data(data_module) or {}
		local tag_parts = {}
		local function ins(text)
			table.insert(tag_parts, text)
		end
		ins('{|class="wikitable"')
		ins("! Canonical tag !! Shortcut(s) !! Tag type !! Display form")
		local saw_any_tag = false
		for _, tag_type in ipairs(tag_type_order) do
			local group_tab = data_tab[tag_type]
			if group_tab then
				for _, namedata in ipairs(group_tab) do
					local sparts = {}
					local name, data = unpack(namedata)
					table.insert(sparts, "| <code>" .. name .. "</code> || ")
					if data.shortcuts then
						local ssparts = {}
						for _, shortcut in ipairs(data.shortcuts) do
							table.insert(ssparts, "<code>" .. shortcut .. "</code>")
						end
						table.insert(sparts, table.concat(ssparts, ", ") .. " ")
					end
					table.insert(sparts, "|| " .. tag_type_desc(tag_type) .. " || " ..
						m_form_of.get_tag_display_form(name, lang))
					ins("|-")
					ins(table.concat(sparts))
					saw_any_tag = true
				end
			end
		end
		ins("|}")

		local tag_table = saw_any_tag and table.concat(tag_parts, "\n") or nil

		-- Then do non-alias shortcuts.
		local non_alias_shortcut_table
		local non_alias_shortcuts = data_module and organize_non_alias_shortcut_data(data_module, lang) or {}
		if #non_alias_shortcuts > 0 then
			local non_alias_shortcut_parts = {}
			local function ins(text)
				table.insert(non_alias_shortcut_parts, text)
			end
			ins('{|class="wikitable"')
			ins("! Shortcut !! Expansion !! Display form")
			for _, spec in ipairs(non_alias_shortcuts) do
				local shortcut, full, display = unpack(spec)
				ins("|-")
				if type(full) == "table" then
					-- Grrr. table.concat() doesn't work on a table that comes from loadData(); use shallowcopy() to
					-- convert to regular table.
					full = "{" .. table.concat(m_table.shallowcopy(full), " ") .. "}"
				end
				ins(("| <code>%s</code> || <code>%s</code> || %s"):format(shortcut, full, display))
			end
			ins("|}")
			non_alias_shortcut_table = table.concat(non_alias_shortcut_parts, "\n")
		end

		-- Then do categories and labels.
		local category_table, label_table
		if m_cats[langcode] then
			local cats, labels = find_categories_and_labels(m_cats[langcode])
			if #cats > 0 then
				category_table = construct_category_table(cats)
			end
			if #labels > 0 then
				label_table = construct_label_table(labels, lang)
			end
		end

		-- Concatenate all the tables together, with appropriate explanatory text.
		if base_lemma_param_table or tag_table or non_alias_shortcut_table or category_table or label_table then
			local langname = lang:getCanonicalName()
			local lang_parts = {}
			local function ins(text)
				table.insert(lang_parts, text)
			end
			ins("===" .. langname .. "===")
			ins(show_editlink(data_module_name))
			if base_lemma_param_table then
				ins(("%s-specific base lemma parameters:"):format(langname))
				ins(base_lemma_param_table)
			end
			if tag_table then
				ins(("%s-specific inflection tags:"):format(langname))
				ins(tag_table)
			end
			if non_alias_shortcut_table then
				ins(("%s-specific non-alias shortcuts:"):format(langname))
				ins(non_alias_shortcut_table)
			end
			if category_table then
				ins(("%s-specific categories (the exact conditions under which these are added are described in [[Module:form of/cats]]):"):
					format(langname))
				ins(category_table)
			end
			if label_table then
				ins(("%s-specific labels (the exact conditions under which these are added are described in [[Module:form of/cats]]):"):
					format(langname))
				ins(label_table)
			end
			table.insert(data_by_lang, {langname, table.concat(lang_parts, "\n")})
		end
	end

	local function sort_by_first_english_first(langdata1, langdata2)
		if langdata1[1] == "English" then
			-- English is "less than" (goes before) all other languages
			return true
		elseif langdata2[1] == "English" then
			-- All other languages are not "less than" (do not go before) English
			return false
		else
			return langdata1[1] < langdata2[1]
		end
	end

	table.sort(data_by_lang, sort_by_first_english_first)

	local parts = {}
	for _, lang_and_data in ipairs(data_by_lang) do
		table.insert(parts, lang_and_data[2])
	end
	return table.concat(parts, "\n")
end


function export.postable()
	m_pos = mw.loadData(m_form_of.form_of_pos_module)
	local shortcut_tab = {}
	for shortcut, full in pairs(m_pos) do
		if not shortcut_tab[full] then
			shortcut_tab[full] = {}
		end
		table.insert(shortcut_tab[full], shortcut)
	end
	local shorcut_list = {}
	for full, shortcuts in pairs(shortcut_tab) do
		table.sort(shortcuts)
		table.insert(shorcut_list, {full, shortcuts})
	end
	table.sort(shorcut_list, function(fs1, fs2) return fs1[1] < fs2[1] end)

	local parts = {}
	table.insert(parts, '{|class="wikitable"')
	table.insert(parts, "! Canonical part of speech !! Shortcut(s)")
	for _, full_shortcuts in ipairs(shorcut_list) do
		local full = full_shortcuts[1]
		local shortcuts = full_shortcuts[2]
		table.insert(parts, "|-")
		local sparts = {}
		for _, shortcut in ipairs(shortcuts) do
			table.insert(sparts, "<code>" .. shortcut .. "</code>")
		end
		table.insert(parts, "| <code>" .. full .. "</code> || " .. table.concat(sparts, ", "))
	end
	table.insert(parts, "|}")
	return table.concat(parts, "\n")
end


function export.lang_independent_category_table()
	local m_cats = mw.loadData(m_form_of.form_of_cats_module)
	if m_cats["und"] then
		local cats, labels = find_categories_and_labels(m_cats["und"])
		if #cats > 0 then
			return construct_category_table(cats)
		end
	end
	return "(no language-independent categories currently)"
end


function export.lang_independent_label_table()
	local m_cats = mw.loadData(m_form_of.form_of_cats_module)
	if m_cats["und"] then
		local cats, labels = find_categories_and_labels(m_cats["und"])
		if #labels > 0 then
			return construct_label_table(labels, m_languages.getByCode("und", true), "replace und")
		end
	end
	return "(no language-independent labels currently)"
end


return export