Jump to content

Module:Track gauge/autodocument

विकिपीडिया से
en>DePiep (Code split: core code in {RailGauge}, in RG/autodocument documentation code only. Several code improvements and functions added. See Template_talk:RailGauge#Code_changes_28_May_2014.) के द्वारा 01:09, 28 मई 2014 के बदलाव
-- This module documents the rail track gauges as defined in [[module:RailGauge/data]].
local p = {}
local getArgs = require('Module:Arguments').getArgs
local dataPageName = 'Module:RailGauge/data' -- sandbox here
local dataPageExtra = 'Module:RailGauge/extra'
local gaugeDataAll = nil
local gaugeDataExtra = nil
local RailGauge = require('module:RailGauge') -- sandbox here
local html = require('Module:HtmlBuilder')
local tableTools = require('Module:tableTools')
-- global counters (to keep between calls)
local ttlSizeClassCount = {}
local ttlAliasCount = 0
local ttlEntries = 0
local ttlUnitCount = {}
local ttlNamedGaugeCount = 0
local ttlLinkCount = 0
local ttlContentCatsCount = 0
local ttlMentioningCatsCount = 0
local ttlMentioningPageCount = 0
local ttlListedRange = {}
--
local ttlDebugCatCount = 0 -- old num counter not stat (to be deprecated)
-----------------------------------------------------------------------------------
-- prepareArgs -- Arguments coming from an #invoke or from a module
-----------------------------------------------------------------------------------
local function prepareArgs(frame)
	local origArgs = getArgs(frame)
	-- Trim whitespace, make lower-case and remove blank arguments for all arguments
	-- searchAlias is the cleaned value of [1]. [1] is kept as rawInput for error message
	local args = {}
	args['searchAlias'] = ''
	args['rawInput'] = origArgs[1] or ''
	for k, v in pairs(origArgs) do
		if tonumber(k) == nil then
			-- Named argument
			if k == 'docsortlabel' then -- not in RG
				args[k] = v
			else
				args[k] = mw.ustring.lower(v)
			end
		else
			-- Unnamed argument, alias to be searched
			-- Into lowercase, remove all whitespace, commas to create the search key
			v = mw.ustring.lower(mw.ustring.gsub(v, '[%s%,]', ''))
			args[k] = v
			if k == 1 then
				args['searchAlias'] = v
			end
		end
	end
	return args
end
-----------------------------------------------------------------------------------
-- debugReturnArgs
-----------------------------------------------------------------------------------
function p.debugReturnArgs(frame)
	local args = prepareArgs(frame)
	local retArgs = {}
	for k, a in pairs(args) do
		table.insert(retArgs, k .. '=' .. a)
	end
	return 'Args: ' .. table.concat(retArgs, '; ')
end

-----------------------------------------------------------------------------------
-- formatUnitPlaintext
-- Pattern '00016.5 mm' for table.sort and catsort. Can be wrapped in class="sortkey"
-----------------------------------------------------------------------------------
local function formatUnitPlaintext(rgData, unit, fmtZeroPadding, toFracChar)
	-- Returns plaintext (ASCII) only. No css.
	if rgData == nil then
		return ''
	end
	if (unit or rgData.def) == 'imp' then
		-- imperial
		local ft = ''
		local inch = ''
		local frac = ''
		if rgData.ft then
			ft = rgData.ft .. ' ft'
		end
		if rgData.num then
			frac = ' ' .. rgData.num .. '/' .. rgData.den
			if toFracChar then
				-- as used in contentCat pagenames
				if frac == ' 1/8' then frac = '⅛'
				elseif frac == ' 1/4' then frac = '¼'
				elseif frac == ' 3/8' then frac = '⅜'
				elseif frac == ' 1/2' then frac = '½'
				elseif frac == ' 3/4' then frac = '¾'
				elseif frac == ' 7/8' then frac = '⅞'
				else
					frac = frac .. ' (error: fraction character missing in module:RailGauge)'
				end
			end
			if rgData['in'] then
				frac = ' ' .. rgData['in'] .. frac .. ' in'
			else
				frac = frac .. ' in'
			end
		else
			if rgData['in'] then
				inch = ' ' .. rgData['in'] .. ' in'
			end
		end
		return mw.text.trim(ft .. inch .. frac)
	else
		-- metric (mm)
		if fmtZeroPadding == nil or tonumber(fmtZeroPadding) <= 0 then
			return rgData.id .. ' mm'
		else
			return string.rep('0', fmtZeroPadding - string.len(math.floor(tonumber(rgData.id))))
				.. rgData.id .. ' mm'
		end
	end
end
-----------------------------------------------------------------------------------
-- documentSortKey
-----------------------------------------------------------------------------------
local function documentSortKey(rgEntry)
	local s = formatUnitPlaintext(rgEntry, 'met', 5)
	s = html.create().tag('span').addClass('sortkey').wikitext(s)
	return tostring(s)
end
-----------------------------------------------------------------------------------
-- catSortFromTitle
-- Currently finds "600 mm" when at end of title, then returns "0600 mm" (for catSort).
-- Blank when not found. Used for cat:mentions category page.
-----------------------------------------------------------------------------------
function p.catSortFromTitle()
	local title = mw.title.getCurrentTitle()
	local catSort = string.match(title.text, '%s(%d+%.?%d*)%smm$') or ''
	if catSort ~= '' then
		catSort = string.rep('0', 4 - string.len(math.floor(tonumber(catSort))))
			.. catSort .. ' mm'
	end
	if catSort == '' then
		return '*'
	else
		return catSort
	end
end
-----------------------------------------------------------------------------------
-- documentGaugeClass
-----------------------------------------------------------------------------------
local function documentGaugeClass(rgEntry)
	local size = tonumber(rgEntry.id or 0)
	local j
	if size > 1435 then
		j = 5
	elseif size == 1435 then
		j = 4
	elseif size > 500 then
		j = 3
	elseif size >= 100 then
		j = 2
	elseif size > 0 then
		j = 1
	else
		j = 6
	end
	ttlSizeClassCount[j][2] = ttlSizeClassCount[j][2] +1
	return '<span class="sortkey">' .. j .. '</span>' .. ttlSizeClassCount[j][1]
end
-----------------------------------------------------------------------------------
-- debugCountNumericAliases
-- quick & crude: list numeric aliases (in /data now down to 27).
-----------------------------------------------------------------------------------
function p.debugCountNumericAliases()
	gaugeDataAll = mw.loadData(dataPageName) --not for /eXTRA
	local ret = {}
	local cat = ''
	local catCount = 0
	for i, rgEntry in ipairs(gaugeDataAll) do
		for j, alias in ipairs(rgEntry.aliases) do
			if tonumber(alias) ~= nil then
				cat = 'Articles that mention rail gauge size' .. ' ' .. rgEntry.id .. ' mm'
				catCount = mw.site.stats.pagesInCategory(cat, pages)
				ttlDebugCatCount = ttlDebugCatCount  + catCount
				table.insert(ret,  rgEntry.id
					.. ' || ' .. catCount
					.. ' || ' .. alias
					.. ' || [&#x5b;:category:' .. cat .. '&#x5d;]'
					.. ' ||' )
			end
		end
	end
	local retFoot = {}
	table.insert(retFoot, #ret)
	table.insert(retFoot, ttlCatCount)
	table.sort(ret)
	return '\n\n|-\n\n| '
		.. table.concat(ret, '\n\n|-\n\n| ')
		.. '\n\n|}\n\n*'
		.. table.concat(retFoot, '\n*')
end

-----------------------------------------------------------------------------------
-- anchor -- Anchor text *here* is: <span id="1000 mm">; anchor *there* is: #1000 mm.
-----------------------------------------------------------------------------------
local function anchor(rgEntry, unit, herethere)
	if rgEntry == nil then
		return ''
	end
	unit = unit or rgEntry.def1
	local anch = formatUnitPlaintext(rgEntry,unit, 0)
	if herethere == 'there' then -- Untested, April 2014
		anch = '#' .. anch
	else
		anch = html.create().tag('span').attr('id', anch)
	end
	return tostring(anch)
end
-----------------------------------------------------------------------------------
-- noWrap -- Add span tags to prevent a string from wrapping.
-----------------------------------------------------------------------------------
local function noWrap(s)
	return mw.ustring.format('<span class="nowrap">%s</span>', s)
end
-----------------------------------------------------------------------------------
-- frac -- A slimmed-down version of the {{frac}} template (a nowrap to be added with the unit)
-----------------------------------------------------------------------------------
local function frac(whole, num, den)
	return mw.ustring.format(
		'<span class="frac">%s%s<sup>%s</sup>&frasl;<sub>%s</sub></span>',
		whole or '', whole and '<span class="visualhide">&nbsp;</span>' or '', num, den
		)
end
-----------------------------------------------------------------------------------
-- checkData -- Public. Performs various checks on the /data subpage.
-----------------------------------------------------------------------------------
function p.checkData(frame)
	local dataPage = frame and frame.args and frame.args[1] or dataPageName
	local data = mw.loadData(dataPage)
	local exists, dupes, dupeSort, ret = {}, {}, {}, {}
	-- Check for duplicate aliases.
	for ti, t in ipairs(data) do
		for ai, alias in ipairs(t.aliases or {}) do
			if not exists[alias] then
				exists[alias] = { ti, ai }
			else
				if not dupes[alias] then
					dupes[alias] = { exists[alias] }
				end
				table.insert(dupes[alias], { ti, ai })
			end
		end
	end
	for alias in pairs(dupes) do
		table.insert(dupeSort, alias)
	end
	table.sort(dupeSort)
	for i1, alias in ipairs(dupeSort) do
		local positions = {}
		for i2, aliasKeys in ipairs(dupes[alias]) do
			local position = mw.ustring.format('gauge %d, alias %d (gauge id: <code>%s</code>)', aliasKeys[1], aliasKeys[2], data[aliasKeys[1]].id or '')
			table.insert(positions, position)
		end
		local aliasText = mw.ustring.format('Duplicate aliases "%s" detected at the following positions: %s.', alias, mw.text.listToText(positions, '; '))
		table.insert(ret, aliasText)
	end
	-- Check for numerators without denominators.
	for ti, t in ipairs(data) do
		local num = t.num
		local den = t.den
		if num and not den then
			table.insert(ret, mw.ustring.format('Numerator "%s" with no denominator detected at gauge %d (id: <code>%s</code>).', num, ti, t.id or ''))
		elseif den and not num then
			table.insert(ret, mw.ustring.format('Denominator "%s" with no numerator detected at gauge %d (id: <code>%s</code>).', den, ti, t.id or ''))
		end
	end
	-- Check for gauges with no imperial or no metric measurements.
	for ti, t in ipairs(data) do
		if not (t.ft or t['in'] or t.num or t.den) then
			table.insert(ret, mw.ustring.format('No imperial measurements found for gauge %d (id: <code>%s</code>).', ti, t.id or ''))
		end
		if not (t.m or t.mm) then
			table.insert(ret, mw.ustring.format('No metric measurements found for gauge %d (id: <code>%s</code>).', ti, t.id or ''))
		end
	end
	-- Check for non-numeric measurements.
	local measurements = { 'ft', 'in', 'num', 'den', 'm', 'mm' }
	for ti, t in ipairs(data) do
		for mi, measurement in ipairs(measurements) do
			local measurementVal = t[measurement]
			if measurementVal and not tonumber(measurementVal) then
				table.insert(ret, mw.ustring.format('Non-numeric <code>%s</code> measurement ("%s") found for gauge %d (id: <code>%s</code>).', measurement, measurementVal, ti, t.id or ''))
			end
		end
	end
	-- Check for gauges with no id.
	for ti, t in ipairs(data) do
		if not t.id then
			local aliases = {}
			for i, alias in ipairs(t.aliases) do
				table.insert(aliases, mw.ustring.format('<code>%s</code>', alias))
			end
			aliases = mw.ustring.format(' (aliases: %s)', mw.text.listToText(aliases))
			table.insert(ret, mw.ustring.format('No id found for gauge %d%s.', ti, aliases or ''))
		end
	end
	-- Check for gauges with no aliases.
	for ti, t in ipairs(data) do
		if type(t.aliases) ~= 'table' then
			table.insert(ret, mw.ustring.format('No aliases found for gauge %d (id: <code>%s</code>).', ti, t.id or ''))
		else
			local isAlias = false
			for ai, alias in ipairs(t.aliases) do
				isAlias = true
				break
			end
			if not isAlias then
				table.insert(ret, mw.ustring.format('No aliases found for gauge %d (id: <code>%s</code>).', ti, t.id or ''))
			end
		end
	end
	-- Check for named gauges with no links and gauges with links but no names.
	-- 20140520: no link? could be acceptable. Code falls back to the unlinked name (in test now).
	if false then -- skipped 2014-05-25
	for ti, t in ipairs(data) do
		if t.name and not t.link then
				table.insert(ret, mw.ustring.format('No link found for the named gauge "%s" at position %d (id: <code>%s</code>).', t.name, ti, t.id or ''))
		elseif t.link and not t.name then
				table.insert(ret, mw.ustring.format('No name found for the gauge with link "%s" at position %d (id: <code>%s</code>).', t.link, ti, t.id or ''))
		end
	end
	end
	-- Check for invalid def1 values.
	for ti, t in ipairs(data) do
	local def = t.def1
		if def ~= 'imp' and def ~= 'met' then
			table.insert(ret, mw.ustring.format('Invalid def1 value "%s" found for gauge %d (id: <code>%s</code>).', def or '', ti, t.id or ''))
		end
	end
	-- Check for unwanted whitespace.
	for ti, t in ipairs(data) do
		for tkey, tval in pairs(t) do
			if tkey == 'aliases' and type(tval) == 'table' then
				for ai, alias in ipairs(tval) do
					if mw.ustring.find(alias, '%s') then
						table.insert(ret, mw.ustring.format('Unwanted whitespace detected in gauge %d alias %d ("%s", gauge id: <code>%s</code>).', ti, ai, alias, t.id or ''))
					end
				end
			elseif tkey == 'name' or tkey == 'link' or tkey == 'pagename' or tkey == 'contentcat' then
				if tval ~= mw.text.trim(tval) then
					table.insert(ret, mw.ustring.format('Unwanted whitespace detected in <code>%s</code> field of gauge %d ("%s", gauge id: <code>%s</code>).', tkey, ti, tval, t.id or ''))
				end
			elseif mw.ustring.find(tval, '%s') then
				table.insert(ret, mw.ustring.format('Unwanted whitespace detected in <code>%s</code> field of gauge %d ("%s", gauge id: <code>%s</code>).', tkey, ti, tval, t.id or ''))
			end
		end
	end
	-- Added April 2014: alias should not double with another id (imp and mm not ambiguous)
	local self_id = ''
	local self_def = ''
	for ti, t in ipairs(data) do
		self_id = t.id
		self_def = t.def1
		for iC, aliasCheck in ipairs(t.aliases) do
			if tonumber(aliasCheck) ~= nil then
				if self_id ~= aliasCheck then
					for iTwo, tTwo in ipairs(data) do
						if aliasCheck == tTwo.id then
							table.insert(ret,
								mw.ustring.format('Input alias %s (%s) from <code>id=%s mm</code> ambiguous with gauge id=<code>%s mm</code> (%s)'
								, aliasCheck, self_def, self_id, tTwo.id, tTwo.def1)
								)
						end
					end
				end
			end
		end
	end
	 -- Return any errors found.
	for i, msg in ipairs(ret) do
		ret[i] = mw.ustring.format('<span class="error">%s</span>', msg)
	end
	if #ret > 0 then
		return mw.ustring.format('Found the following errors in %s:\n* %s', dataPage, table.concat(ret, '\n* '))
	else
		return mw.ustring.format('No errors found in %s.', dataPage)
	end
end
-----------------------------------------------------------------------------------
-- catContent -- this one for public
-----------------------------------------------------------------------------------
function p.catContent(frame)
	-- catContent (content category for this alias)
	-- can be hardcoded in the data, or build by size (pattern)
	local args = prepareArgs(frame)
	local rgEntry = RailGauge.getRgEntry(args.searchAlias)
	if rgEntry == nil then
		return 'No entry for "' .. args[1] .. '".'
	end
	local catTitle
	local label
	local catC
	local docsortlabel = ''
	if args.docsortlabel ~= nil then
		docsortlabel = '|' .. args.docsortlabel
	end
	if rgEntry.contentcat == '' then
		catC = ''
	elseif rgEntry.contentcat ~= nil then
		catC = '[[:Category:' .. rgEntry.contentcat .. docsortlabel .. ']]'
	else -- no name given, try default name:
		local catCsuffix = ' gauge railways'
		if rgEntry.def1 == 'met' then
			label = formatUnitPlaintext(rgEntry, 'met')
			catTitle = mw.title.makeTitle(14, label .. catCsuffix)
			if catTitle.exists then
				catC = '[[:' .. catTitle.fullText .. docsortlabel .. ']]'
			end
		elseif rgEntry.def1 == 'imp' then
			label = formatUnitPlaintext(rgEntry, 'imp', nil, true)
			catTitle = mw.title.makeTitle(14, label .. catCsuffix)
			if catTitle.exists then
				catC = '[[:' .. catTitle.fullText .. docsortlabel .. ']]'
			end
		end
	end
	return catC
end
-----------------------------------------------------------------------------------
-- catMentions
-----------------------------------------------------------------------------------
function p.catMentions(frame)
	local args = prepareArgs(frame)
	local rgEntry = RailGauge.getRgEntry(args.searchAlias)
	if rgEntry == nil then
		return 'No gauge entry found for ' .. args[1] .. '.'
	end
	local catM = RailGauge.catMentions(rgEntry, args.docsortlabel, 'show')
	return catM
end
-----------------------------------------------------------------------------------
-- createCatMentions -- Create page, with preload
-----------------------------------------------------------------------------------
function p.createCatMentions(frame)
	local pagePreload = 'Template:RailGauge/preload-categorypage-railgauge-mentionings'
	local urlAction = 'edit'
	local args = prepareArgs(frame)
	local rgEntry = RailGauge.getRgEntry(args.searchAlias)
	if rgEntry == nil then
		return 'No gauge entry found for ' .. args[1] .. '.'
	end
	-- Create this cat
	local catM = RailGauge.catMentions(rgEntry, nil, 'fullpagename')
	local qryString = mw.uri.parseQueryString('action=' .. urlAction .. '&preload=' .. pagePreload)
	local urlCreate = tostring(mw.uri.fullUrl(catM, qryString))
	local ret = {}
	table.insert(ret, 'id=' .. rgEntry.id)
	table.insert(ret, '[[:' .. catM .. ']]')
	table.insert(ret, '[' .. urlCreate .. ' create]')
	table.insert(ret, '[[' .. pagePreload .. '|preload]]')
	table.insert(ret, 'Data source: [[' .. dataPageName .. '|/data]] or [[' .. dataPageName .. '|/extra]]')

	return table.concat(ret, '; ')
end
-----------------------------------------------------------------------------------
-- fromInputToId -- Used cleaned Alias as searchkey
-----------------------------------------------------------------------------------
local function fromInputToId(searchAlias)
	gaugeDataAll = mw.loadData(dataPageName)
	for i, rgEntry in ipairs(gaugeDataAll) do
		for j, alias in ipairs(rgEntry.aliases) do
			if alias == searchAlias then
				return rgEntry.id
			end
		end
	end
	-- Second try: in /Extra
	gaugeDatAExtra = mw.loadData(dataPageExtra)
	for i, rgEntry in ipairs(gaugeDataExtra) do
		for j, alias in ipairs(rgEntry.aliases) do
			if alias == searchAlias then
				return rgEntry.id
			end
		end
	end
	-- Third search: by id
	if tonumber(searchAlias) ~= nil then
		for i, rgEntry in ipairs(gaugeDataAll) do
			if rgEntry.id == searchAlias then
				return rgEntry.id
			end
		end
		for i, rgEntry in ipairs(gaugeDataExtra) do
			if rgEntry.id == searchAlias then
				return rgEntry.id
			end
		end
	end
end
-----------------------------------------------------------------------------------
-- documentInchCount -- Gives the number of inches in decimals.
-----------------------------------------------------------------------------------
local function documentInchCount(rgEntry)
	local inches = 0
	if rgEntry['num'] ~= nil then
		inches = tonumber((rgEntry['num'] or 0) / (rgEntry['den'] or 1))
	end
	inches = tostring(inches + (tonumber(rgEntry['ft'] or 0) * 12)
		+ tonumber(rgEntry['in'] or 0))
	return inches
end
-----------------------------------------------------------------------------------
-- documentInchToMm -- Not used lately
-----------------------------------------------------------------------------------
local function documentInchToMm(inchCount)
	return tonumber(inchCount or 0) * 25.4
end
-----------------------------------------------------------------------------------
-- documentGaugeSizeFromTitle -- Currently finds "1620 mm" when at end of title,
-- then returns "1620". Blank when not found.
-----------------------------------------------------------------------------------
function p.documentGaugeSizeFromTitle()
	local title = mw.title.getCurrentTitle()
	return string.match(title.text, '%s(%d+%.?%d*)%smm$') or ''
end
-----------------------------------------------------------------------------------
-- documentBuildRgList -- The table of id's to fill the table
-----------------------------------------------------------------------------------
function documentBuildRgList(args)
	-- Build series from the list. idFrom and idTo are numerical
	local rgList = {}
	local idFrom = -1
	local idTo = -1
	for i, v in ipairs(args) do
		if v == 'all' then
			idFrom = -math.huge
			idTo = math.huge
			break
		end
	end
	if args.docfrom ~= nil then
		idFrom = tonumber(fromInputToId(args.docfrom)
			or mw.ustring.gsub(args.docfrom, 'mm', ''))
		idTo = math.huge
	end
	if args.docto ~= nil then
		idTo = tonumber(fromInputToId(args.docto)
			or mw.ustring.gsub(args.docto, 'mm', ''))
	end
	if idTo > 0 then -- Some list is requested from the whole data set
		if idFrom > idTo then
			local dummy = idFrom
			idFrom = idTo
			idTo = dummy
		end
		for i, rgEntry in ipairs(gaugeDataAll) do
			if (tonumber(rgEntry.id) >= idFrom) and (tonumber(rgEntry.id) <= idTo) then
				table.insert(rgList, tonumber(rgEntry.id))
			end
		end
		for i, rgEntry in ipairs(gaugeDataExtra) do
			if (tonumber(rgEntry.id) >= idFrom) and (tonumber(rgEntry.id) <= idTo) then
				table.insert(rgList, tonumber(rgEntry.id))
			end
		end
		rgList = tableTools.removeDuplicates(rgList)
		table.sort(rgList)
		if #rgList > 1 then
			ttlListedRange[1] = rgList[1] .. ' mm &ndash; ' .. rgList[#rgList] .. ' mm '
		end
	end
	-- Individual entries can be mentioned in args (all unnamed = numbered params)
	-- Need a straight table.to keep sequence right
	local argsAliases = tableTools.compressSparseArray(args)
	for i, argsAlias in ipairs(argsAliases) do
		id = fromInputToId(argsAlias)
		if id ~= nil then
			-- Add to the top, use i
			table.insert(rgList, i, tonumber(id))
			table.insert(ttlListedRange, i, id .. ' mm; ')
		end
		rgList = tableTools.removeDuplicates(rgList)
	end
	return rgList
end
-----------------------------------------------------------------------------------
-- documentPostListStats -- build footer table, after list only
-----------------------------------------------------------------------------------
local function documentPostListStats(countRgList)
	-- Report data counters
	local retFoot = {}
	table.insert(retFoot, '')
	table.insert(retFoot, '*Data:')
	table.insert(retFoot, ':Sources: [[:' .. dataPageName .. ']], [[:' .. dataPageExtra .. ']]')
	table.insert(retFoot, ':Listed: ' .. table.concat(ttlListedRange, '') .. ' (' .. countRgList .. ' rows)')
	table.insert(retFoot, ':Aliases (input options): ' .. ttlAliasCount)
	table.insert(retFoot, ":'''Gauges''' (gauge by size): " .. countRgList)
	table.insert(retFoot, ":'''Entries''' (gauge by alias): " .. ttlEntries)
	for i, stat in ipairs (ttlUnitCount) do
		table.insert(retFoot, ':' .. stat[2] .. ': ' .. stat[1])
	end
	table.insert(retFoot, ':Entries with an article link: ' .. ttlLinkCount)
	table.insert(retFoot, ':Entries with a name: ' .. ttlNamedGaugeCount .. ' (not shown in table)')
	-- Categories (content, maintenance)
	table.insert(retFoot, '*Categories:')
	table.insert(retFoot, ':Content categories: ' .. ttlContentCatsCount)
	table.insert(retFoot, ':"Article mentions rail gauge" categories: ' .. ttlMentioningCatsCount)
	table.insert(retFoot, ':Articles listed in "mentions" categories: ' .. ttlMentioningPageCount .. ' (not unique)')
	table.insert(retFoot, ':Note: articles mentioning standard gauge are not categorised')
	-- Size classes (narrow, broad, ..)
	table.insert(retFoot, '*Size classes:')
	for i, stat in ipairs (ttlSizeClassCount) do
		if stat[2] ~= 0 then
			table.insert(retFoot, ':' .. stat[2] .. ' ' .. stat[3])
		end
	end
	return retFoot
end
-----------------------------------------------------------------------------------
-- documentHeader
-----------------------------------------------------------------------------------
local function documentHeader(numberOfEntries, docTitle, docState)
	if docTitle == '' then
		if (numberOfEntries or 0) <= 1 then
			docTitle = 'Rail gauge'
		else
			docTitle = 'Rail gauges'
		end
	end
	if docState == '' then
		docState = 'uncollapsed'
	end
	-- Row with sort buttons (hourglass triangles):
	local sortColHeaders = ''
	local sortClass = ''
	if (numberOfEntries or 0) > 1 then
		sortClass = 'sortable'
		sortColHeaders = '\n|- '
			.. '\n! style="line-height:90%;" | &nbsp; || || || || || || || ||'
	end
	local pagetitle = mw.title.getCurrentTitle()
	urlPurgePage = 'https://en.wikipedia.org/w/index.php?title=' .. pagetitle.nsText .. ':' .. pagetitle:partialUrl()  .. '&action=purge'
	urlPurgePage = '<span class="plainlinks purgelink" title="Purge this page (update countings)">[' .. urlPurgePage .. ' &#x03a3;&nbsp;P]</span>'
	local catMparent = RailGauge.catMentions(nil, 'Mentionings', 'show')

	-- 9 columns:
	local tableStyle = 'style="text-align:right; width:100%; font-size:95%;" '
	local retHdr = {}
		table.insert(retHdr, '\n{| class="wikitable collapsible ' .. docState .. ' ' .. sortClass .. '" ' .. tableStyle)
		table.insert(retHdr, '|+ style="background:#d8d8d8;" | ' .. docTitle)
		table.insert(retHdr, '|-')
		table.insert(retHdr, '! style="background:#d8d8d8; width:7em;" | Aliases<br>(input&nbsp;options)') --5
		table.insert(retHdr, '! style="background:#d8d8d8;" | Entry<br>units') --4
		table.insert(retHdr, '! style="background:#d8d8d8;" | Gauge<br>(mm)') --1
		table.insert(retHdr, '! style="background:#d8d8d8;" | Gauge<br>(ft,&nbsp;in)') --2
		table.insert(retHdr, '! style="background:#d8d8d8;" | Gauge<br>(inches)') --3
		table.insert(retHdr, '! style="background:#d8d8d8;" | Class<br>&nbsp;') --6
		table.insert(retHdr, '! style="background:#d8d8d8; min-width:7em;" | Link<br>&nbsp;') --7
		table.insert(retHdr, '! style="background:#d8d8d8;" | [[:Category:Track gauges by size|Category]]<br>(content)') --8
		table.insert(retHdr, '! style="background:#d8d8d8;" | ' .. urlPurgePage .. '&nbsp;' .. catMparent .. '<br>(maintenance)') --9
		
	return table.concat(retHdr, '\n') .. sortColHeaders
end
-----------------------------------------------------------------------------------
-- documentFooter --todo: experimental counters?
-----------------------------------------------------------------------------------
local function documentFooter()
	return {'\n|}'}
end
-----------------------------------------------------------------------------------
-- documentFromIdToEntrySet -- from fromIdToEntrySet
-- From one id, make the set with all one-two-three-more entries (met, inp, variants)
-----------------------------------------------------------------------------------
local function documentFromIdToEntrySet(id, searchedAlias)
	local rowSplit = '<div style="border-top:1px solid #ccc; height:1px;"/>'
	-- From the id, build the set of existing entries (met, imp, and variants)
	local entry = {}
	local defType = 0
	-- data
	for i, rgEntry in ipairs(gaugeDataAll) do
		if id == rgEntry.id then
			if rgEntry.def1 == 'met' and entry[1] == nil then
				entry[1] = rgEntry
				defType = defType + 1
			elseif rgEntry.def1 == 'imp' and entry[2] == nil then
				entry[2] = rgEntry
				defType = defType + 2
			else
				entry[3 + tableTools.size(entry)] = rgEntry
			end
		end
	end
	for i, rgEntry in ipairs(gaugeDataExtra) do
		if id == rgEntry.id then
			if rgEntry.def1 == 'met' and entry[1] == nil then
				entry[1] = rgEntry
				defType = defType + 1
			elseif rgEntry.def1 == 'imp' and entry[2] == nil then
				entry[2] = rgEntry
				defType = defType + 2
			else
				entry[3 + tableTools.size(entry)] = rgEntry
			end
		end
	end
	entry = tableTools.compressSparseArray(entry)
	-- Entry set is now complete & clean
	-- Result: the entry table with entries present in data, in sequence if present (1. met, 2. imp, any extra)
	
	--Build cell elements, then string row together.
	local inchCount = documentInchCount(entry[1])
	local sortKey = documentSortKey(entry[1], 'met', 5)
	local aliasList = {}
	for i, e in ipairs(entry) do
		local alis = {}
		for j, v in ipairs(e.aliases) do
			if tonumber(v) == nil then -- (plain numbers are not shown)
				table.insert(alis, tostring(v))
			end
		end
		for j, v in ipairs(alis) do
			if string.match(v, '^%d') == nil then -- textual so to italic.
				alis[j] = tostring(html.create().tag('span').wikitext(v).css('font-style', 'italic'))
			end
		end
		table.insert (aliasList, table.concat(alis, '; '))
		ttlAliasCount = ttlAliasCount + #alis
		-- Count link
		if e.name or '' ~= '' then
			ttlNamedGaugeCount = ttlNamedGaugeCount + 1
		end
	end
	local def = {} -- Definition unit code: 'met' or 'imp'
	local defText = {}
	for i, v in ipairs (entry) do
		table.insert(def, v.def1)
		if v.def1 == 'imp' then
			table.insert(defText, 'imperial')
			ttlUnitCount[1][1] = ttlUnitCount[1][1] + 1
		elseif v.def1 == 'met' then
			table.insert(defText, 'metric')
			ttlUnitCount[2][1] = ttlUnitCount[2][1] + 1
		end
	end
	if #entry >= 2 then
		if #entry == 2 and entry[1].def1 ~= entry[2].def1 then -- Regular pair: def in met and in imp
			ttlUnitCount[3][1] = ttlUnitCount[3][1] + 1
		else -- More than 2, or a double unit definition
			ttlUnitCount[4][1] = ttlUnitCount[4][1] .. ' ' .. id .. '&nbsp;mm (' .. #entry ..'&times; total)'
		end
	end
	-- mm; ft in -- Measurement (number & unit; met and imp; anchor to here)
	local measure = {}
	local unitanchor = { '', '' }
	measure[1] = RailGauge.formatMet(entry[1])
	measure[2] = RailGauge.formatImp(entry[1]) -- both met and imp from entry[1]
	if math.fmod(defType, 2) == 1 then
		measure[1] = tostring(html.create().tag('span').wikitext(measure[1]).css('font-weight', 'bold'))
		unitanchor[1] = anchor(entry[1], 'met')
	end
	if defType >= 2 then
		measure[2] = tostring(html.create().tag('span').wikitext(measure[2]).css('font-weight', 'bold'))
		unitanchor[2] = anchor(entry[1], 'imp')
	end
	-- Counter
	local rgSizeClass = documentGaugeClass(entry[1])
	-- Linked article
	local linkArticle = {}
	for i, e in ipairs(entry) do
		table.insert(linkArticle, e.pagename)
	end
	ttlLinkCount = ttlLinkCount + #linkArticle
	if #linkArticle == 2 then
		if linkArticle[1] == linkArticle[2] then
			linkArticle[2] = nil
		end
	end
	for i, lp in ipairs(linkArticle) do
		local fmtLp = ''
		fmtLp = '[[' .. lp .. ']]'
		linkArticle[i] = tostring(html.create().tag('span').css('text-align', 'left').wikitext(fmtLp))
	end
	-- catContent (content category for this alias). note: function p.catContent is a reduced code of this.
	-- can be hardcoded in the data, or build by size (pattern)
	local catContent = {}
	local catTitle
	local label
	local skipCheck = false
	for i, e in ipairs(entry) do
		if e.contentcat == '' then
			-- no cat
			skipCheck = true
		elseif e.contentcat ~= nil then
			label = string.match(e.contentcat, '([%S]*)') or 'nomatch'
			table.insert(catContent,
				'[[:Category:' .. e.contentcat .. '|cat:' .. label .. '&nbsp;...]]')
		end
	end
	if #catContent == 0 and not skipCheck then
		local catCsuffix = ' gauge railways'
		if math.fmod(defType, 2) == 1 then
			label = formatUnitPlaintext(entry[1], 'met')
			catTitle = mw.title.makeTitle(14, label .. catCsuffix)
			if catTitle.exists then
				table.insert(catContent,
					'[[:' .. catTitle.fullText .. '|cat:' .. noWrap(label) .. ']]')
			end
		end
		if defType >= 2 then
			label = formatUnitPlaintext(entry[1], 'imp', nil, true)
			catTitle = mw.title.makeTitle(14, label .. catCsuffix)
			if catTitle.exists then
				table.insert(catContent, '[[:' ..catTitle.fullText .. '|cat:' .. noWrap(label) .. ']]')
			end
		end
	end
	ttlContentCatsCount = ttlContentCatsCount + #catContent
	-- Mentions category
	local catMentions = RailGauge.catMentions(entry[1], "cat:ment's", 'show')
	local catCount = mw.site.stats.pagesInCategory(
		RailGauge.catMentions(entry[1], nil, 'pagename'), pages)
	ttlMentioningCatsCount = ttlMentioningCatsCount + 1 -- Exists
	ttlMentioningPageCount = ttlMentioningPageCount + catCount
	ttlEntries = ttlEntries + #entry
	sortCount  = mw.text.truncate('00000' .. tostring(catCount), -5, '')
	sortCount = '<span class="sortkey">' .. sortCount .. '</span>'
	catCount = sortCount .. catCount .. '&nbsp;P'

	-- Compose the id-row with all cell values
	local row = {}
		table.insert(row, table.concat(aliasList, rowSplit)) --5
		table.insert(row, table.concat(defText, rowSplit)) --4
		table.insert(row, sortKey .. unitanchor[1] .. measure[1]) --1 met
		table.insert(row, sortKey .. unitanchor[2] .. measure[2]) --2 imp
		table.insert(row, sortKey .. inchCount) --3
		table.insert(row, rgSizeClass) --6
		table.insert(row, table.concat(linkArticle, rowSplit)) --7
		table.insert(row, table.concat(catContent, rowSplit)) --8
		table.insert(row, catCount .. '&nbsp;' .. catMentions) --9
	return '\n|- style="background:#e8e8e8; border-top:2px solid #aaa;" |'
		.. '\n|'
		.. table.concat(row, ' || ')
end
-----------------------------------------------------------------------------------
-- documentGauge -- Selfdocument gauge data (one, multiple, range, all)
-----------------------------------------------------------------------------------
function p.documentGauge(frame)
	local args = prepareArgs(frame)
	gaugeDataAll = mw.loadData(dataPageName)
	gaugeDataExtra = mw.loadData(dataPageExtra)

		-- Init glolbal counters by table:
		ttlUnitCount =
			{
			[1] = {0, 'Entries defined metric'},
			[2] = {0, 'Entries defined imperial'},
			[3] = {0, 'Gauge sizes defined both metric and imperial'},
			[4] = {'', 'Gauge sizes with multiple definitions in one unit'}
			}
		ttlSizeClassCount =
			{
			[1] = {'scaled', 0, 'scaled or model gauges'},
			[2] = {'minimum', 0, 'minimum gauges'},
			[3] = {'narrow', 0, 'narrow gauges'},
			[4] = {'s.g.', 0, 'standard gauge'},
			[5] = {'broad', 0, 'broad gauges'},
			[6] = {'unknown', 0, 'unknown'}
			}

	local rgList = documentBuildRgList(args)
	-- Now loop through the prepared rgList[id] and add rows to result 	le
	-- One row contains all available entries for the id (met, imp, a third variant)
	local rowRGid = {}
	for i, numId in ipairs(rgList) do
		table.insert(rowRGid, documentFromIdToEntrySet(tostring(numId)))
	end
	-- Return args
	local retArgs = ''
	if args.docReturnArgs == 'on' then
		retArgs = '\n' .. p.debugReturnArgs(frame)
	end
	-- Build statistics footer
	local retStats = {}
	if args.docstats == 'on' then
		retStats = documentPostListStats(#rgList)
	end
	-- Build up
	return documentHeader(#rgList, args.doctitle or '', args.docstate or '')
		.. table.concat(rowRGid, '')
		.. table.concat(documentFooter(), '')
		.. table.concat(retStats, '\n')
		.. retArgs
end

return p