Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

// request.cf · coarse context

A page that knows where it met you.

Only coarse request metadata is shown. This demo does not display or persist visitor IP addresses.

Country
US
Cloudflare location
CMH
Connection
HTTP/2
Language
Not provided

Ray ID: a3ff156eda9a3564

Jump to content

Module:MilLeadership

Permanently protected module
From Wikipedia, the free encyclopedia

require('strict')
-- Leadership table renderer. Incumbent lists stay on the article;
-- award codes are resolved through Module:MilAward (PostNom / Stack).
--
-- Field separator inside a post list is " // " (space-slash-slash-space).
-- Raw "|" cannot be used as a field separator in a template parameter
-- (MediaWiki would split the parent template). Use {{MilLdr}} instead.
--
-- Multiple named footnotes on one row: use named-note-1 / named-note-2
-- (aliases NamedNote-N, note-name-N) with optional note-1 / note-2 texts.
-- Do not repeat the same parameter name (MediaWiki keeps only the last).

local p = {}
local getArgs = require('Module:Arguments').getArgs

local RESERVED = {
	caption = true,
	float = true,
	color = true,
	colour = true,
	country = true,
	ribbons = true,
	ribbon_size = true,
	['ribbon-size'] = true,
	ribbonsize = true,
	size = true,
	nocat = true,
	class = true,
	content = true,
	posts = true,
}

local KNOWN_LINE_KEYS = {
	from = true,
	['from-date'] = true,
	to = true,
	['to-date'] = true,
	rank = true,
	name = true,
	incumbent = true,
	['post-incumbent'] = true,
	awards = true,
	note = true,
	notes = true,
	['note-name'] = true,
	notename = true,
	['named-note'] = true,
	ref = true,
	link = true,
	vacant = true,
}

local DEFAULT_COLOR = '#CF9C65'
local DEFAULT_RIBBON_SIZE = 'x8px'

local function trim(s)
	if s == nil then
		return ''
	end
	return (mw.text.trim(tostring(s)))
end

local function oneLine(s)
	s = trim(s)
	s = string.gsub(s, '[\r\n]+', ' ')
	s = string.gsub(s, '%s+', ' ')
	return s
end

local function isYes(v)
	if v == nil then
		return false
	end
	local s = mw.ustring.lower(trim(v))
	return s == 'yes' or s == 'y' or s == 'true' or s == '1' or s == 'on'
end

local function splitPlain(s, delim)
	local out = {}
	local start = 1
	local dlen = #delim
	while true do
		local i = string.find(s, delim, start, true)
		if not i then
			table.insert(out, string.sub(s, start))
			break
		end
		table.insert(out, string.sub(s, start, i - 1))
		start = i + dlen
	end
	return out
end

local function splitFields(line)
	-- Exact " // " keeps an empty rank as its own field. A greedy
	-- %s+//%s* match would swallow " //  // " and leave "// name".
	-- Require the spaces so http:// in URLs is not split.
	if string.find(line, ' // ', 1, true) then
		return splitPlain(line, ' // ')
	end
	if string.find(line, ' / ', 1, true) then
		return splitPlain(line, ' / ')
	end
	if string.find(line, ' | ', 1, true) then
		return splitPlain(line, ' | ')
	end
	return { line }
end

local function splitRange(s)
	s = trim(s)
	local a, b = string.match(s, '^(.-)%s*[–—]%s*(.+)$')
	if a and trim(a) ~= '' and trim(b) ~= '' then
		return trim(a), trim(b)
	end
	local y1, y2 = string.match(s, '^(%d%d%d%d)%s*%-%s*(%d%d%d%d)$')
	if y1 then
		return y1, y2
	end
	return nil
end

local function parseAwards(raw)
	local codes = {}
	if raw == nil or trim(raw) == '' then
		return codes
	end
	raw = string.gsub(raw, '|', ',')
	for token in string.gmatch(raw, '[^,%s]+') do
		table.insert(codes, token)
	end
	return codes
end


local function isNumberedNoteKey(key)
	key = string.lower(key or '')
	return string.match(key, '^named%-note%-%d+$')
		or string.match(key, '^namednote%-%d+$')
		or string.match(key, '^note%-name%-%d+$')
		or string.match(key, '^notename%-%d+$')
		or string.match(key, '^note%-%d+$')
end

-- Collect NamedNote-1 / named-note-2 / Note-1 pairs (and legacy single named-note + note).
local function collectNamedNotes(kv)
	local byIndex = {}
	local maxIndex = 0

	local function remember(index, name, text)
		index = tonumber(index)
		if not index then
			return
		end
		local slot = byIndex[index] or {}
		if name and trim(name) ~= '' then
			slot.name = trim(name)
		end
		if text ~= nil and trim(text) ~= '' then
			slot.text = trim(text)
		end
		byIndex[index] = slot
		if index > maxIndex then
			maxIndex = index
		end
	end

	for key, val in pairs(kv) do
		local k = string.lower(key)
		local n = string.match(k, '^named%-note%-(%d+)$')
			or string.match(k, '^namednote%-(%d+)$')
			or string.match(k, '^note%-name%-(%d+)$')
			or string.match(k, '^notename%-(%d+)$')
		if n then
			remember(n, val, nil)
		else
			n = string.match(k, '^note%-(%d+)$')
			if n then
				remember(n, nil, val)
			end
		end
	end

	-- Legacy single params map to index 1 when that slot has no name yet.
	local legacyName = kv['note-name'] or kv.notename or kv['named-note']
	if legacyName and trim(legacyName) ~= '' then
		if not byIndex[1] or not byIndex[1].name then
			remember(1, legacyName, nil)
		end
	end
	if kv.note and trim(kv.note) ~= '' then
		-- Prefer attaching bare note= to the highest named slot that still lacks text;
		-- otherwise to index 1 (legacy behaviour).
		local attached = false
		for i = maxIndex, 1, -1 do
			local slot = byIndex[i]
			if slot and slot.name and (not slot.text or slot.text == '') then
				slot.text = trim(kv.note)
				attached = true
				break
			end
		end
		if not attached then
			remember(1, nil, kv.note)
		end
	end

	local list = {}
	for i = 1, math.max(maxIndex, 1) do
		local slot = byIndex[i]
		if slot and ((slot.name and slot.name ~= '') or (slot.text and slot.text ~= '')) then
			table.insert(list, { name = slot.name, text = slot.text })
		end
	end
	return list
end

local function parseNotesList(raw)
	local notes = {}
	if raw == nil or trim(raw) == '' then
		return notes
	end
	for _, item in ipairs(splitPlain(raw, ' ;; ')) do
		item = trim(item)
		if item ~= '' then
			table.insert(notes, item)
		end
	end
	if #notes == 0 then
		for _, item in ipairs(splitPlain(raw, '; ')) do
			item = trim(item)
			if item ~= '' then
				table.insert(notes, item)
			end
		end
	end
	return notes
end

-- Exported for testcases / console.
function p._parseLine(line)
	line = trim(line)
	line = string.gsub(line, '^[*#]+%s*', '')
	-- Empty from serialises as "*  // to // rank // name". After stripping
	-- the list marker and spaces the line starts with "//", which collapses
	-- the empty first field and shifts rank into the To column. Prepend a
	-- space so splitFields keeps that empty from slot.
	if string.sub(line, 1, 2) == '//' then
		line = ' ' .. line
	end
	if line == '' then
		return nil
	end

	local fields = splitFields(line)
	local positional = {}
	local kv = {}
	local unknown = {}

	for _, field in ipairs(fields) do
		field = trim(field)
		local k, v = string.match(field, '^([%a][%w_%-]*)%s*=%s*(.*)$')
		if k then
			local key = string.lower(k)
			kv[key] = v
			if not KNOWN_LINE_KEYS[key] and not isNumberedNoteKey(key) then
				unknown[key] = v
			end
		else
			-- Keep empty positional fields (vacant rank/name).
			table.insert(positional, field)
		end
	end

	local fromDate = kv.from or kv['from-date']
	local toDate = kv.to or kv['to-date']
	local rank = kv.rank
	local name = kv.name or kv.incumbent or kv['post-incumbent']

	if fromDate == nil then
		if #positional >= 4 then
			fromDate, toDate, rank, name = positional[1], positional[2], positional[3], positional[4]
		elseif #positional == 3 then
			local a, b = splitRange(positional[1])
			if a then
				fromDate, toDate, rank, name = a, b, positional[2], positional[3]
			else
				fromDate, toDate, rank = positional[1], positional[2], positional[3]
				name = name or ''
			end
		elseif #positional == 2 then
			local a, b = splitRange(positional[1])
			if a then
				fromDate, toDate, name = a, b, positional[2]
			else
				fromDate, toDate = positional[1], positional[2]
			end
		elseif #positional == 1 then
			name = positional[1]
		end
	else
		if rank == nil and positional[1] then
			rank = positional[1]
		end
		if name == nil and positional[2] then
			name = positional[2]
		elseif name == nil and positional[1] and rank ~= nil then
			name = positional[1]
		end
	end

	fromDate = trim(fromDate)
	toDate = trim(toDate)
	rank = trim(rank)
	name = trim(name)
	fromDate = string.gsub(fromDate, '^//%s*', '')
	toDate = string.gsub(toDate, '^//%s*', '')
	rank = string.gsub(rank, '^//%s*', '')
	name = string.gsub(name, '^//%s*', '')

	local namednotes = collectNamedNotes(kv)
	-- Keep legacy single fields for older call sites / debugging.
	local first = namednotes[1] or {}
	return {
		from = trim(fromDate),
		to = trim(toDate),
		rank = trim(rank),
		name = name,
		awards = parseAwards(kv.awards),
		note = first.text or kv.note,
		notes = parseNotesList(kv.notes),
		notename = first.name or kv['note-name'] or kv.notename or kv['named-note'],
		namednotes = namednotes,
		ref = kv.ref,
		link = kv.link,
		vacant = kv.vacant,
		unknown = unknown,
	}
end

local function parseMemberLines(raw)
	local members = {}
	if raw == nil then
		return members
	end
	-- Fold wrapped note text back onto the previous item. A newline inside
	-- note= must not become a new table row.
	local folded = {}
	for line in string.gmatch(tostring(raw) .. '\n', '([^\r\n]*)\r?\n') do
		local t = trim(line)
		if t == '' then
			-- skip
		elseif string.match(t, '^[*#]') or string.find(t, ' // ', 1, true) then
			table.insert(folded, t)
		elseif #folded > 0 then
			folded[#folded] = folded[#folded] .. ' ' .. t
		else
			table.insert(folded, t)
		end
	end
	for _, line in ipairs(folded) do
		local row = p._parseLine(line)
		if row then
			table.insert(members, row)
		end
	end
	return members
end

local function parseContent(raw)
	local posts = {}
	if raw == nil or trim(raw) == '' then
		return posts
	end
	local currentName = nil
	local buf = {}
	local function flush()
		if currentName then
			table.insert(posts, {
				name = currentName,
				members = parseMemberLines(table.concat(buf, '\n')),
			})
		end
		buf = {}
	end
	for line in string.gmatch(tostring(raw) .. '\n', '([^\r\n]*)\r?\n') do
		local heading = string.match(line, '^%s*==+%s*(.-)%s*==+%s*$')
		if heading then
			flush()
			currentName = trim(heading)
		else
			table.insert(buf, line)
		end
	end
	flush()
	return posts
end

local function collectNumberedPosts(args)
	local posts = {}
	local i = 1
	while true do
		local title = args['title' .. i] or args['post' .. i]
		local list = args['list' .. i] or args['members' .. i]
		if title == nil and list == nil then
			break
		end
		table.insert(posts, {
			name = trim(title),
			members = parseMemberLines(list),
		})
		i = i + 1
	end
	return posts
end

local function collectNamedPosts(src)
	local posts = {}
	if src == nil then
		return posts
	end
	-- Scribunto frame.args pairs() yields parameters in the order they were
	-- written. Do not copy through a new table first or Lua 5.1 will scramble
	-- Commanding Officers / RSM / Chaplains.
	for key, value in pairs(src) do
		if type(key) == 'string' then
			local lk = string.lower(key)
			if not RESERVED[lk] and not string.match(lk, '^title%d+$')
				and not string.match(lk, '^post%d+$')
				and not string.match(lk, '^list%d+$')
				and not string.match(lk, '^members%d+$') then
				table.insert(posts, {
					name = key,
					members = parseMemberLines(value),
				})
			end
		end
	end
	return posts
end

local function collectPosts(args, src)
	local numbered = collectNumberedPosts(args)
	if #numbered > 0 then
		return numbered
	end
	local content = args.content or args.posts
	if content then
		return parseContent(content)
	end
	return collectNamedPosts(src or args)
end

local function expand(frame, title, targs)
	local ok, result = pcall(function()
		return frame:expandTemplate{ title = title, args = targs }
	end)
	if ok then
		return result
	end
	return '<span style="color:#d33"> (MilLeadership: failed to expand ' .. title .. ')</span>'
end

local function formatAwards(frame, codes)
	if #codes == 0 then
		return ''
	end
	local targs = { size = '85%', sep = ', ' }
	for i, code in ipairs(codes) do
		targs[i] = code
	end
	return expand(frame, 'MilAward PostNom', targs)
end

local function formatRibbons(frame, codes, size)
	if #codes == 0 then
		return ''
	end
	local targs = { size = size or DEFAULT_RIBBON_SIZE }
	for i, code in ipairs(codes) do
		targs[i] = code
	end
	return expand(frame, 'MilAward Stack', targs)
end

local function formatEfn(frame, text, name)
	local targs = {}
	if text and trim(text) ~= '' then
		targs[1] = text
	end
	if name and trim(name) ~= '' then
		targs.name = name
	end
	if targs[1] == nil and targs.name == nil then
		return ''
	end
	return expand(frame, 'Efn', targs)
end

local function formatName(row)
	local name = row.name
	if isYes(row.vacant) and (name == '' or string.lower(name) == 'post vacant') then
		return "''No permanent appointment''"
	end
	if name == '' and isYes(row.vacant) then
		return "''No permanent appointment''"
	end
	local link = row.link
	if link and trim(link) ~= '' and not string.find(name, '%[%[', 1) then
		if isYes(link) then
			return '[[' .. name .. ']]'
		end
		return '[[' .. trim(link) .. '|' .. name .. ']]'
	end
	return name
end

local function formatIncumbent(frame, row, definedEfn)
	local parts = {}
	local rank = row.rank
	if rank ~= '' then
		table.insert(parts, rank)
	end
	local name = formatName(row)
	if name ~= '' then
		table.insert(parts, name)
	elseif #parts == 0 then
		table.insert(parts, "''Post vacant''")
	end

	local awards = formatAwards(frame, row.awards)
	if awards ~= '' then
		table.insert(parts, awards)
	end

	local namednotes = row.namednotes
	if namednotes == nil then
		namednotes = {}
		if row.notename or row.note then
			table.insert(namednotes, { name = row.notename, text = row.note })
		end
	end
	for _, nn in ipairs(namednotes) do
		local nname = nn.name
		local ntext = nn.text
		if nname and ntext then
			table.insert(parts, formatEfn(frame, ntext, nname))
			definedEfn[nname] = true
		elseif nname then
			table.insert(parts, formatEfn(frame, nil, nname))
		elseif ntext then
			table.insert(parts, formatEfn(frame, ntext, nil))
		end
	end

	for _, item in ipairs(row.notes or {}) do
		local named, rest = string.match(item, '^([%w_%-]+)%s*:%s*(.+)$')
		if named then
			table.insert(parts, formatEfn(frame, rest, named))
			definedEfn[named] = true
		elseif string.match(item, '^[%w_%-]+$') then
			-- Bare token: named efn reference (definition comes from note-name + note).
			table.insert(parts, formatEfn(frame, nil, item))
			definedEfn[item] = true
		else
			table.insert(parts, formatEfn(frame, item, nil))
		end
	end

	if row.ref and trim(row.ref) ~= '' then
		if string.find(row.ref, '<ref', 1, true) then
			table.insert(parts, row.ref)
		else
			table.insert(parts, '<ref>' .. row.ref .. '</ref>')
		end
	end

	return table.concat(parts, ' ')
end

local function tracking(nocat, errors)
	if nocat then
		return ''
	end
	local ns = mw.title.getCurrentTitle().namespace
	local out = {}
	if ns == 0 then
		table.insert(out, '[[Category:Pages using MilLeadership]]')
	end
	if errors > 0 then
		table.insert(out, '[[Category:MilLeadership errors]]')
	end
	return table.concat(out)
end

function p._table(args, frame, src)
	frame = frame or mw.getCurrentFrame()
	local caption = args.caption or ''
	local float = trim(args.float)
	local color = args.color or args.colour or DEFAULT_COLOR
	local showRibbons = isYes(args.ribbons)
	local ribbonSize = args.ribbon_size or args['ribbon-size'] or args.ribbonsize or args.size or DEFAULT_RIBBON_SIZE
	local nocat = isYes(args.nocat)
	local extraClass = args.class or ''

	local posts = collectPosts(args, src)
	local errors = 0
	local definedEfn = {}

	local root = mw.html.create('table')
	root:addClass('wikitable')
	if extraClass ~= '' then
		root:addClass(extraClass)
	end
	if float == 'right' or float == 'floatright' then
		root:addClass('floatright')
	elseif float == 'left' or float == 'floatleft' then
		root:addClass('floatleft')
	elseif float == 'center' or float == 'centre' then
		root:addClass('center')
	end
	if float == 'center' or float == 'centre' then
		root:css('margin', '0.5em auto')
	else
		root:css('margin', '0.5em 0')
	end
	root:css('font-size', '95%')
	root:css('border-bottom', '5px solid ' .. color)
	root:css('border-top', '5px solid ' .. color)

	if caption ~= '' then
		root:tag('caption'):wikitext(caption)
	end

	local fromWidth = showRibbons and '15%' or '20%'
	local nameWidth = showRibbons and '50%' or '60%'
	local toWidth = showRibbons and '15%' or '20%'
	local ribbonWidth = '20%'

	if #posts == 0 then
		errors = errors + 1
		root:tag('tr'):tag('td'):attr('colspan', showRibbons and '4' or '3')
			:wikitext('<span style="color:#d33">MilLeadership: no posts listed</span>')
	end

	for _, post in ipairs(posts) do
		local header = root:tag('tr')
		header:css('border-top', '5px solid ' .. color)
		local function th(width, text)
			header:tag('th')
				:css('width', width)
				:css('text-align', 'center')
				:css('background', 'lightgrey')
				:wikitext(text)
		end
		th(fromWidth, 'From')
		th(nameWidth, post.name ~= '' and post.name or 'No post name')
		th(toWidth, 'To')
		if showRibbons then
			th(ribbonWidth, 'Ribbon')
		end

		if #post.members == 0 then
			errors = errors + 1
			local empty = root:tag('tr'):css('text-align', 'center')
			empty:tag('td'):attr('colspan', showRibbons and '4' or '3')
				:wikitext('<span style="color:#d33">MilLeadership: no members for ' .. post.name .. '</span>')
		end

		for _, row in ipairs(post.members) do
			for uk, _ in pairs(row.unknown) do
				errors = errors + 1
				row.name = (row.name ~= '' and row.name or '') ..
					' <span style="color:#d33">(unknown field: ' .. uk .. ')</span>'
			end
			local tr = root:tag('tr'):css('text-align', 'center')
			tr:tag('td'):wikitext(row.from)
			tr:tag('td'):wikitext(formatIncumbent(frame, row, definedEfn))
			tr:tag('td'):wikitext(row.to)
			if showRibbons then
				tr:tag('td'):wikitext(formatRibbons(frame, row.awards, ribbonSize))
			end
		end
	end

	return tostring(root) .. tracking(nocat, errors)
end

function p.table(frame)
	local wrappers = {
		'Template:MilLeadership',
		'Template:MilLeadership/sandbox',
		'User:BoonDock/MilLeadership',
	}
	local args = getArgs(frame, {
		wrappers = wrappers,
		removeBlanks = false,
	})
	local src = frame.args
	local parent = frame.getParent and frame:getParent()
	if parent and parent.getTitle then
		local title = parent:getTitle():gsub('/sandbox$', '')
		for _, wrapper in ipairs(wrappers) do
			if title == wrapper then
				src = parent.args
				break
			end
		end
	end
	return p._table(args, frame, src)
end

p.main = p.table

function p.line(frame)
	-- Serialise one row. Used by {{MilLdr}} / {{User:BoonDock/MilLdr}} so
	-- editors can use pipes without MediaWiki splitting the parent template.
	local args = getArgs(frame, {
		wrappers = {
			'Template:MilLdr',
			'Template:MilLdr/sandbox',
			'User:BoonDock/MilLdr',
		},
		removeBlanks = false,
	})
	local function val(name, n)
		local v = args[name]
		if v == nil or trim(v) == '' then
			v = args[n]
		end
		return oneLine(v)
	end
	local line = '* ' .. val('from', 1) .. ' // ' .. val('to', 2) .. ' // ' .. val('rank', 3) .. ' // ' .. val('name', 4)
	local function append(key, aliases)
		local v = args[key]
		if (v == nil or trim(v) == '') and aliases then
			for _, alt in ipairs(aliases) do
				v = args[alt]
				if v and trim(v) ~= '' then
					break
				end
			end
		end
		if v and trim(v) ~= '' then
			line = line .. ' // ' .. key .. '=' .. oneLine(v)
		end
	end
	append('awards')
	-- Numbered named notes: named-note-1 / NamedNote-2 / note-1 / Note-2 ...
	local noteIndexes = {}
	for key, _ in pairs(args) do
		local k = string.lower(tostring(key))
		local n = string.match(k, '^named%-note%-(%d+)$')
			or string.match(k, '^namednote%-(%d+)$')
			or string.match(k, '^note%-name%-(%d+)$')
			or string.match(k, '^notename%-(%d+)$')
			or string.match(k, '^note%-(%d+)$')
		if n then
			noteIndexes[tonumber(n)] = true
		end
	end
	local sorted = {}
	for n, _ in pairs(noteIndexes) do
		table.insert(sorted, n)
	end
	table.sort(sorted)
	for _, n in ipairs(sorted) do
		local name = args['named-note-' .. n] or args['NamedNote-' .. n] or args['namednote-' .. n]
			or args['note-name-' .. n] or args['notename-' .. n]
		local text = args['note-' .. n] or args['Note-' .. n]
		if name and trim(name) ~= '' then
			line = line .. ' // named-note-' .. n .. '=' .. oneLine(name)
		end
		if text and trim(text) ~= '' then
			line = line .. ' // note-' .. n .. '=' .. oneLine(text)
		end
	end
	-- Legacy single note-name / named-note / note (only if no numbered notes present)
	if #sorted == 0 then
		append('note')
		append('note-name', { 'notename', 'named-note', 'NamedNote' })
	end
	append('notes')
	append('ref')
	append('link')
	append('vacant')
	return line
end

return p