Module:MilLeadership
| This module is rated as beta. It is considered ready for widespread use, but as it is still relatively new, it should be applied with some caution to ensure results are as expected. |
| Editing of this module by new or unregistered users is currently disabled. See the protection policy and protection log for more details. If you cannot edit this module and you wish to make a change, you can submit an edit request, discuss changes on the talk page, request unprotection, log in, or create an account. |
This module builds a unit leadership table (commanding officers, RSMs, chaplains, and similar posts) from a list of people and from/to dates. Award codes are looked up in Module:MilAward (post-nominals and optional ribbon stacks). Dates are display strings; they are not passed through {{date}}.
Do not invoke this module from articles except through the templates below (or their /sandbox pages).
Templates that call this module
| Template | Invoke | What it does |
|---|---|---|
| {{MilLeadership}} | {{#invoke:MilLeadership|table}} |
Renders the whole wikitable: caption, post headings, rows, optional ribbon column. |
| {{MilLeadership/sandbox}} | same | Sandbox copy of the table wrapper. |
| {{MilLdr}} | {{#invoke:MilLeadership|line}} |
Serialises one person to an internal list line (from // to // rank // name // key=value). Does not draw a table by itself.
|
| {{MilLdr/sandbox}} | same | Sandbox copy of the row helper. |
User:BoonDock/MilLeadership and User:BoonDock/MilLdr redirect to the live templates. Module:Sandbox/BoonDock/MilLeadership forwards with require('Module:MilLeadership').
Why two templates
A raw pipe (|) inside {{MilLeadership}} is a new template parameter, so this cannot work:
* 1978 | 1978 | Cmdt | [[Frank Bestbier]]
{{MilLdr}} keeps those pipes on the row template. It calls p.line, which returns a line using the delimiter // (space-slash-slash-space). p.table then parses those lines. http:// in URLs is not split, because that form has no spaces around //.
How it fits together
Article
{{MilLeadership | caption=... | ribbons=yes | content= == Post == {{MilLdr|...}} }}
│
▼
Module:MilLeadership.table → HTML wikitable
│
├── {{MilAward PostNom}} for awards=SD, SM, MMM
└── {{MilAward Stack}} ribbon column when |ribbons=yes (default size x8px)
Incumbent lists stay on the article. This module does not store COs in a data module. Medal data lives in Module:MilAward/data.
Entry points
| Function | Called from | Role |
|---|---|---|
p.table / p.main |
{{MilLeadership}} | Read template arguments and render the table. |
p.line |
{{MilLdr}} | Turn from/to/rank/name (and named extras) into one list line. |
p._table |
other Lua | Same renderer, from a Lua args table (optional src for argument order).
|
p._parseLine |
console / tests | Parse one member line into a row table. |
{{MilLeadership}} arguments
Passed through to p.table:
| Parameter | Description |
|---|---|
|caption= |
Table caption |
|float= |
right or left. Default: no float. Empty {{{1}}} is the same as omitting it.
|
|color= / |colour= |
Header and border colour. Default #CF9C65
|
|country= |
Reserved (ZAR). Award lookup uses MilAward codes, which are global. |
|ribbons= |
yes to add a right-hand Ribbon column. Default off.
|
|ribbon_size= |
File height for that column. Default x8px. Aliases: |ribbon-size=, |ribbonsize=, |size=
|
|class= |
Extra CSS class on the table |
|nocat= |
yes suppresses tracking categories
|
|content= / |posts= |
Member lists grouped by == Post name == headings. Preferred when there is more than one post, so order is preserved.
|
|title1= + |list1= |
Numbered posts (|title2=/|list2=, …). Aliases: |post1=, |members1=
|
| any other named parameter | Treated as a post heading; the value is that post’s member list. Do not rely on this for several posts: Lua 5.1 may not keep parameter order. |
Example
| From | Commanding Officers | To |
|---|---|---|
| 1978 | Cmdt Frank Bestbier [a] | 1978 |
| Jan 1981 | Cmdt Roland de Vries SD,SM,MMM [b] | 1982 |
| From | Chaplains | To |
| 1983 | No permanent Appointment | 1983 |
{{MilLeadership
| caption = Example
| content =
== Commanding Officers ==
{{MilLdr|1978|1978|Cmdt|[[Frank Bestbier]]|note=Veggroup Juliet}}
{{MilLdr|Jan 1981|1982|Cmdt|[[Roland de Vries]]|awards=SD, SM, MMM|note=Later Major General}}
== Chaplains ==
{{MilLdr|1983|1983||''No permanent Appointment''}}
}}
With ribbons:
{{MilLeadership
| caption = Example
| ribbons = yes
| content =
== Commanding Officers ==
{{MilLdr|Jan 1981|1982|Cmdt|[[Roland de Vries]]|awards=SD, SM, MMM}}
}}
{{MilLdr}} arguments
Passed through to p.line:
| Field | Description |
|---|---|
|1= / |from= |
From date (display string: 1978, Nov 1978)
|
|2= / |to= |
To date |
|3= / |rank= |
Rank or style at the time of the posting (Cmdt, WO1, Ds, …). May be empty (vacancy). |
|4= / |name= |
Name. Wikilink it yourself; this module will not invent links. Aliases: |incumbent=, |post-incumbent=
|
|awards= |
MilAward codes, comma-separated: SD, SM, MMM
|
|note= |
Explanatory footnote ({{efn}}) |
|note-name= |
Named efn (e.g. RIP). First row should also set |note= to define the text; later rows use |note-name= only. Alias: |notename=
|
|notes= |
Extra footnotes, separated by ;;
|
|ref= |
Citation, wrapped in <ref>...</ref> unless it already contains one
|
|link= |
If the name is not already a wikilink: yes wraps [[name]], or a page title wraps [[title\|name]]
|
|vacant= |
yes shows ''No permanent appointment'' when the name is empty
|
Compact year range (list form only): * 1978–1982 // Cmdt // [[Name]]
The article still needs {{notelist}} (and {{reflist}}) where footnotes and references are collected.
Dependencies
- Module:Arguments
- {{MilAward PostNom}} and {{MilAward Stack}} (and therefore Module:MilAward / Module:MilAward/data)
- {{Efn}} for notes
Tracking categories
On mainspace pages only, unless |nocat=yes:
- Category:Pages using MilLeadership
- Category:MilLeadership errors (unknown line fields, empty post lists, missing posts)
See also
- {{MilLeadership}} / {{MilLeadership/testcases}}
- {{MilLdr}}
- {{MilAward}}
- {{Post-start}} (legacy)
- User:BoonDock/sandbox/MilLeadership (61 Mech conversion demo)
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