Terraria Mods Wiki
Register
Advertisement

Documentation for this module may be created at Module:Format TemplateData/doc

-- Module for displaying a table with TemplateData information of
-- a template that is much richer than the table that is displayed
-- by the <templatedata> tags by default.

-- Originally based on https://en.wikipedia.org/wiki/Module:Format_TemplateData,
-- heavily refactored for readability and maintainability,
-- and adjusted to the needs of the Terraria Wiki.

local TemplateData = {}
local plaintext = require("Module:Plain text")
local trim = mw.text.trim



-----------------------------------------------------------------
-- Global tables


---The keys in this table are the arguments from the module invocation that
---are recognized for modifying the internal ``Config`` table, the values
---are the respective keys of the ``Config`` table
local ConfigAliases = {
	-- e.g. using "|cat=foo" in the #invoke will set Config.maintenanceCateName to "foo"
	cat = "maintenanceCateName",
	cate = "maintenanceCateName",

	tocclass = "classForToc",
	classNoNumTOC = "classForToc",

	tablecss = "stylesForParamstable",
	tablestyle = "stylesForParamstable",
	cssParams = "stylesForParamstable",

	wrappercss = "stylesForParamstableWrapper",
	wrapperstyle = "stylesForParamstableWrapper",
	cssParWrap = "stylesForParamstableWrapper",

	docpageCreate = "patternForCreatingSubpage",

	docpageDetect = "patternForMatchingSubpage",

	missingDescText = "textIfDescriptionIsMissing",
	msgDescMiss = "textIfDescriptionIsMissing",

	detailtrue = "paramDetailValueTrue",

	detailfalse = "paramDetailValueFalse"
}

---Global table with internal configuration options,
---filled from module and template arguments
local Config = {
	---Boolean, whether to display the raw ``<templatedata>`` table even if
	---``Data.showRawTempldataTable`` is true
	alwaysDisplayRawTempldataTable = false,

	---Boolean, whether not to display the template description above the parameter table
	hideDescription = true,

	---String, error message to display when the template description is not set
	textIfDescriptionIsMissing = false,

	---String, name of the maintenance category for erroneous module calls
	maintenanceCateName = false,

	---String, CSS styling for the parameter table
	stylesForParamstable = false,

	---String, CSS styling for a ``<div>`` around the parameter table
	stylesForParamstableWrapper = false,

	---String, pattern that identifies a page to be a documentation subpage
	patternForMatchingSubpage = false,

	---String, pattern for ``string.format()`` from which the documentation subpage
	---title is created; e.g. "%s/doc" to create "Foo/doc" from "Foo"
	patternForCreatingSubpage = false,

	---String, CSS class for the table of contents,
	---intended to be used for suppressing the numbering
	classForToc = false,

	---String, HTML to display when the default/example/auto value of
	---a parameter is "True" (1)
	paramDetailValueTrue = false,

	---String, HTML to display when the default/example/auto value of
	---a parameter is "False" (0)
	paramDetailValueFalse = false
}

---Global table with various pieces of information
---set and accessed throughout the process
local Data = {
	---``mw.html`` table, wrapper element around the entire output: ``<div class="mw-templatedata-doc-wrap">``
	outputWrapper = false,

	---Table, initial TemplateData JSON object, obtained from
	---decoding the TemplateData JSON code in ``TemplateData.getPlainJSON()``
	templdataJsonTurnedLua = false,

	---Table, stores all parameter inheritances in the format ``{["child"]="parent"}``,
	---filled in ``processTempldataJson()`` and used in ``applyInheritance()``
	heirs = false,

	---Boolean, whether to hide the entire output
	hideEntireOutput = false,

    ---Boolean, whether the main template description is missing,
	---is set in ``makeTemplateOrParamDescription()``
	templateDescriptionIsMissing = false,

    ---Boolean, whether old syntax was encountered in at least one parameter's type field
	---(old syntax = one of the three deprecated type values "string/line",
	---"string/wiki-page-name", or "string/wiki-user-name"),
	---is set in ``makeParamRow()`` when making the cell for the parameter type
	oldSyntaxInAnyParameterType = false,

	---Boolean, whether to display a table of contents, used in ``makeEntireHtmlOutput()``
	showToc = false,

	---Table, sorted array of parameter names, set in ``getParameterOrder()``
	---either from the "paramOrder" array from JSON or from the params'
	---order in the TemplateData JSON code
	order = false,

	---Table, the Lua representation of the "params" object from JSON,
	---contains all information about all parameters
	params = false,

	---String, concatenation of all error messages that are created in various functions
	stringWithAllErrors = false,

	---String, language code of the target output language
	slang = false,

	---String, TemplateData JSON code generated from the processed input
	---(e.g. stripped of markup), is used to make the invisible ``<templatedata>`` tag,
	---is set in ``processJsonAndMakeOutputFromIt()``
	templdataJsonPlain = false,

	---Boolean, whether to fill ``Data.templdataJsonPlain`` in
	---``processJsonAndMakeOutputFromIt()``, i.e. make effective ``<templatedata>`` tags
	dontMakeRawTempldataTable = false,

	---Boolean, whether to display the raw TemplateData table (used in
	---``processJsonAndMakeOutputFromIt()``)
	showRawTempldataTable = false,

	---String, unprocessed TemplateData JSON code (comments *are* removed, though)
	---that was found on the page
	templdataJson = false,

	---String, contains ``<templatedata>`` wikitext; either the one found
	---on the page (set in ``main()``) or the one created from the input
	---(set in ``processJsonAndMakeOutputFromIt()``)
	templdataWikitext = false,

	---Table, the Lua representation of the entire processed
	---TemplateData JSON object, filled in ``processTempldataJson()``
	---from ``Data.templdataJsonTurnedLua``
	tree = false,

	---Table, similar to ``Data.tree`` (but does not contain any
	---parameter information), used for creating a functional
	---``<templatedata>`` tag
	treeForExport = false,

	---Table, the ``mw.title`` object of the current page
	---(or the subpage defined in Config, if that one has to be searched for TemplateData code)
	pageTitleObject = false
}

---Pattern for finding a parameter object in the TemplateData JSON code,
---the "%s" in the middle that is not "%%s" will be replaced by
---the parameter name, in ``findPositionOfParameterObjectInJson()``
local JsonParameterPattern  = "[{,]%%s*(['\"])%s%%1%%s*:%%s*%%{"


---Table with all valid parameter types, as per https://www.mediawiki.org/wiki/Extension:TemplateData#parameter_types
local ValidParameterTypes = {
	["boolean"] = true,
	["content"] = true,
	["date"] = true,
	["line"] = true,
	["number"] = true,
	["string"] = true,
	["unknown"] = true,
	["url"] = true,
	["wiki-file-name"] = true,
	["wiki-page-name"] = true,
	["wiki-template-name"] = true,
	["wiki-user-name"] = true,
	["unbalanced-wikitext"] = true,

	-- the following three are old syntax
	["string/line"] = "line",
	["string/wiki-page-name"] = "wiki-page-name",
	["string/wiki-user-name"] = "wiki-user-name"
}

---Table that contains all expected keys in the ``params`` or root JSON objects
---of the TemplateData JSON code; the values in these tables are "content flags"
---that are needed by ``processTempldataJson()``
local ValidJsonKeys = {
	params = {
		-- all valid JSON keys in the "params" object
		["aliases"] = "table",
		["autovalue"] = "string",
		["default"] = "string table I18N nowiki",
		["deprecated"] = "boolean string",
		["description"] = "string table I18N",
		["example"] = "string table I18N nowiki",
		["label"] = "string table I18N",
		["inherits"] = "string",
		["required"] = "boolean",
		["suggested"] = "boolean",
		["type"] = "string"
	},
	root = {
		-- all valid JSON keys in the root object
		["description"] = "string table I18N",
		["format"] = "string",
		["maps"] = "table",
		["params"] = "table",
		["paramOrder"] = "table",
		["sets"] = "table"
	}
}



-----------------------------------------------------------------
-- Utility functions


---Append the ``errorMsg`` string to the global
---error string, ``Data.stringWithAllErrors``.
---@param errorMsg string The error message to append
local function Fault(errorMsg)
	if Data.stringWithAllErrors then
		Data.stringWithAllErrors = string.format("%s *** %s", Data.stringWithAllErrors, errorMsg)
	else
		Data.stringWithAllErrors = errorMsg
	end
end -- Fault()



---Reduce all consecutive spaces and newlines to a single space,
---a bit like HTML does.
---@param inputText string
---@return string
local function collapseWhitespace(inputText)
	return inputText:gsub("%s*\n%s*", " "):gsub("%s%s+", " ")
end -- collapseWhitespace()



-----------------------------------------------------------------
-- Regular functions



---Find the position of the ``parameterName`` in the ``Data.templdataJson``.
---Be intelligent about it, i.e. search for a JSON object that has a key that
---is recognized to be associated with a parameter object.
---@param parameterName string The name of the parameter to search for
---@param init number The position to start the search at, defaults to 1
---@return number pos The position of the parameter
local function findPositionOfParameterObjectInJson(parameterName, init)
	-- JsonParameterPattern has the regex for finding the parameter object,
	-- but it is missing the parameter name, so add it, but it
	-- must be prepared first, i.e. special regex characters in
	-- the parameter name must be escaped:
	local formatStringForPattern = parameterName
		:gsub("%%", "%%%%") -- escape the escape character
		:gsub("([%-.()+*?^$%[%]])", "%%%1") -- escape other special chars
	local parameterPattern = string.format(JsonParameterPattern, formatStringForPattern)

	-- find the position of the parameter name in the JSON
	local startIndex, endIndex = string.find(Data.templdataJson, parameterPattern, init)

	local result

	-- check if there is a valid parameter object at the position of the parameter name
	-- that we found, and if not, retry in the text that follows it
	while startIndex and not result do
		-- everything after the parameter name
		local remainingText = string.sub(Data.templdataJson, endIndex + 1)

		-- the first key of the parameter object
		local slice = remainingText:match("^%s*\"([^\"]+)\"s*:") or remainingText:match("^%s*'([^']+)'%s*:")

		if (slice and ValidJsonKeys.params[slice]) or remainingText:match("^%s*%}") then
			-- the first key of the parameter object is a recognized key
			-- or the parameter object is empty
			result = endIndex
		else
			-- there is no valid parameter object at the position of the parameter name
			-- that we found, so retry in the text that follows it
			startIndex, endIndex = string.find(Data.templdataJson, parameterPattern, endIndex)
		end
	end

	return result
end -- findPositionOfParameterObjectInJson()



---Return a MediaWiki system message whose name begins with ``templatedata-``,
---localized text for the current language.
---@param msgName string The name of the system message after ``templatedata-``
---@return any
local function getLocalizedText(msgName)
	return mw.message.new("templatedata-" .. msgName)
		:inLanguage(Data.slang) -- translate to target language
		:plain() -- transform to wikitext
end -- getLocalizedText()



---Attempt to return the Boolean value of an input.
---@param toCheck string|boolean|nil The input to retrieve the Boolean value of
---@return boolean result True if ``toCheck`` is not empty and not "0", "n", or "no"; false otherwise
local function checkBool(toCheck)
	local inputType = type(toCheck)
	local result
	if inputType == "string" then
		result = trim(toCheck)
		result = (result ~= "" and result ~= "0" and result ~= "n" and result ~= "no")
	elseif inputType == "boolean" then
		-- input already is boolean, simply return it
		result = toCheck
	else
		-- input is neither string nor boolean
		result = false
	end
	return result
end -- checkBool()



---Turn the error messages stored in the global ``Data.stringWithAllErrors``
---into an "error" ``<span>`` and return it, add the error category if necessary.
---Also add the error messages to the error text that will be displayed above the preview.
---@return string
local function getAllErrorMessages()
	local errorStringToReturn

	if Data.stringWithAllErrors then
		-- all error messages wrapped in an "error"-classed <span>
		local errorElement = mw.html.create("span")
			:addClass("error")
			:wikitext(Data.stringWithAllErrors)
		errorStringToReturn = tostring(errorElement)

		-- display the error messages above the preview
		mw.addWarning("'''TemplateData'''<br/>" .. Data.stringWithAllErrors)

		-- add error category
		if Config.maintenanceCateName then
			errorStringToReturn = errorStringToReturn .. "[[Category:" .. Config.maintenanceCateName .. "]]"
		end
	end

	return errorStringToReturn or ""
end -- getAllErrorMessages()



---Reduce runs of spaces, including newlines, to a single space, so the
---whole string is on one line. Leave ``<noexport>`` blocks alone, but
---remove the <noexport> tags themselves. Manually expand ``{{p}}``s.
---@param inputText string Text to process
---@return string
local function handleNoexportAndWhitespaceAndP(inputText)
	local result

	-- handle noexport
	if inputText:find("<noexport>", 1, true) then
		local i = 1
		local startIndex, endIndex = inputText:find("<noexport>", i, true)
		result = ""

		-- handle nested tags
		while startIndex do
			if startIndex > 1 then
				result = result .. collapseWhitespace(inputText:sub(i, startIndex - 1))
			end
			i = endIndex + 1
			startIndex, endIndex = inputText:find("</noexport>", i, true)
			if startIndex then
				result = result .. inputText:sub(i, startIndex - 1)
				i = endIndex + 1
				startIndex, endIndex = inputText:find("<noexport>", i, true)
			else
				Fault("missing closing tag </noexport>")
			end
		end
		result = result .. inputText:sub(i)

	else
		result = collapseWhitespace(inputText)
	end

	-- handle {{p}}
	result = result:gsub("%{%{p%|(.-)%}%}", "<code>[[#templatedata:%1|$%1]]</code>")

	return result
end -- handleNoexportAndWhitespaceAndP()



---Extract the value of the key that matches the wiki language (``Data.slang``)
---from the ``langTable``. For instance, if the wiki language is "en" and the
---``langTable`` is ``{["pl"]="foo", ["en"]="bar"}``, return "bar".
---
---Make several attempts to find the wiki language: Exact match, partial match,
---match of fallbacks. Use any of the languages if neither of the attempts is
---successful.
---@param langTable table The table from which to extract; keys should be valid
---language codes and values should be the localized texts of the respective language
---@return string|nil text The localized text in the wiki language
---@return table|nil restTable The ``langTable`` without the key that has ``text`` as value
local function extractWikilangText(langTable)
	-- normalize the ``langTable``: remove languages for which
	-- the value is not a string or is an empty string;
	-- also, while at it, count the languages
	local langTableNormalized = {}
	local langCount = 0
	for langCode, localizedText in pairs(langTable) do
		if type(localizedText) == "string" then
			localizedText = trim(localizedText)
			if localizedText ~= "" then
				langTableNormalized[langCode] = localizedText
				langCount = langCount + 1
			end
		end
	end

	if langCount == 0 then
		-- there are no languages with valid strings in the langTable
		return nil, nil
	end

	-- the value in the ``langTableNormalized`` at the key that
	-- equals the wiki language (i.e. the text that we are searching for)
	-- or, if there is only one language in the table, that language
	local textInWikilangOrInOnlylang

	-- the ``langTableNormalized`` without the ``textInWikilangOrInOnlylang``
	local restTable

	for langCode, localizedText in pairs(langTableNormalized) do
		if localizedText then
			if langCount == 1 then
				-- there is only one language in the langTableNormalized,
				-- so take its value
				textInWikilangOrInOnlylang = localizedText
			elseif langCode:lower() == Data.slang then
				-- there is more than one language and we have reached
				-- the wiki language
				textInWikilangOrInOnlylang = localizedText
				-- remove the wiki language from the table
				langTableNormalized[langCode] = nil
				-- store the remaining languages
				restTable = langTableNormalized
				break
			end
		end
	end

	if not textInWikilangOrInOnlylang then
		-- the langTableNormalized contains more than one language,
		-- but not the wiki language

		-- hence, search again for the wiki language, but this time,
		-- look for keys that begin with the wiki language code plus
		-- a hyphen (e.g. match "zh-Hans" if wiki lang is "zh")
		-- (but "zh-Foo bar" also, for that matter)
		local langPattern = "^" .. Data.slang .. "%-"
		for langCode, localizedText in pairs(langTableNormalized) do
			if localizedText and langCode:lower():match(langPattern) then
				textInWikilangOrInOnlylang = localizedText
				langTableNormalized[langCode] = nil
				restTable = langTableNormalized
				break
			end
		end
	end

	if not textInWikilangOrInOnlylang then
		-- still haven't found the wiki language, so search for
		-- fallbacks of the wiki language
		local others = mw.language.getFallbacksFor(Data.slang)
		table.insert(others, "en") -- always add EN as the last fallback
		for i = 1, #others do
			local fallbackLang = others[i]
			if langTableNormalized[fallbackLang] then
				textInWikilangOrInOnlylang = langTableNormalized[fallbackLang]
				langTableNormalized[fallbackLang] = nil
				restTable = langTableNormalized
				break
			end
		end
	end

	if not textInWikilangOrInOnlylang then
		-- searching for fallbacks still didn't yield any results,
		-- so now we can only resort to picking any language
		for langCode, localizedText in pairs(langTableNormalized) do
			if localizedText then
				textInWikilangOrInOnlylang = localizedText
				langTableNormalized[langCode] = nil
				restTable = langTableNormalized
				break
			end
		end
	end

	if restTable then
		-- report invalid language tags
		for langCode, localizedText in pairs(restTable) do
			if localizedText then
				-- match any 2- to 3-ASCII-letter string, optionally appended
				-- with a hyphen and more ASCII letters, and optionally surrounded
				-- by spaces
				local baseCode = langCode:match("^%s*(%a%a%a?)%s*$") or langCode:match("^%s*(%a%a%a?)%-%a*%s*$")
				if not baseCode or not mw.language.isKnownLanguageTag(baseCode) then
					Fault(string.format("Invalid <code>lang=%s</code>", langCode))
				end
			end
		end
	end

	return textInWikilangOrInOnlylang, restTable
end -- extractWikilangText()



---For each of the parameters defined in ``Data.heirs``, inherit
---the parameter information and update the parameter's entries in
---``Data.params`` and ``Data.tree.params``, including setting the
---``inherits`` object to nil. Also remove the parameter from
---``Data.heirs`` if inheriting was performed successfully.
local function applyInheritance()
	-- count number of parameters in Data.heirs
	local paramCount = 0
	for _ in pairs(Data.heirs) do
		paramCount = paramCount + 1
	end

	if paramCount == 0 then
		-- Data.heirs is empty, nothing to process
		return
	end

	local dataParams = Data.params
	local dataTreeParams = Data.tree.params

	local paramsToHandle = paramCount

	-- this following slightly awkward double loop facilitates
	-- easily avoiding circular inheritances; its functionality
	-- is basically: "for each param in Data.heirs, perform the
	-- inheritance"
	for _ = 1, paramCount do
		for paramChild, paramParent in pairs(Data.heirs) do
			if paramParent and not Data.heirs[paramParent] then
				paramsToHandle = paramsToHandle - 1

				-- do not handle this child again
				Data.heirs[paramChild] = nil
				dataTreeParams[paramChild].inherits = nil

				-- inherit paraminfo (label, type, etc.) from the parent
				-- to the child: first get all the paraminfo from parent,
				-- then add the paraminfo from the child to that and
				-- overwrite data where necessary

				local newParaminfoForChild = {}

				-- copy paraminfo from parent to temporary table
				for k, v in pairs(dataParams[paramParent]) do
					newParaminfoForChild[k] = v
				end

				-- add paraminfo from child to temporary table
				if dataParams[paramChild] then
					for k, v in pairs(dataParams[paramChild]) do
						if type(v) ~= "nil" then
							newParaminfoForChild[k] = v
						end
					end
				end

				-- the temporary table newParaminfoForChild now contains
				-- all of the child's paraminfo plus the paraminfo
				-- inherited from the parent, so apply it
				dataParams[paramChild] = newParaminfoForChild

				-- repeat the process for Data.tree.params now
				-- (the first time above was for Data.params)

				newParaminfoForChild = {}
				for k, v in pairs(dataTreeParams[paramParent]) do
					newParaminfoForChild[k] = v
				end
				for k, v in pairs(dataTreeParams[paramChild]) do
					if type(v) ~= "nil" then
						newParaminfoForChild[k] = v
					end
				end
				dataTreeParams[paramChild] = newParaminfoForChild
			end
		end
	end

	if paramsToHandle > 0 then
		local errorString
		for paramChild, paramParent in pairs(Data.heirs) do
			if paramParent then
				if errorString then
					errorString = errorString .. " &#124; " .. paramChild
				else
					errorString = "Circular inherits: " .. paramChild
				end
			end
		end
		Fault(errorString)
	end
end -- applyInheritance()



---Make a description string for the entire template or for a specific parameter,
---based on the information passed in the input ``tableWithDescription``. That input table
---needs to have a ``description`` key whose value is either the description string directly,
---or a table with two elements where the first one is a description string and the second
---one an array of description details.
---
---Use the global ``Config.textIfDescriptionIsMissing`` here and set the global
---``Data.templateDescriptionIsMissing`` here.
---@param tableWithDescription table Table with a ``description`` key
---@param shouldFailIfNoDescription boolean Whether to fail if no description can be gotten, only has an effect if the global var is set
---@return table html The HTML of the description string
local function makeTemplateOrParamDescription(tableWithDescription, shouldFailIfNoDescription)
	local descriptionOutput = mw.html.create("div")
	local outputAddition -- will be appended to the output

	if tableWithDescription and tableWithDescription.description then
		if type(tableWithDescription.description) == "string" then
			-- description is a simple string, just display it
			descriptionOutput:wikitext(tableWithDescription.description)
		else
			-- description is not a string, but (likely) a table, so
			-- display its first element as description and display its
			-- second element as a list
			descriptionOutput:wikitext(tableWithDescription.description[1])

			outputAddition = mw.html.create("ul")
			if not Config.alwaysDisplayRawTempldataTable then
				-- hide list of description details
				outputAddition:addClass("templatedata-maintain")
					:css("display", "none")
			end
			for k, v in pairs(tableWithDescription.description[2]) do
				outputAddition:node(mw.html.create("li")
					:node(mw.html.create("code"):wikitext(k))
					:node(mw.html.create("br"))
					:wikitext(handleNoexportAndWhitespaceAndP(v)))
			end
		end

	elseif Config.textIfDescriptionIsMissing and shouldFailIfNoDescription then
		descriptionOutput:addClass("error")
			:wikitext(Config.textIfDescriptionIsMissing)
		Data.templateDescriptionIsMissing = true

	else
		-- either ``tableWithDescription`` is nil or it doesn't have a ``description`` key,
		-- and either there is no text for missing descriptions defined or we should fail silently,
		-- so don't output anything
		return
	end

	if outputAddition then
		-- combine outputAddition and descriptionOutput in a div
		return mw.html.create("div")
			:node(descriptionOutput)
			:node(outputAddition)
	end
	return descriptionOutput
end -- makeTemplateOrParamDescription()



---Fill ``Data.order`` with the names of all parameters (based on ``Data.tree.params``),
---sorted in a way that represents their order in the TemplateData JSON code.
local function sortParametersByTheirOrderInJson()
	local paramCount = 0
	local nameOfFirstParam -- only needed if there is only one parameter

	-- iterate over all parameters in an unspecified order
	-- to find out if there are 0, 1, or >= 2 parameters
	for parameterName, _ in pairs(Data.tree.params) do
		if paramCount == 0 then
			-- we are at the very first parameter
			Data.order = {}
			paramCount = 1
			nameOfFirstParam = parameterName
		else
			-- we are at the second parameter, this is all
			-- that we need to know for now, so stop looping
			paramCount = 2
			break
		end
	end

	if paramCount < 2 then
		-- there is no or only one parameter, so there is no
		-- order to determine – simply add the param to the array
		if nameOfFirstParam then
			table.insert(Data.order, nameOfFirstParam)
		end
		return
	end

	-- there are at least 2 parameters, so continue

	-- for each parameter, get its position in the JSON and
	-- attach its name to that position, then later sort the
	-- positions and restore the names from that;
	-- with this method, it doesn't matter that pairs() yields
	-- an unspecified order

	local parameterPositions = {} -- array of parameter positions
	local parameterPositionToName = {} -- table that stores the parameter name for each position

	-- iterate over all parameters in an unspecified order
	for parameterName, _ in pairs(Data.tree.params) do

		-- determine the position of this parameter
		local parameterPosition = findPositionOfParameterObjectInJson(parameterName, 1)

		if parameterPosition then
			-- save the position of this parameter to the array of positions
			table.insert(parameterPositions, parameterPosition)

			-- attach the name of this parameter to its position
			parameterPositionToName[parameterPosition] = parameterName

			-- try to find the parameter a second time
			if findPositionOfParameterObjectInJson(parameterName, parameterPosition) then
				Fault(string.format("Parameter '%s' detected twice", parameterName))
			end
		else
			Fault(string.format("Parameter '%s' not detected", parameterName))
		end

	end

	-- iterate over the table that stored the parameter names with their
	-- positions in the numerical order of positions and add the parameter
	-- names to Data.order
	table.sort(parameterPositions)
	for paramPosition = 1, #parameterPositions do
		-- turn current position into name
		local parameterName = parameterPositionToName[parameterPositions[paramPosition]]
		table.insert(Data.order, parameterName)
	end
end -- sortParametersByTheirOrderInJson()



---Fill the global ``Data.order`` array with the parameter names,
---ordered in the correct way. If ``Data.tree.paramOrder`` is defined,
---i.e. if there is a ``paramOrder`` array in the TemplateData JSON code,
---then use that, otherwise use the order in which the parameters are listed
---in the TemplateData JSON code.
local function getParameterOrder()
	if not Data.templdataJson then
		return
	end

	if Data.tree["paramOrder"] then
		-- there is already an explicit order defined, so simply copy it
		Data.order = {}
		for i = 1, #Data.tree["paramOrder"] do
			table.insert(Data.order, Data.tree["paramOrder"][i])
		end
	else
		-- there is no order explicitly defined, so get it from how the
		-- parameters are defined in the TemplateData JSON code
		sortParametersByTheirOrderInJson()
	end
end -- getParameterOrder()



---If the ``key`` is a number, set the ``data-sort-value`` attribute of
---the ``cell`` to a zero-padded, five-character string representation of
---the number.
---@param cell table The cell whose attribute is to be adjusted
---@param key string The string which is a number or not
---@return table cell The cell with the adjusted ``data-sort-value`` attribute
local function adjustSortvalueForNumericalKey(cell, key)
	if key:match("^%d+$") then
		-- key is a number, so set sort-value to a zero-padded,
		-- five-character string representation of the number
		cell:attr("data-sort-value", string.format("%05d", tonumber(key)))
	end
	return cell
end -- adjustSortvalueForNumericalKey()



---Make a row for the parameter outputtable with information about one parameter.
---Use the parameter's entry in the global ``Data.tree`` for that.
---@param parameterName string The name of the parameter for which to make the row
---@return table paramRow The ``<tr>`` element with the information
local function makeParamRow(parameterName)
	-- whether this row should not be marked with a red border
	local legal = true

	local fine = function(paramName)
		if paramName == trim(paramName) and paramName ~= "" then
			return not paramName:find("%|=\n") and not paramName:find("%s%s")
		end
		return false
	end

	-- get data about this parameter
	local paraminfo = Data.tree.params[parameterName]

	-- turn empty strings in the paraminfo into falses
	for k, v in pairs(paraminfo) do
		if v == "" then
			paraminfo[k] = false
		end
	end

	local Modes = { -- enum
		["Required"] = 1,
		["Suggested"] = 2,
		["Optional"] = 3,
		["Deprecated"] = 4
	}
	-- inverse of the enum, so that e.g. statusNames[1] == "required"
	local statusNames = {}
	for statusname, statusvalue in pairs(Modes) do
		table.insert(statusNames, statusvalue, statusname:lower())
	end

	-- prepare status (needed for border-left on first (label) table cell)
	local mode
	if paraminfo.required then
		mode = Modes.Required
		if paraminfo.deprecated then
			Fault(string.format("Required deprecated <code>%s</code>", parameterName))
			legal = false
		end
	elseif paraminfo.deprecated then
		mode = Modes.Deprecated
	elseif paraminfo.suggested then
		mode = Modes.Suggested
	else
		mode = Modes.Optional
	end
	local status = statusNames[mode]

	-- make cell 1: label
	local paramLabelCell = mw.html.create("td")
	local sortkey = paraminfo.label or parameterName
	paramLabelCell = adjustSortvalueForNumericalKey(paramLabelCell, sortkey)
	paramLabelCell:wikitext(sortkey)
	-- label styling, depending on parameter mode (deprecated/required/...)
	paramLabelCell:addClass("templatedata-doc-param")
	paramLabelCell:addClass("param-" .. status)

	-- make cell 2: name and aliases
	local codeElem = mw.html.create("code")
	codeElem:wikitext(parameterName)
	if not fine(parameterName) then
		codeElem:addClass("error")
		Fault(string.format("Bad ID params.<code>%s</code>", parameterName))
		legal = false
		paramLabelCell:attr("data-sort-value",  " " .. sortkey)
	end

	local paramNameCell = mw.html.create("td"):node(codeElem)
	paramNameCell = adjustSortvalueForNumericalKey(paramNameCell, parameterName)

	-- aliases
	if type(paraminfo.aliases) == "table" then
		local errorWithAnyAlias = false

		-- iterate over all aliases, add them to the cell with the parameter name
		for _, alias in pairs(paraminfo.aliases) do
			paramNameCell:tag("br")

			if type(alias) == "string" then
				if fine(alias) then
					paramNameCell:node(mw.html.create("code"):wikitext(trim(alias)))
				else
					errorWithAnyAlias = true
					paramNameCell:node(mw.html.create("span")
						:addClass("error")
						:css("font-style", "italic")
						:wikitext("string")
					)
				end
			else
				errorWithAnyAlias = true
				paramNameCell:node(mw.html.create("code")
					:addClass("error")
					:wikitext(type(alias))
				)
			end
		end

		if errorWithAnyAlias then
			local invalidvalue = string.format("params.<code>%s</code>.aliases", parameterName)
			Fault(getLocalizedText("invalid-value"):gsub("$1", invalidvalue))
			legal = false
		end
	end
	paramNameCell:css("font-size", "92%"):css("white-space", "nowrap")

	-- make cell 3: description
	local descriptionCell = mw.html.create("td")
	local descriptionText = makeTemplateOrParamDescription(paraminfo)
	if descriptionText then
		descriptionCell:node(descriptionText)
	end

	-- details (below description): default, example, auto
	if paraminfo.default or paraminfo.example or paraminfo.autovalue then
		local details = { "default", "example", "autovalue" }
		local allDetails = mw.html.create("dl"):cssText("font-size: small;")

		-- iterate over the three types of details
		for i = 1, #details do
			local thisDetailType = details[i] -- current type: default, example, or autovalue
			local detaildata = paraminfo[thisDetailType]
			if detaildata then
				-- label for the detail (e.g. "Auto value: ")
				local thisDetailLabel = mw.html.create("dt"):wikitext(getLocalizedText("doc-param-" .. thisDetailType) .. ": ")
				if (string.len(detaildata) < 80) then
					thisDetailLabel:cssText("float: left; margin-right: 1.6em; font-weight: normal;")
				end
				allDetails:node(thisDetailLabel)

				-- content of the detail
				local thisDetailContent = mw.html.create("dd")
				if paraminfo.type == "boolean" then
					-- special formatting for parameters with Boolean values
					-- (only if available, otherwise fall back to standard formatting)
					if detaildata == "0" then
						thisDetailContent:wikitext(Config.paramDetailValueFalse or "<code>0</code>")
					elseif detaildata == "1" then
						thisDetailContent:wikitext(Config.paramDetailValueTrue or "<code>1</code>")
					else
						thisDetailContent:wikitext("<code>" .. detaildata .. "</code>")
					end
				else
					thisDetailContent:wikitext("<code>" .. detaildata .. "</code>")
				end
				allDetails:node(thisDetailContent)
			end
		end
		descriptionCell:node(allDetails)
	end

	-- make cell 4: type
	local typeCell = mw.html.create("td")
	if paraminfo.type then

		-- Convert the parameter type to modern syntax if necessary. If conversion
		-- was necessary, then ``paramTypeInModernSyntax`` will hold the name of
		-- the parameter in modern syntax.
		-- Otherwise, it will be ``true`` if the parameter type is known and valid, and ``nil`` if not.
		local paramTypeInModernSyntax = ValidParameterTypes[paraminfo.type]

		if paramTypeInModernSyntax then
			if type(paramTypeInModernSyntax) == "string" then
				-- the parameter type is in old syntax, so update it in the data source
				Data.params[parameterName].type = paramTypeInModernSyntax
				typeCell:wikitext(getLocalizedText("doc-param-type-" .. paramTypeInModernSyntax))
					:tag("br")
				typeCell:node(mw.html.create("span")
					:addClass("error")
					:wikitext(paraminfo.type))
				Data.oldSyntaxInAnyParameterType = true
			else
				-- the parameter type is in modern syntax, nothing to change
				typeCell:wikitext(getLocalizedText("doc-param-type-" .. paraminfo.type))
			end
		else
			-- the parameter type is not defined in the list of valid parameter types
			Data.params[parameterName].type = "unknown"
			typeCell:addClass("error")
				:wikitext("INVALID")
			Fault(getLocalizedText("invalid-value"):gsub("$1", string.format("params.<code>%s</code>.type", parameterName)))
			legal = false
		end
	else
		typeCell:wikitext(getLocalizedText("doc-param-type-unknown"))
	end

	-- make cell 5: status
	local statusText = mw.html.create("div")
		:wikitext(getLocalizedText("doc-param-status-" .. status))
	if mode == Modes.Required or mode == Modes.Deprecated then
		-- emphasize the status for required and deprecated params
		statusText:css("font-weight", "bold")
		-- add deprecation details, if provided
		if type(paraminfo.deprecated) == "string" then
			statusText:node(mw.html.create("div")
				:css("font-size", "92%"):css("font-style", "italic")
				:wikitext(paraminfo.deprecated)
			)
		end
	end
	local statusCell = mw.html.create("td")
		-- sort by status order, not by status name
		:attr("data-sort-value", tostring(mode))
		:node(statusText)


	-- combine the five cells into a table row
	local paramRow = mw.html.create("tr")
		:attr("id", mw.uri.anchorEncode("templatedata:" .. parameterName)) -- for linking
		:node(paramLabelCell)
		:node(paramNameCell)
		:node(descriptionCell)
		:node(typeCell)
		:node(statusCell)
		:newline()
	if not legal then
		paramRow:addClass("templatedata-doc-param-illegal")
	end

	return paramRow
end -- makeParamRow()



---Create the table that displays information about the parameters
---from ``Data.tree`` and ``Data.tree.params``. Include header and styling.
---@return table|nil html The table object (or div if a wrapper is defined)
local function makeParamsTable()
	if not Data.tree or not Data.tree.params then
		-- there is nothing to display
		return
	end

	local paramsTable = mw.html.create("table")
		:addClass("terraria")
		:addClass("lined")
		:addClass("templatedata-doc")

	-- make header
	local headerRow = mw.html.create("tr")
		:node(mw.html.create("th")
			:attr("colspan", "2")
			:wikitext(getLocalizedText("doc-param-name")))
		:node(mw.html.create("th")
			:wikitext(getLocalizedText("doc-param-desc")))
		:node(mw.html.create("th")
			:wikitext(getLocalizedText("doc-param-type")))
		:node(mw.html.create("th")
			:wikitext(getLocalizedText("doc-param-status")))

	paramsTable:newline()
		:node(headerRow)
		:newline()

	-- fill Data.order
	getParameterOrder()

	if Data.order then
		-- make table sortable if there are multiple parameters
		if #Data.order > 1 then
			paramsTable:addClass("sortable")
		end

		-- add table row for each parameter
		for i = 1, #Data.order do
			paramsTable:node(makeParamRow(Data.order[i]))
		end
	end

	-- apply custom styling if necessary and return
	if Config.stylesForParamstable then
		paramsTable:cssText(Config.stylesForParamstable)
	end
	if Config.stylesForParamstableWrapper then
		return mw.html.create("div")
			:cssText(Config.stylesForParamstableWrapper)
			:node(paramsTable)
	else
		return paramsTable
	end
end -- makeParamsTable()



---Turn the HTML of ``Data.outputWrapper`` into a string,
---append all error messages.
---@return string
local function outputhtmlToStringAndAppendErrors()
	local stringToReturn

	if Data.outputWrapper then
		-- regular output, formatted table
		stringToReturn = tostring(Data.outputWrapper)
	elseif Data.templdataWikitext then
		-- only raw templatedata table
		stringToReturn = Data.templdataWikitext
	else
		stringToReturn = ""
	end

	return stringToReturn .. getAllErrorMessages()
end -- outputhtmlToStringAndAppendErrors()



---Find TemplateData JSON within the page source of the ``Data.pageTitleObject``.
---Search for data enclosed in ``<templatedata>...</templatedata>``
---and for data enclosed in ``|JSON=...|data=1``.
---@return string|nil data The data that was found, ``nil`` if none found
local function findJsonInPage()
	local pageText = Data.pageTitleObject:getContent()

	-- search for a templatedata opening tag ("opener")
	local openerStartIndex, openerEndIndex = string.find(pageText, "<templatedata>", 1, true)
	local tags = true -- the string that was found is enclosed in templatedata tags

	if not openerStartIndex then
		-- couldn't find a templatedata opening tag, so search for template opening string ("opener")
		openerStartIndex, openerEndIndex = string.find(pageText, "|JSON=", 1, true)
		tags = false
	end

	if openerStartIndex then
		-- find the closing part of the enclosure ("closer")
		local closerStartIndex
		if tags then
			-- the data is enclosed in templatedata tags, so search for the closing tag
			closerStartIndex = pageText:find("</templatedata>", openerEndIndex, true)
		else
			-- the data is in a template call, so search for the closing braces
			closerStartIndex = pageText:find("|data=1}}", openerEndIndex, true)
		end
		if closerStartIndex then
			-- extract the data, now that we have the indices of the enclosure
		   return trim(string.sub(pageText, openerEndIndex + 1, closerStartIndex - 1))
		end
	end
	return
end -- findJsonInPage()



---Remove most markup from the ``inputText``:
---* Turn newlines into spaces
---* Remove everything in ``<noexport>`` tags
---* Replace ``{{p}}``s with quotes
---* Remove/replace all wiki markup
---* Escape HTML entities
---@param inputText string
---@return string result
local function removeMarkup(inputText)
	-- (This function is called for the template description and
	-- all parameter properties whose "content flags" contain "I18N",
	-- and its result is put into Data.treeForExport.)

	local result

	if inputText then
		-- newlines to spaces
		result = inputText:gsub("\n", " ")
		-- remove noexport
		if result:find("<noexport>", 1, true) then
			result = result:gsub("<noexport>(.*)</noexport>", "")
		end
		-- {{p}} to quotes
		result = result:gsub("%{%{p%|(.-)%}%}", "&#34;%1&#34;")
		-- wiki markup
		result = plaintext._main(result)
		-- escape HTML entities
		if result:find("&", 1, true) then
			result = mw.text.decode(result)
		end
	end

	return result
end -- removeMarkup()



---Make a JSON string with TemplateData JSON code, from the
---information that was previously collected (``Data.treeForExport``,
---``Data.order``, and ``Data.params`` in particular).
---@return string json The TemplateData JSON code
local function makeTempldataJsonFromData()
	local jsonString
	if Data.treeForExport then
		-- Data.treeForExport contains everything that is not
		-- parameter information, e.g. template description, so
		-- begin with that, and replace the closing brace with a comma
		jsonString = mw.text.jsonEncode(Data.treeForExport):gsub("%}$", ",")
	else
		jsonString = "{"
	end

	-- add parameter information
	jsonString = jsonString .. "\n\"params\":{"
	if Data.order then
		local paramsJson = {} -- table of parameter JSON strings to concatenate

		-- iterate over parameters
		for i = 1, #Data.order do
			local parameterName = Data.order[i]
			table.insert(paramsJson,
				mw.text.jsonEncode(parameterName) .. -- param name
				":" ..
				mw.text.jsonEncode(Data.params[parameterName]) -- param info
			)
		end
		jsonString = jsonString .. table.concat(paramsJson, ",\n")

	end
	jsonString = jsonString .. "\n}\n}"

	return jsonString
end -- makeTempldataJsonFromData()



---Take the ``Data.templdataJsonTurnedLua`` table and process it,
---which includes removal of wiki markup and the like.
---Put the data in the tables ``Data.tree`` and ``Data.treeForExport``.
---The function should be called once with a nil ``parameterName``, to process
---the root TemplateData JSON object, and then once for each parameter.
---@param parameterName string|nil The name of the parameter for which to perform the operations
local function processTempldataJson(parameterName)

	-- helper function for making error messages
	local f = function (a, at)
		local result
		if at then
			result = string.format("<code>params.%s</code>", at)
		else
			result = "''root''"
		end
		if a then
			result = string.format("%s<code>.%s</code>", result, a)
		end
		return result
	end

	local parent -- the table to iterate over
	if parameterName then
		-- we are iterating over one parameter object of
		-- the (Lua representation of the) TemplateData JSON code
		parent = Data.templdataJsonTurnedLua["params"][parameterName]
	else
		-- we are iterating over the root element of
		-- the (Lua representation of the) TemplateData JSON code
		parent = Data.templdataJsonTurnedLua
	end

	if type(parent) ~= "table" then
		Fault(f() .. " needs to be of <code>object</code> type")
		return
	end

	local dataToPutIntoTree

	-- tables that will be filled with the contents of the JSON object,
	-- will be set to Data.tree or Data.tree.params (Data.treeForExport or Data.params resp.)
	-- (depending on whether we are iterating over one param or root) once needed
	local treeTarget, treeForExportTarget

	-- name of the parameter of this function call,
	-- nil when calling this function for the root JSON object
	local thisParameter

	-- table of keys that are valid for the parent JSON object
	local validKeys
	-- The values of this table are "content flags" that define the content type
	-- of the respective values in the JSON object.
	-- For example, the "description" key in this table (when a ``parameterName`` is not defined)
	-- has the content flags "string table I18N", which means that it can be a string or a table of languages,
	-- which accurately represents its characteristics in the root TemplateData JSON object.


	if parameterName then
		validKeys = ValidJsonKeys.params
		if type(parameterName) == "number" then
			thisParameter = tostring(parameterName)
		else
			thisParameter = parameterName
		end
	else
		validKeys = ValidJsonKeys.root
	end

	-- iterate over the children of the parent JSON object
	for jsonKey, jsonValue in pairs(parent) do

		-- get the "content flags" for this JSON key, e.g. "boolean" or "string table I18N"
		local contentFlags = validKeys[jsonKey]

		if contentFlags then
			local valueType = type(jsonValue)

			if valueType == "string" then
				jsonValue = trim(jsonValue)
			end

			if contentFlags:find(valueType, 1, true) then

				-- put the contents of this JSON object into Data.tree and Data.treeForExport

				if contentFlags:find("I18N", 1, true) then

					if valueType == "string" then
						dataToPutIntoTree = handleNoexportAndWhitespaceAndP(jsonValue)
					else
						local translated
						-- get the string for the current language from the table of defined languages
						jsonValue, translated = extractWikilangText(jsonValue)
						if jsonValue then
							if translated and jsonKey == "description" then
								dataToPutIntoTree = {
									[1] = handleNoexportAndWhitespaceAndP(jsonValue),
									[2] = translated -- the rest of the language table, minus our language
								}
							else
								dataToPutIntoTree = handleNoexportAndWhitespaceAndP(jsonValue)
							end
						else
							dataToPutIntoTree = false
						end
					end

					if jsonValue then
						if contentFlags:find("nowiki", 1, true) then
							dataToPutIntoTree = mw.text.nowiki(jsonValue)
						else
							jsonValue = removeMarkup(jsonValue)
						end
					end

				else

					if jsonKey == "params" and not parameterName then
						-- we are iterating over the root TemplateData JSON object
						-- and are now at the "params" child, so don't do anything with it
						jsonValue = nil
						dataToPutIntoTree = nil

					elseif jsonKey == "format" and not parameterName then
						-- we are iterating over the root TemplateData JSON object
						-- and are now at the "format" child
						jsonValue = mw.text.decode(jsonValue)
						dataToPutIntoTree = jsonValue

					elseif jsonKey == "inherits" then
						-- we are iterating over one parameter object
						-- and are now at the "inherits" child
						dataToPutIntoTree = jsonValue

						-- set the heirs of the parameter that we are iterating over
						-- in the global inheritance table
						if not Data.heirs then
							Data.heirs = {}
						end
						Data.heirs[thisParameter] = jsonValue

						jsonValue = nil

					elseif valueType == "string" then
						-- the current child is not any of the three above
						-- and is a string
						jsonValue = mw.text.nowiki(jsonValue)
						dataToPutIntoTree = jsonValue

					else
						-- the current child is not any of the three above
						-- and is not a string
						dataToPutIntoTree = jsonValue
					end
				end

				-- put data into tree
				if type(dataToPutIntoTree) ~= "nil" then
					if not treeTarget then
						-- this is the first child for which dataToPutIntoTree is not nil
						if parameterName then
							-- we are iterating over one parameter object, so
							-- put the data into Data.tree.params
							if not Data.tree.params then
								Data.tree.params = {}
							end
							Data.tree.params[thisParameter] = {}
							treeTarget = Data.tree.params[thisParameter]
						else
							-- we are iterating over the root TemplateData JSON object, so
							-- put the data in Data.tree
							Data.tree = {}
							treeTarget = Data.tree
						end
					end
					treeTarget[jsonKey] = dataToPutIntoTree
					dataToPutIntoTree = false
				end

				-- put data into treeForExport
				if type(jsonValue) ~= "nil" then
					if not treeForExportTarget then
						-- this is the first child for which jsonValue is not nil
						if parameterName then
							-- we are iterating over one parameter object, so
							-- put the data into Data.params
							if not Data.params then
								Data.params = {}
							end
							Data.params[thisParameter] = {}
							treeForExportTarget = Data.params[thisParameter]
						else
							-- we are iterating over the root TemplateData JSON object, so
							-- put the data in Data.treeForExport
							Data.treeForExport = {}
							treeForExportTarget = Data.treeForExport
						end
					end
					treeForExportTarget[jsonKey] = jsonValue
				end
			else
				-- the type of this child of the JSON object is not among the
				-- "content flags" that are defined for this JSON key
				Fault(string.format(
					"Type <code>%s</code> bad for %s",
					contentFlags, f(jsonKey, thisParameter)
				))
			end
		else
			-- the name of this child of the JSON object is not among the ``validKeys``
			Fault("Unknown component " .. f(jsonKey, thisParameter))
		end
	end
end -- processTempldataJson()



---Return an HTML string with the entire output:
---template description, TOC, parameters table, and format string.
---@return table div The ``<div>`` element that wraps the output
local function makeEntireHtmlOutput()
	local div = mw.html.create("div")

	-- template description
	if not Config.hideDescription then
		local templateDescription = makeTemplateOrParamDescription(Data.tree, true)
		if templateDescription then
			div:node(templateDescription)
		end
	end

	-- table of contents
	if Data.showToc then
		local toc = mw.html.create("div")
		if Config.classForToc then
			toc:addClass(Config.classForToc)
		end
		toc:css("margin-top", "0.5em")
			:wikitext("__TOC__")
		div:newline()
			:node(toc)
			:newline()
	end

	-- main table with template parameters
	local paramsTable = makeParamsTable()
	if paramsTable then
		if Data.showToc then
			local heading = mw.html.create("h2"):wikitext(getLocalizedText("doc-params"))
			div:node(heading)
				:newline()
		end
		div:node(paramsTable)
	end

	-- note about template transclusion format
	if Data.tree and Data.tree["format"] then
		local formatStyle = string.lower(Data.tree["format"])
		if formatStyle ~= "inline" and formatStyle ~= "block" then
			-- format is neither inline nor block, so it is a custom formatting string
			formatStyle = "custom"
		end

		-- display description about the formatting style
		local p = mw.html.create("p")
			:wikitext(getLocalizedText("doc-format-" .. formatStyle))

		if formatStyle == "custom" then
			-- append the custom formatting style, wrapped in <code>
			p:newline()
				:node(mw.html.create("code")
					:wikitext(Data.tree["format"])
				)
		end

		div:node(p)
	end
	return div
end -- makeEntireHtmlOutput()



---Process the TemplateData JSON code and build the HTML output from that.
---Append an invisible ``<templatedata>`` block, if necessary.
---When this function is done, ``Data.outputWrapper`` will hold the entire output.
local function processJsonAndMakeOutputFromIt()

	Data.outputWrapper = mw.html.create("div"):addClass("mw-templatedata-doc-wrap")
	if Data.hideEntireOutput then
		Data.outputWrapper:cssText("display:none;")
	end

	-- fill Data.tree/Data.treeForExport from Data.templdataJsonTurnedLua
	-- for the root TemplateData JSON object, i.e. things like
	-- template description and template transclusion format
	processTempldataJson()

	-- fill Data.tree/Data.treeForExport from Data.templdataJsonTurnedLua
	-- for each parameter, i.e. param name, description, etc.
	if Data.treeForExport then
		if type(Data.templdataJsonTurnedLua["params"]) == "table" then
			for paramname, _ in pairs(Data.templdataJsonTurnedLua["params"]) do
				processTempldataJson(paramname)
			end
			-- Data.heirs was filled in processTempldataJson()
			-- and contains the inheritances of all parameters
			if Data.heirs then
				applyInheritance()
			end
		end
	end

	-- build HTML and append it to output
	Data.outputWrapper:node(makeEntireHtmlOutput())

	-- build raw templatedata table and append it to output
	if not Data.dontMakeRawTempldataTable then

		Data.templdataJsonPlain = makeTempldataJsonFromData()

		if not TemplateData.frame then
			-- there's no frame to call the parser function from
			return
		end

		local templatedataTagWrapper = mw.html.create("div")
		Data.templdataWikitext = TemplateData.frame:callParserFunction{
			name = "#tag",
			args = { "templatedata", Data.templdataJsonPlain }
		}
		templatedataTagWrapper:wikitext(Data.templdataWikitext)

		if Config.alwaysDisplayRawTempldataTable then
			-- simply display raw templatedata table,
			-- ignore Data.showRawTempldataTable
			Data.outputWrapper:node(mw.html.create("hr"))
			Data.outputWrapper:node(templatedataTagWrapper)
		else
			-- wrap raw templatedata table in a collapsible element,
			-- respect Data.showRawTempldataTable
			templatedataTagWrapper:addClass("mw-collapsible-content")
			local wrapperCollapse = mw.html.create("div")
				:addClass("mw-collapsible")
				:addClass("mw-collapsed")
				:css("font-size", "85%")
				:wikitext("'''Test of raw TemplateData output''': ")
				:node(templatedataTagWrapper)
			if not Data.showRawTempldataTable then
				wrapperCollapse:css("display", "none")
			end
			Data.outputWrapper:node(wrapperCollapse)
		end
	end
end -- processJsonAndMakeOutputFromIt()



---Remove commented-out lines from ``Data.templdataJson``.
local function removeComments()
	string.gsub(Data.templdataJson,
		"([{,\"'])(%s*\n%s*//.*\n%s*)([},\"'])",
		"%1%3"
	)
end -- removeComments()



---Fill the global ``Config`` and ``Data`` tables based on the module and template
---arguments. Retrieve the TemplateData JSON code from the page.
---@param moduleArgs table The parameters passed when invoking this module
---@param templateArgs table The parameters passed when transcluding the template that invokes this module
---@return string output The entire output string
local function main(moduleArgs, templateArgs)

	-- set Config parameters with input from module parameters
	for k, v in pairs(ConfigAliases) do
		if moduleArgs[k] and moduleArgs[k] ~= "" then
			Config[v] = moduleArgs[k]
		end
	end
	Config.alwaysDisplayRawTempldataTable = checkBool(templateArgs.debug or moduleArgs.debug)
	Config.hideDescription = not templateArgs.description

	-- language for the process: from input, or wiki default language code (disregarding user language)
	Data.slang = templateArgs.lang or moduleArgs.lang or mw.language.getContentLanguage():getCode()

	Data.hideEntireOutput = checkBool(templateArgs.hide or moduleArgs.hide) and not Config.alwaysDisplayRawTempldataTable
	Data.dontMakeRawTempldataTable = checkBool(templateArgs.lazy or moduleArgs.lazy) and not Config.alwaysDisplayRawTempldataTable
	Data.showToc = checkBool(templateArgs.TOC)
	Data.showRawTempldataTable = checkBool(templateArgs.showRaw)

	-- get TemplateData JSON code

	local source -- the TemplateData JSON code

	-- for that, first check the template transclusion: $JSON and $1
	if templateArgs.JSON then
		source = templateArgs.JSON
	elseif templateArgs[1] then
		local templateArg1 = trim(templateArgs[1])
		local start = templateArg1:sub(1, 1)
		local charseq = mw.ustring.char(127, 39, 34, 96, 85, 78, 73, 81) -- <DEL> ' " ` U N I Q
		if start == "<" then
			Data.templdataWikitext = templateArg1
		elseif start == "{" then
			source = templateArg1
		elseif mw.ustring.sub(templateArg1, 1, 8) == charseq then
			Data.templdataWikitext = templateArg1
		end
	end

	-- if the template transclusion did not have any useable TemplateData,
	-- then check for TemplateData JSON code on the current page
	if not source then
		Data.pageTitleObject = mw.title.getCurrentTitle()
		source = findJsonInPage()
	end

	-- if the current page does not contain any TemplateData JSON code either,
	-- then check for TemplateData JSON code on the subpage defined in Config
	-- (most likely a documentation subpage)
	if not source then
		local currentPageNameUnprefixed = Data.pageTitleObject.text -- title of the current page without namespace
		local currentPageNamePrefixed = Data.pageTitleObject.prefixedText -- title of the current page with namespace

		if Config.patternForMatchingSubpage and Config.patternForCreatingSubpage and not string.match(currentPageNameUnprefixed, Config.patternForMatchingSubpage) then
			-- only if current page is not the subpage
			local subpageName = string.format(Config.patternForCreatingSubpage, currentPageNamePrefixed)
			-- change the pageTitleObject to the one of the subpage (since findJsonInPage uses the pageTitleObject)
			Data.pageTitleObject = mw.title.new(subpageName)

			if Data.pageTitleObject.exists then
				source = findJsonInPage()
			end
		end
	end

	--[[
	this part seems unwanted?
	if the TemplateData JSON code came from the doc subpage, then it would actually be wanted to make
	a functional <templatedata> tag for the template main page, so that it gets recognized by VisualEditor.
	anyway, if this part is active, particularly the second to last line, then the data used by the VE seems
	to be taken from the <templatedata> tags inside the {{templatedata}} call on the doc subpage – and no
	processing occurs whatsoever.

	-- if the TemplateData JSON code came from the subpage,
	-- then set Data.dontMakeRawTempldataTable to true
	if not Data.dontMakeRawTempldataTable and Config.patternForMatchingSubpage then
		if not Data.pageTitleObject then
			Data.pageTitleObject = mw.title.getCurrentTitle()
		end
		Data.dontMakeRawTempldataTable = string.match(Data.pageTitleObject.text, Config.patternForMatchingSubpage)
	end
	]]

	TemplateData.getPlainJSON(source) -- process JSON and make all output

	return outputhtmlToStringAndAppendErrors()

end -- main()



-----------------------------------------------------------------
-- TemplateData table functions



---Put the ``source`` TemplateData JSON code into the global ``Data.templdataJson``,
---turn it into Lua tables and process it (removing markup and making the output
---table (i.e. filling ``Data.outputWrapper`` with all HTML output)), then generate
---TemplateData JSON code from that again and return it
---@param source string The TemplateData JSON code to be processed
---@return string json The processed TemplateData JSON code
TemplateData.getPlainJSON = function(source)

	if type(source) ~= "string" then
		-- there's no usable TemplateData JSON code
		return
	end

	Data.templdataJson = source -- copy to global variable

	-- prepare the TemplateData JSON code
	removeComments()

	-- turn the TemplateData JSON code into a Lua table
	Data.templdataJsonTurnedLua = mw.text.jsonDecode(Data.templdataJson)

	if Data.templdataJsonTurnedLua then
		processJsonAndMakeOutputFromIt()

		if Data.oldSyntaxInAnyParameterType then
			Fault('the parameter types "string/line", "string/wiki-page-name", and "string/wiki-user-name" are deprecated')
		end
		if Data.templateDescriptionIsMissing then
			Fault(Config.textIfDescriptionIsMissing)
		end

	elseif not Data.templdataWikitext then
		Fault("fatal JSON error")
	end

	return Data.templdataJsonPlain
end -- TemplateData.getPlainJSON()



---Function for simulating a ``go`` call from another module.
TemplateData.test = function(moduleArgs, templateArgs)
	TemplateData.frame = mw.getCurrentFrame()
	return main(moduleArgs, templateArgs)
end -- TemplateData.test()



-----------------------------------------------------------------
-- main return object

return {

---Main entry function for invoking the module from a template.
---Wraps ``main()`` in an exception handler.
---@param frame table
---@return string
go = function(frame)
	TemplateData.frame = frame
	local successful, result = pcall(main, frame.args, frame:getParent().args)
	if not successful then
		Fault("INTERNAL: " .. result)
		result = getAllErrorMessages()
	end
	return result
end,

---Interface for usage by other modules.
---@return table
TemplateData = function()
	return TemplateData
end

}
Advertisement