Module:WPSHIPS utilities
Documentation for this module may be created at Module:WPSHIPS utilities/doc
require('strict')
local get_args = require ('Module:Arguments').getArgs;
local styles = require ('Module:WPMILHIST Infobox style'); -- infobox css
local data = mw.loadData ('Module:WPSHIPS utilities/data');
local namespace = mw.title.getCurrentTitle().namespace; -- used for categorization
--[[--------------------------< E R R O R _ M A P _ T >--------------------------------------------------------
]]
local error_map_t = { -- [1] is error message; [2] is error category
synonymous = {'has synonymous parameter', 'Category:Pages using infobox ship with synonymous parameters'};
missing = {'missing required parameter: %s', 'Category:WPSHIPS: sclass and sclass2 errors'},
format = {'invalid format code: %s. Should be 0–5, or blank', 'Category:WPSHIPS: sclass and sclass2 errors'},
}
--[[--------------------------< E R R O R _ M S G _ M A K E >--------------------------------------------------
assembles an error message from message text and category in <error_map_t>. creates a help link to the category
page; categorizes only main and template namespaces.
<no_cat> disables categorization for those templates that support it; to disable categorization set <no_cat> true
]]
local function error_msg_make (msg_idx, template, detail, no_cat)
local out = {};
local category;
table.insert (out, '<span style=\"font-size: 100%; font-style: normal;\" class=\"error\">Error: '); --TODO: simplify?
if template then
table.insert (out, table.concat ({'{{', template, '}} '})); -- TODO: get template names for synonymous parameter errors
end
table.insert (out, string.format (error_map_t[msg_idx][1], detail)); -- build error message from base + <detail>
table.insert (out, table.concat ({' ([[:', error_map_t[msg_idx][2], '|help]])'})); -- help text on category pages; TODO: help text on template pages also?
table.insert (out, '</span>');
if (0 == namespace or 10 == namespace) and not no_cat then -- categorize in article space (and template space to take care of broken usages)
table.insert (out, table.concat ({'[[', error_map_t[msg_idx][2], ']]'}));
end
return table.concat (out); -- make a big string and done
end
--[[--------------------------< I S _ S E T >------------------------------------------------------------------
Returns true if argument is set; false otherwise. Argument is 'set' when it exists (not nil) or when it is not an empty string.
]]
local function is_set( var )
return not (var == nil or var == '');
end
--[[--------------------------< S I Z E O F _ S H I P _ T Y P E >----------------------------------------------
Returns the size in words of ship type. Inputs are the fragment table, the number of elements in the fragment table,
and the number of words that make up nationality.
The number of fragments (words) in a ship name dictate the possible sizes of ship type. If nationality takes one fragment,
and ship type takes four fragments, then the minimum number of fragments in a composite ship name is:
5 = 1 (nationality) + 3 (ship type) + 1 (ship name) (same as 4 fragments (words) after nationality)
This function starts at the longest possible series of fragments that might be ship type. This order is important becuase
some ship types might begin with similar fragments: 'ship' and 'ship of the line'. Starting with the least possible
series of fragments (1) would find 'ship' and make 'of the line' part of the italicized name.
Returns 0 if there is no recognizable ship type.
]]
local function sizeof_ship_type (frag, frag_len, nat_len)
local ship_type;
if 5 <= (frag_len - nat_len) then -- must have at least five fragments after nationality for four-word ship type
ship_type = table.concat (frag, ' ', nat_len+1, nat_len+4); -- four-word ship type
if data.ship_type_t[ship_type] then
return 4;
end
end
if 4 <= (frag_len - nat_len) then -- must have at least four fragments after nationality for three-word ship type
ship_type = table.concat (frag, ' ', nat_len+1, nat_len+3); -- three-word ship type
if data.ship_type_t[ship_type] then
return 3;
end
end
if 3 <= (frag_len - nat_len) then -- must have at least three fragments after nationality for two-word ship type
ship_type = table.concat (frag, ' ', nat_len+1, nat_len+2); -- two-word ship type
if data.ship_type_t[ship_type] then
return 2;
end
end
if 2 <= (frag_len - nat_len) then -- must have at least two fragments after nationality for one-word ship type
if data.ship_type_t[frag[nat_len+1]] then -- one-word ship type
return 1;
end
end
return 0; -- no recognizable ship type
end
--[[--------------------------< S I Z E O F _ N A T I O N A L I T Y >------------------------------------------
This function the size (in words) of the nationality from the fragments table. Nationality may be one or two
words that occupy the first one or two positions in the table.
Returns the number of words that identify the nationality:
1 for French or German, etc.
2 for United States;
0 when table doesn't have a recognizable nationality
]]
local function sizeof_nationality (frag, frag_len)
local nat = '';
if not data.nationality_t [frag[1]] then -- if not a one-word nationality
if 2 <= frag_len - 2 then -- must have at least two fragments after nationality for minimal ship type and name
nat = table.concat (frag, ' ', 1, 2);
if data.nationality_t [nat] then -- is it a two-word nationality?
return 2; -- yes
else
return 0; -- no
end
end
return 0; -- not one-word and not enough fragments for two-word
end
return 1; -- one-word nationality
end
--[[-------------------------< D O _ S H I P _ N A M E _ F O R M A T >-----------------------------------------
This function applies correct styling to ship and ship-class names. These names are, for example, ship-article titles
used by templates {{navsource}}, {{Infobox ship begin}}, where the article title is to be rendered with proper
styling.
This function requires one argument:
|name= (required): a name is required; if missing or empty, this function returns an error message (may or may not be visible depending on where it is used)
used in {{infobox ship begin}} to provide a value for {{DISPLAYTITLE:}} and to provide a value for |infobox caption=
{{#invoke:WPSHIPS_utilities|ship_name_format|name={{PAGENAME}}}}
Optional arguments to support {{infobox ship begin}}:
|dab=none – displays ship name without parenthetical disambiguator; use when |infobox caption=nodab
|sclass=2 – for ship classes only; displays class name without italics (parameter name is loosely similar to {{slcass2}} which does the same thing); use when |infobox caption=class
|adj=off – for ship classes only; displays class name as a noun (no hyphen, no ship type); use when |infobox caption=class
Arguments are passed in a table.
to call this function locally:
do_ship_name_format ({['name=name'], ['dab']=dab, ['sclass']=sclass, ['adj']=adj, ['showerrs']=showerrs}) or
args = {['name=name'], ['dab']=dab, ['sclass']=sclass, ['adj']=adj, ['showerrs']=showerrs};
do_ship_name_format (args)
The function returns the formatted name or, if unable to format the name, the original name and an unformatted error message.
]]
local function do_ship_name_format (args)
local name_sans_dab; -- the ship or class name without a trailing parenthetical dab
local dab; -- the dab stripped from the name
local fragments = {}; -- a table of words that make up name_sans_dab
local ship_type; -- a word or phrase that describes a ship
local type_len; -- the number of words that describe a ship
local nat_len; -- the number of words used to specify a ship's nationality
local name = ''; -- the reassembles and formatted ship name
local error_msg = ''; -- a repository for error messages if any
-- args.name = mw.text.decode (args.name); -- replace html entities in title with their characters; doesn't work for & and & in prefix
args.name = args.name:gsub ("'", "\'"); -- replace html appostrophe with the character
-- args.name = args.name:gsub ("&", "&");
-- args.name = args.name:gsub ("&", "&");
-- args.name = args.name:gsub ("&", "&");
if args.name:match ('.+%-class%s+%a+') then -- if a ship-class
fragments = mw.text.split (args.name, '-class' ); -- split at -class
if '2' == args.sclass then -- for DISPLAYTITLE and infobox caption when class not named for a member of the class
if 'off' == args.adj then
return fragments[1] .. ' class'; -- for infobox caption do noun form <name> class (no hyphen, no ship type)
end
return args.name; -- nothing to do so return original unformatted name
end
if 'off' == args.adj then
return "''" .. fragments[1] .. "'' class"; -- for infobox caption do noun form <name> class (no hyphen, no ship type)
end
return "''" .. fragments[1] .. "''-class" .. fragments[2]; -- and return formatted adjectival name
end
-- not a ship class so try to format a ship name
name_sans_dab, dab = args.name:match('^(.+)%s+(%b())%s*$'); -- split name into name_sans_dab and dab
if is_set (dab) then
dab = ' ' .. dab; -- insert a space for later reassembly
else
name_sans_dab = args.name; -- because without a dab, the string.match returns nil
dab = ''; -- empty string for concatenation
end
fragments = mw.text.split (name_sans_dab, '%s' ); -- split into a table of separate words
nat_len = sizeof_nationality (fragments, #fragments); -- get the number of words in the ship's nationality
if 0 < nat_len then -- if not zero we have a valid nationality
type_len = sizeof_ship_type (fragments, #fragments, nat_len); -- get the number of words in the ship type
if 0 < type_len then -- if not zero, ship type is valid; nationality and type not italics, the rest is name
name = "''" .. table.concat (fragments, ' ', nat_len + type_len + 1) .. "''"; -- format name
if 'none' == args.dab then -- for |infobox caption=nodab
return name; -- return the formatted name without the nationality or ship type or dab
end
name = table.concat (fragments, ' ', 1, nat_len + type_len) .. " " .. name; -- assemble everything but dab
else
error_msg = ' unrecognized ship type;'; -- valid nationality, invalid ship type
end
elseif data.ship_prefix_t[fragments[1]] then -- if the first fragment is a ship prefix
name = table.remove (fragments, 1); -- fetch it from the table
name = name .. " ''" .. table.concat (fragments, ' ') .. "''"; -- assemble formatted name
else
error_msg = ' no nationality or prefix;'; -- invalid nationality and first word in ship name not a valid prefix
end
if is_set (name) then -- name will be set if we were able to format it
if 'none' == args.dab then -- for |infobox caption=nodab
return name; -- return the formatted name without the dab
end
return name .. dab; -- return the formatted name with the dab
end
if is_set (dab) then
if dab:match ('%(%u+[%- ]?%d+%)') or -- one or more uppercase letters, optional space or hyphen, one or more digits
dab:match ('%(%d+[%- ]?%u+%)') or -- one or more digits, optional space or hyphen, one or more uppercase letters
dab:match ('%(%u[%u%-]*%-%d+%)') or -- one or more uppercase letters with hyphens, a hyphen, one or more digits (e.g., T-AO-157)
dab:match ('%([12]%d%d%d%)') then -- four digits representing year in the range 1000–2999
name = "''" .. table.concat (fragments, ' ') .. "''"; -- format the name
if 'none' == args.dab then -- for |infobox caption=nodab
return name; -- return the formatted name without the dab
end
return name .. dab; -- return the formatted name with the dab
end
-- last chance, is there a ship type in the dab?
for key, _ in pairs (data.ship_type_t) do -- spin through the ship type list and see if there is a ship type (key) in the dab
if dab:find ('%f[%a]' .. key .. '%f[^%a]') then -- avoid matches that are not whole word
name = "''" .. table.concat (fragments, ' ') .. "''"; -- format the name
if 'none' == args.dab then -- for |infobox caption=nodab
return name; -- return the formatted name without the dab
end
return name .. dab; -- return the formatted name with the dab
end
end
error_msg = error_msg .. ' no ship type in dab;';
if 'none' == args.dab then -- for |infobox caption=nodab
return table.concat (fragments, ' '), error_msg; -- return the unformatted name without the dab, and an error message
end
end
return args.name, error_msg; -- return original un-formatted name with unformatted error message if any
end
--[[-------------------------< S H I P _ N A M E _ F O R M A T >-----------------------------------------------
This function is the external interface to do_ship_name_format().
The function requires one parameter:
|name= (required): a name is required; if missing or empty, this function returns an error message (may or may not be visible depending on where it is used)
used in {{infobox ship begin}} to provide a value for {{DISPLAYTITLE:}} and to provide a value for |infobox caption=
{{#invoke:WPSHIPS_utilities|ship_name_format|name={{PAGENAME}}}}
Optional parameters to support {{infobox ship begin}}:
|dab=none – displays ship name without parenthetical disambiguator; use when |infobox caption=nodab
|sclass=2 – for ship classes only; displays class name without italics (parameter name is loosely similar to {{slcass2}} which does the same thing); use when |infobox caption=class
|adj=off – for ship classes only; displays class name as a noun (no hyphen, no ship type); use when |infobox caption=class
Other optional parameters:
|showerrs=yes – marginally useful; can display error messages if the module invocation is not buried in a template
Values from the above parameters are placed in a table and that table passed as an argument in the call to do_ship_name_format().
do_ship_name_format() returns two strings: a name and an error message. If do_ship_name_format() could format the name, it returns the formatted name and
an empty string for the error message. If it could not format the name, do_ship_name_format() returns the original name and an error message.
Formatting of the error message, in response to |showerrs=yes is the responsibility of the calling function.
]]
local function ship_name_format(frame)
local name = ''; -- destination of the formatted ship name
local error_msg = ''; -- destination of any error message
if not is_set (frame.args.name) then -- if a ship name not provided
if 'yes' == frame.args.showerrs then -- and we're supposed to show errors
error_msg = '<span style="font-size:100%; font-weight:normal" class="error">Empty name</span>'; -- return an empty string error message if there is no name
end
else
name, error_msg = do_ship_name_format (frame.args); -- get formatted name and error message
if is_set (error_msg) and 'yes' == frame.args.showerrs then -- if appropriate, show error message
error_msg = '<span style="font-size:100%; font-weight:normal" class="error">' .. error_msg .. '</span>';
else
error_msg = ''; -- for concatenation
end
end
return name .. error_msg; -- return name and error message
end
--[[--------------------------< H N S A >----------------------------------------------------------------------
Similar to {{navsource}}, this code supports {{hnsa}} by attempting to construct a link to a ship article at the
the Historic Nava Ships Association website.
The template has the form:
{{hnsa|<page>|<name>}}
where:
<page> is the name of the page at http://hnsa.org/hnsa-ships/<page>
<name> (optional) is the name of the ship; if left blank, the template uses the current page title; if a ship name, it is formatted
from which this code produces:
[http://hnsa.org/hnsa-ships/<page> <name>] at Historic Naval Ships Association
]]
local function hnsa (frame)
local pframe = frame:getParent() -- get arguments from calling template frame
local ship_name = '';
local error_msg = '';
local article_title = mw.title.getCurrentTitle().text; -- fetch the article title
if not is_set (pframe.args[1]) then
return '<span style="font-size:100%; font-weight:normal" class="error">missing hsna page</span>';
end
local fmt_params = {['name']='', ['showerrs']=nil};
if is_set (pframe.args.showerrs) then -- if showerrs set in template, override showerrs in #invoke:
fmt_params.showerrs = pframe.args.showerrs; -- template value
else
fmt_params.showerrs = frame.args.showerrs; -- invoke value
end
if is_set (pframe.args[2]) then
fmt_params.name = pframe.args[2];
else
fmt_params.name = article_title; -- use article title
end
ship_name, error_msg = do_ship_name_format (fmt_params);
if is_set (error_msg) and is_set (pframe.args[2]) then -- if unable to format the name
local escaped_name = pframe.args[2]:gsub("([%(%)%.%-])", "%%%1"); -- escape some of the Lua magic characters
if pframe.args[2] == article_title or -- is name same as article title?
nil ~= article_title:find ('%f[%a]' .. escaped_name .. '%f[%s]') or -- is name a word or words substring of article title?
nil ~= article_title:find ('%f[%a]' .. escaped_name .. '$') then -- is name a word or words substring that ends article title?
ship_name = "''" .. pframe.args[2] .. "''"; -- non-standard 'name'; perhaps just the name without prefix and dab;
error_msg = ''; -- unset because we think we have a name
end
end
if is_set (error_msg) and 'yes' == fmt_params.showerrs then
error_msg = '<span style="font-size:100%; font-weight:normal" class="error">' .. error_msg .. '</span>';
else
error_msg = ''; -- unset so it doesn't diplay
end
local output = {
'[http://www.hnsa.org/hnsa-ships/',
pframe.args[1],
'/ ',
ship_name,
'] at Historic Naval Ships Association',
error_msg,
}
return table.concat (output);
end
--[[--------------------------< N A V S O U R C E >------------------------------------------------------------
This version of the template {{navsource}} was added as a test vehicle for do_ship_name_format().
]]
local function navsource (frame)
local pframe = frame:getParent() -- get arguments from calling template frame
local ship_name = '';
local error_msg = '';
local article_title = mw.title.getCurrentTitle().text; -- fetch the article title
if not is_set (pframe.args[1]) then
return '<span style="font-size:100%; font-weight:normal" class="error">missing navsource URLcode</span>';
end
local fmt_params = {['name']='', ['showerrs']=nil};
if is_set (pframe.args.showerrs) then -- if showerrs set in template, override showerrs in #invoke:
fmt_params.showerrs = pframe.args.showerrs; -- template value
else
fmt_params.showerrs = frame.args.showerrs; -- invoke value
end
if is_set (pframe.args[2]) then
fmt_params.name = pframe.args[2];
else
fmt_params.name = article_title; -- use article title
end
ship_name, error_msg = do_ship_name_format (fmt_params);
if is_set (error_msg) and is_set (pframe.args[2]) then -- if unable to format the name
local escaped_name = pframe.args[2]:gsub("([%(%)%.%-])", "%%%1"); -- escape some of the Lua magic characters
if pframe.args[2] == article_title or -- is name same as article title?
nil ~= article_title:find ('%f[%a]' .. escaped_name .. '%f[%s]') or -- is name a word or words substring of article title?
nil ~= article_title:find ('%f[%a]' .. escaped_name .. '$') then -- is name a word or words substring that ends article title?
ship_name = "''" .. pframe.args[2] .. "''"; -- non-standard 'name'; perhaps just the name without prefix and dab;
error_msg = ''; -- unset because we think we have a name
end
end
if is_set (error_msg) and 'yes' == fmt_params.showerrs then
error_msg = '<span style="font-size:100%; font-weight:normal" class="error">' .. error_msg .. '</span>';
else
error_msg = ''; -- unset so it doesn't diplay
end
local output = {
'[http://www.navsource.org/archives/',
pframe.args[1],
'.htm Photo gallery] of ',
ship_name,
' at NavSource Naval History',
error_msg,
}
return table.concat (output);
end
--[[--------------------------< _ S H I P >--------------------------------------------------------------------
This is a possible replacement for the template {{ship}}. It has better error detection and handling.
]]
local function _ship (prefix, name, dab, control, unlinked_prefix, unlinked_whole)
local error_msg = '';
local category = '';
if not is_set (control) then
control = ''; -- if not provided, ensure that control is empty string for comparisons
elseif control:find ('%-') then -- shortcut for |link=no when using a format control parameter ...|SSBN-659|-6}} same as ...|SSBN-659|6|link=no}}
unlinked_whole = true; -- set the unlinked flag
control = control:match ('%d'); -- strip out the hyphen
end
-- dispose of error conditions straight away
if not is_set (name) then -- this is the only required parameter
error_msg = ' missing name';
elseif not is_set (dab) and ('1' == control or '3' == control or '5' == control) then -- dab required when control value set to expect it
error_msg = ' missing disambiguator';
elseif not is_set (prefix) and ('5' == control or '6' == control) then -- prefix required when control value set to expect it
error_msg = ' missing prefix';
elseif '4' == control then -- displaying only the prefix
error_msg = 'invalid control parameter: ' .. control;
elseif is_set (control) then
if ('number' ~= type (tonumber (control))) or (1 > tonumber (control) or 6 < tonumber (control)) then -- control must be a number between 1 through 6
error_msg = 'invalid control parameter: ' .. control;
end
elseif not is_set (prefix) and unlinked_prefix then -- prefix required when |up=yes
error_msg = ' missing prefix';
end
if is_set (error_msg) then
if 0 == mw.title.getCurrentTitle().namespace then -- only categorize pages in article space
category = '[[Category:WPSHIPS: template ship parameter errors]]';
end
return '<span style="font-size:100%" class="error">' .. error_msg .. '</span>' .. category; -- return an error message; don't bother with making a link
end
local link_name;
local link = '[[';
if is_set (prefix) then
link = link .. prefix .. ' ' .. name; -- begin assembling the article name portion of the wikilink
else
link = link .. name;
end
if is_set (dab) then
link = link .. ' (' .. dab .. ')'; -- wrap dab in parentheses
end
name = "''" .. name .. "''"; -- name is always italicized so do it now
if '1' == control then
link_name = dab; -- special case when displaying only the dab, don't wrap in parentheses
end
if is_set (dab) then -- for all other cases that display dab
if '5' == control then
dab = " \'\'" .. dab .. "\'\'"; -- for prefix with dab display HMS A1. italicize the dab
else
dab = ' (' .. dab .. ')'; -- except for dab-only, all others display with parentheses
end
end
if not is_set (control) then -- when control not set: prefix, name, and dab
if unlinked_prefix then -- 'unlinked prefix'?
link = prefix .. ' ' .. link; -- yes, modify link so that prefix is not linked in final render
link_name = name .. dab;
elseif is_set (prefix) then
link_name = prefix .. ' ' .. name .. dab;
else
link_name = name .. dab;
end
else -- when control is not 1 or none
if '2' == control then -- name only
link_name = name;
elseif '3' == control then -- name and dab
link_name = name .. dab;
elseif '5' == control then -- prefix and dab
if unlinked_prefix then -- 'unlinked prefix'?
link = prefix .. ' ' .. link; -- yes, modify link so that prefix is not linked in final render
link_name = dab;
else
link_name = prefix .. dab;
end
elseif '6' == control then -- prefix and name
if unlinked_prefix then -- 'unlinked prefix'?
link = prefix .. ' ' .. link; -- yes, modify link so that prefix is not linked in final render
link_name = name;
else
link_name = prefix .. ' ' .. name;
end
end
end
if unlinked_whole then
return link_name;
else
return link .. '|' .. link_name .. ']]';
end
end
--[[--------------------------< S H I P >----------------------------------------------------------------------
This is a possible replacement for the template {{ship}}. It has better error detection and handling.
This function is the externally accessible entry point for template {{ship}}, {{HMS}}, {{USS}}, etc
{{#invoke:WPSHIPS_utilities/sandbox|ship|{{{1|}}}|{{{2|}}}|{{{3|}}}|{{{4|}}}|link={{{link|}}}|up={{{up|}}}}}
]]
local function ship (frame) -- this version not supported from the template yet
local prefix = mw.text.trim (frame.args[1] or ''); -- fetch positional parameters into named variables for readability
local name = mw.text.trim (frame.args[2] or ''); -- stripped of leading and trailing whitespace
local dab = mw.text.trim (frame.args[3] or ''); -- and and set to empty string (is that needed?)
local control = frame.args[4];
local unlinked_prefix = 'yes' == frame.args.up;
local unlinked_whole = 'no' == frame.args.link;
return _ship (prefix, name, dab, control, unlinked_prefix, unlinked_whole);
end
--[[--------------------------< L I S T _ E R R O R >----------------------------------------------------------
Assembles an error message, the original parameter value and, if appropriate a category. The error message precedes
the existing value of the parameter. If the first non-whitespace character in the parameter is a '*', set prefix to
a '*' and sep to '\n'. For line-break lists, set prefix to empty string and sep to '<br />'.
Category is appended to the end of the returned value only for pages in article space.
Inputs:
prefix – a string of characters that precede the error message span; typically '' and '*'
message – the error message to be displayed; goes inside the span
sep – a string of characters that separates the span from the parameter value; typically '\n' and '<br />'
param_val – the unmodified parameter value
showerrs – a boolean, true to display the error message text
]]
local function list_error (prefix, message, sep, param_val, showerrs)
local err_msg = '%s<span style="font-size:100%%" class="error">list error: %s ([[:Category:WPSHIPS:Infobox list errors|help]])</span>%s%s%s';
local category = '';
if 0 == mw.title.getCurrentTitle().namespace then -- only categorize pages in article space
category = '[[Category:WPSHIPS:Infobox list errors]]';
end
if true == showerrs then
return string.format (err_msg, prefix, message, sep, param_val, category); -- put it all together
else
return param_val .. category;
end
end
--[[--------------------------< U N B U L L E T E D _ L I S T >------------------------------------------------
Mediawiki:Common.css imposes limitations on plain, unbulleted lists. The template {{plainlist}} does not render this correctly:
{{plainlist|
*Item 1
*Item 2
**Item 2a
***Item 2a1
**Item 2b
*Item 3}}
The above renders without proper indents for items marked ** and ***.
If the list is not wrapped in {{plainlist}} then the above list is rendered with bullets which is contrary to the Infobox ship usage guide.
This code translates a bulleted list into an html unordered list:
<ul style="list-style:none; margin:0;">
<li>Name 1</li>
<li>Name 2</li>
<ul style="list-style:none">
<li>Subname 2a</li>
<ul style="list-style:none">
<li>Subname 2a1</li>
</ul>
<li>Subname 2b</li>
</ul>
<li>Name 3</li>
</ul>
There are rules:
1. The parameter value must begin with a splat but may have leading and trailing whitespace.
2. Each list item after the first must begin on a new line just as is required by normal bulleted lists.
3. When adding a sublevel, the number of splats may increase by one and never more. This is illegal:
*item
***item
When any of these rules are violated, unbulleted_list() returns the original text and adds the article
to Category:WPSHIPS:Infobox list errors. Error messaging in this function is somewhat sketchy so they are
disabled. After initial adoption, better error messaging could/should be implemented.
This function receives the content of one parameter:
{{#invoke:WPSHIPS utilities|unbulleted_list|{{{param|}}}}}
]]
local function _unbulleted_list (param)
local showerrs = true; -- set to false to hide error messages
local List_item_otag = '<li style="padding-left: .4em; text-indent: -.4em;">'; -- hanging indent markup; everything moves right with padding-left; first line moved left by neg indent
if nil == param:match ('^%s*%*') then -- parameter value must begin with a splat (ignoring leading white space)
if param:match ('<[%s/]*[Bb][Rr][%s/]*>') then -- if the parameter value has a list using variants of <br /> tag
return list_error ('', '<br /> list', '<br />', param, showerrs); -- return an error message with maintenance category
elseif param:match ('<div style="clear:') then
return list_error ('', '{{clear}} list', '<br />', param, showerrs); -- return an error message with maintenance category
elseif param:match ('.+\n%*') then -- if the parameter value has text followed by an unordered list
return list_error ('', 'mixed text and list', '<br />', param, showerrs); -- return an error message with maintenance category
end
return param; -- return the parameter as is
end
local item_table = mw.text.split (mw.text.trim (param), '\n'); -- trim white space from end then make a table of bulleted items by splitting on newlines
if 1 == #item_table then -- if only one bulleted item, no need for a list
return (item_table[1]:gsub ('^%*%s*', '')); -- trim off the splat and any following white space and done
end
if item_table[1]:match ('^%*%*+') then -- if first list item uses more than one splat, that's an error
return list_error ('*', 'too many * at start of list', '\n', param, showerrs); -- return an error message with maintenance category
end
local html_table = {}; -- table to hold the html output
local level = 1; -- used to indicate when a new <ul> is required
local splats = 0; -- number of splats that start each item in the list
local item = ''; -- the item text
table.insert (html_table, '<ul style="list-style:none; margin:0;">') -- this for first <ul> tag; sets no bullets and no indent
for _,v in ipairs (item_table) do
splats, item = v:match ('(%*+)%s*(.*)'); -- split the item into splats and item text
if nil == splats then -- nil if there is an extra line between items
return list_error ('*', 'list item missing markup', '\n', param, showerrs); -- return an error message with maintenance category
elseif '' == item then
return list_error ('*', 'empty list item', '\n', param, showerrs); -- return an error message with maintenance category
elseif item:match ('^[;:]') then -- if the list item is mixed unordered list / description list markup (*:)
return list_error ('*', 'mixed list type', '\n', param, showerrs); -- return an error message with maintenance category
end
splats = splats:len(); -- change string of splats into a number indicating how many splats there are
if splats == level then -- if at the same level as previous item
table.insert (html_table, List_item_otag .. item .. '</li>');
elseif splats == level + 1 then -- number of splats can only increase by one
level = splats; -- record the new level
table.insert (html_table, '<ul style="list-style: none">'); -- add a new sublist
table.insert (html_table, List_item_otag .. item .. '</li>'); -- and the item
elseif splats < level then -- from three splats to one splat for example
while splats < level do
table.insert (html_table, '</ul>'); -- close each sub <ul> until level and splats match
level = level - 1;
end
table.insert (html_table, List_item_otag .. item .. '</li>'); -- add the item
else -- jumping more than one level up – one splat to three splats for example – is an error
return list_error ('*', 'too many asterisks', '\n', param, showerrs); -- return an error message with maintenance category
end
end
while 0 < level do
table.insert (html_table, '</ul>'); -- close each <ul> until level counted down to zero
level = level - 1;
end
return table.concat (html_table, '\n'); -- return the list as a string
end
--[[--------------------------< U N B U L L E T E D _ L I S T >------------------------------------------------
external entry point
]]
local function unbulleted_list (frame)
return _unbulleted_list (frame.args[1])
end
--[=[-------------------------< _I N F O B O X _ S H I P _ F L A G >--------------------------------------------
Output of {{shipboxflag|USA}}:
[[File:Flag of the United States.svg|100x35px|alt=|link=]]
Image syntax:
[[File:Name|Type|Border|Location|Alignment|Size|link=Link|alt=Alt|Caption]]
This function standardizes the size of flag images in the infobox ship career header by simply overwriting the Size
parameter value in the Image wikilink with |100x28px. This size leave a 1px gap between the top and bottom of the flag image and
the header edge. A similar left-side gap of 2px is supplied by {{infobox ship career}}.
]=]
local function _infobox_ship_flag (image)
if image:match ('|[%s%dx]+px%s*') then -- is there a size positional parameter?
image = image:gsub('|[%s%dx]+px%s*', '%|100x28px'); -- overwrite it with |100x28px
else
return '<span style="font-size:100%" class="error">malformed flag image</span>'
end
return image; -- return the modified image
end
--[=[-------------------------< I N F O B O X _ S H I P _ F L A G >--------------------------------------------
external entry point
]=]
local function infobox_ship_flag (frame)
if not is_set (frame.args[1]) then -- if |Ship flag= not set
return ''; -- return empty string
end
return _infobox_ship_flag (frame.args[1]); -- return the modified image
end
--[=[-------------------------< C I T E _ D A N F S _ T I T L E >----------------------------------------------
This function attempts to render a DANFS article title in more or less proper (per Wikipedia) format for the
template {{cite danfs}}. DANFS titles typically take one of four forms:
<ship name> <disambiguator> <hull number>
<ship name> <hull number>
<ship name> <disambiguator>
<ship name>
Here, we extract the various parts, italicize the ship name and reassemble for use by the cite danfs |title= parameter.
To call this function:
|title={{#invoke:WPSHIPS utilities|cite_danfs_title|{{{title}}}}}
]=]
local function cite_danfs_title (frame)
local name;
local disambiguator;
local hullnum;
if not is_set (frame.args[1]) then -- if |title= not set
return ''; -- return empty string
end
name, disambiguator, hullnum = frame.args[1]:match ('(.-)( [XVI]+)( %([^%)]+%))$');
if not (name and disambiguator and hullnum) then
disambiguator = ''; -- empty string for concatenation
name, hullnum = frame.args[1]:match ('(.-)( %([^%)]+%))$');
if not (name and hullnum) then
hullnum = ''; -- empty string for concatenation
name, disambiguator = frame.args[1]:match ('(.-)( [XVI]+)$');
if not (name and disambiguator) then
disambiguator = '';
name = frame.args[1]; -- just a name or something we don't recognize
end
end
end
return table.concat ({"''", name, "''", disambiguator, hullnum}); -- reassemble and done
end
--[[--------------------------< S Y N O N Y M _ C H E C K >----------------------------------------------------
support function for infoboxen functions
there are a handful of infoboxen parameters that are synonymous pairs. This function is called to see if both
of the parameters in a synonymous pair have been assigned a value. When both have assigned values, each gets an
error message appended to it. Most of the synonymous pairs are UK Emglish v US English so the variable names
<args_t> table of template parameters and values
<uk_param> UK English parameter name
<us_param> US English parameter name
<error_flag> boolean that this function sets true when both of a pair are set; controls addition of maint category
]]
local function synonym_check (args_t, uk_param, us_param, error_flag)
if args_t[uk_param] and args_t[us_param] then
args_t[uk_param] = args_t[uk_param] .. error_msg_make ('synonymous'); -- both are set so append error message with category
args_t[us_param] = args_t[us_param] .. error_msg_make ('synonymous', nil, nil, true); -- but append error message without category
return true; -- inform the calling function that it needs to emit maint category
end
return error_flag; -- no match so return unmodified <error_flag>
end
--[[--------------------------< L I N E _ I T E M S >----------------------------------------------------------
support function for infoboxen functions
This function handles all infobox ship parameters that are 'line items' (label followed by value) because all
of these sorts of parameters are rendered with exacty the same formatting.
params_t{} is a table of tables where the params_t{} keys are the template's parameter names. The params_t{}
values are sequences where [1] is an index number that defines where in the rendering the label/value pair is
positioned and [2] is the label that will be rendered when the parameter has a value.
This indexing is used because params_t{} is not a sequence and because pairs() does not necessarily return the
'next' k/v pair.
This function spins through params_t{} and writes html for parameters that have assigned values into temp_t{}
according to the 'index' value in the associated sequance table. When parameters are missing or empty, this
function writes an empty string into the associated location in lines_t{} so that lines_t{} can be concatenated
into a string value that is returned to the calling function.
args_t is the arguments table from the template frame.
]]
local function line_items (args_t, params_t)
local lines_t = {}; -- a sequence table of rendered label/value html lines; one for each key in params_t{}
for k, v in pairs (params_t) do -- k is templat e parameter name; v is a sequence table with index and associated label
local temp_t = {} -- initialize/reinitialize for next line item
if not args_t[k] then -- if no assigned value then
lines_t[v[1]] = ''; -- set to empty string for concatenation
else
table.insert (temp_t, '<tr style="vertical-align:top;"><td style="font-weight: bold">'); -- open the line item row and cell
table.insert (temp_t, v[2]); -- add parameter's label text
table.insert (temp_t, '</td><td>'); -- close that cell and open the parameter value cell
table.insert (temp_t, _unbulleted_list (args_t[k])); -- add the parameter's value; formatted as unordered list if appropriate
table.insert (temp_t, '</td></tr>\n'); -- close that cell and this row
end
lines_t[v[1]] = table.concat (temp_t); -- concatenate and put the line item in the lines sequence table at the index position
end
return table.concat (lines_t); -- make a big string of line items and done
end
--[[--------------------------< I N F O B O X _ S H I P _ C A R E E R >----------------------------------------
implements {{Infobox ship career}}
{{#invoke:WPSHIPS utilities|infobox_ship_career}}
]]
local function infobox_ship_career (frame)
local args_t = get_args (frame); -- get a table of all parameters in the template call
local html_out_t = {}; -- html table text goes here
local error_flag = false; -- controls emission of maint category when synonymous parameters are both set
args_t['Hide header'] = args_t['Hide header'] and args_t['Hide header']:lower(); -- set to lowercase if set
error_flag = synonym_check (args_t, 'Ship stricken', 'Ship struck', error_flag); -- error if both synonymous parameters set
error_flag = synonym_check (args_t, 'Ship honours', 'Ship honors', error_flag);
if 'yes' ~= args_t['Hide header'] then -- |Hide header=yes then no header
if not ('title' == args_t['Hide header']) then -- |Hide header=title then no title bar
table.insert (html_out_t, '<tr><th colspan="2" ');
table.insert (html_out_t, styles.header_bar); -- style from WPMILHIST
table.insert (html_out_t, '>History</th></tr>\n');
end
if args_t['Ship country'] and args_t['Ship flag'] then
table.insert (html_out_t, '<tr><th height="30" colspan="2" style="background-color:#B0C4DE;text-align:left;padding-left:2px;vertical-align:middle;font-size:110%;">');
table.insert (html_out_t, _infobox_ship_flag (args_t['Ship flag']));
table.insert (html_out_t, '<span style="padding-left:1em">');
table.insert (html_out_t, args_t['Ship country']);
table.insert (html_out_t, '</span>');
table.insert (html_out_t, '</th></tr>\n');
elseif args_t['Ship country'] then
table.insert (html_out_t, '<tr><th height="30" colspan="2" style="background-color:#B0C4DE;text-align:center;vertical-align:middle;font-size:110%;">');
table.insert (html_out_t, args_t['Ship country']);
table.insert (html_out_t, '</th></tr>\n');
elseif args_t['Ship flag'] then
table.insert (html_out_t, '<tr><th height="30" colspan="2" style="background-color:#B0C4DE;padding-left:2px;">');
table.insert (html_out_t, _infobox_ship_flag (args_t['Ship flag']));
table.insert (html_out_t, '</th></tr>\n');
end
end
table.insert (html_out_t, line_items (args_t, data.infobox_career_params_t)); -- add all of the rest of the template's html
--mw.logObject (table.concat (html_out_t));
return table.concat (html_out_t); -- make a big string and done
end
--[[--------------------------< I N F O B O X _ S H I P _ C H A R A C T E R I S T I C S >----------------------
implements {{Infobox ship characteristics}}
{{#invoke:WPSHIPS utilities|infobox_ship_characteristics}}
]]
local function infobox_ship_characteristics (frame)
local args_t = get_args (frame); -- get a table of all parameters in the template call
local html_out_t = {}; -- html table text goes here
local error_flag = false; -- controls emission of maint category when synonymous parameters are both set
args_t['Hide header'] = args_t['Hide header'] and args_t['Hide header']:lower(); -- set to lowercase if set
error_flag = synonym_check (args_t, 'Ship armour', 'Ship armor', error_flag); -- error if both synonymous parameters set
error_flag = synonym_check (args_t, 'Ship draught', 'Ship draft', error_flag);
if 'yes' ~= args_t['Hide header'] then -- |Hide header=yes then no header
table.insert (html_out_t, '<tr><th colspan="2" ');
table.insert (html_out_t, styles.header_bar); -- style from WPMILHIST
table.insert (html_out_t, '>General characteristics');
if args_t['Header caption'] then
table.insert (html_out_t, ' ');
table.insert (html_out_t, args_t['Header caption']);
end
table.insert (html_out_t, '</th></tr>\n');
end
table.insert (html_out_t, line_items (args_t, data.infobox_characteristics_params_t)); -- add all of the rest of the template's html
--mw.logObject (table.concat (html_out_t));
return table.concat (html_out_t); -- make a big string and done
end
--[[--------------------------< I N F O B O X _ S H I P _ C L A S S _ O V E R V I E W >------------------------
implements {{Infobox ship class overview}}
{{#invoke:WPSHIPS utilities|infobox_ship_class_overview}}
]]
local function infobox_ship_class_overview (frame)
local args_t = get_args (frame); -- get a table of all parameters in the template call
local html_out_t = {}; -- html table text goes here
args_t['Hide header'] = args_t['Hide header'] and args_t['Hide header']:lower();
if 'yes' ~= args_t['Hide header'] then -- |Hide header=yes then no header
table.insert (html_out_t, '<tr><th colspan="2" ');
table.insert (html_out_t, styles.header_bar); -- style from WPMILHIST
table.insert (html_out_t, '>Class overview</th></tr>\n');
end
table.insert (html_out_t, line_items (args_t, data.infobox_class_overview_params_t)); -- add all of the rest of the template's html
--mw.logObject (table.concat (html_out_t));
return table.concat (html_out_t); -- make a big string and done
end
--[[--------------------------< I S _ P L I M S O L L _ F I L E N A M E >--------------------------------------
validate cite plimsoll |filename=<filename>
<filename> format is: YYvssss.pdf where
YY - least significant two digits of four-digit first year in a two-year range
30 -> 1930–1931
allowed values are all integers between and including 30–45
v - a single lowercase letter 'a' or 'b'
'a' -> volume I
'b' -> volume II
ssss - scan number begins with 0001
odd numbered scans have English headings
even numbered scans have French headings
{{#invoke:WPSHIPS utilities|is_plimsoll_filename|{{{filename}}}}}
]]
local function is_plimsoll_filename (frame)
local args_t = get_args (frame); -- get a table of all parameters in the invoke
local year, volume, scan;
if args_t[1] then -- this to avoid script errors when args_t[1] missing
year, volume, scan = args_t[1]:match ('(%d%d)(%l)(%d%d%d%d)%.[Pp][Dd][Ff]'); -- get the various parts
end
if not year then return nil end -- nil when no match so we're done
year = tonumber (year);
scan = tonumber (scan);
if (30 > year) or (45 < year) then return nil end
if not (('a' == volume) or ('b' == volume)) then return nil end
if (1 > scan) then return nil end
return true;
end
--[[--------------------------< S E T _ P L I M S O L L _ D A T E >--------------------------------------------
create two-year range from first two digits in |filename=<filename>
30 -> 1930–1931
{{#invoke:WPSHIPS utilities|set_plimsoll_date|{{{filename}}}}}
]]
local function set_plimsoll_date (frame)
local args_t = get_args (frame); -- get a table of all parameters in the invoke
if not args_t[1] then
return nil;
end
local year = args_t[1]:match ('(%d%d)%l%d%d%d%d'); -- get the intial year
year = 1900 + tonumber (year); -- make it a four-digit year
return string.format ('%d–%d', year, year + 1); -- and then add one for the second year in the range
end
--[[--------------------------< S E T _ P L I M S O L L _ S U B T I T L E >-----------------------------------1
used in {{cite plimsoll}}
return appropriate subtitle string given |subtitle=<keyword>
{{#invoke:WPSHIPS utilities|set_plimsoll_subtitle|{{{subtitle}}}}}
]]
local function set_plimsoll_subtitle (frame)
local subtitle = get_args (frame)[1]; -- get the subtitle parameter
if not subtitle then
return nil;
end
if not data.subtitles_t[subtitle] then
return ': ' .. subtitle; -- not predefined so return whatever |subtitle= holds with leading ': '
end
return ': ' .. data.subtitles_t[subtitle]; -- return predefined subtitle with leading ': '
end
--[[--------------------------< S E T _ P L I M S O L L _ U R L >----------------------------------------------
create plimsoll url from |filename=<filename>
{{#invoke:WPSHIPS utilities|set_plimsoll_url|{{{filename}}}}}
]]
local function set_plimsoll_url (frame)
local args_t = get_args (frame); -- get a table of all parameters in the invoke
if args_t[1] then
return string.format ('https://plimsoll.southampton.gov.uk/shipdata/pdfs/%s/%s',
args_t[1]:match ('(%d%d)%l%d%d%d%d'), -- get the year path portion from <filename>
args_t[1]); -- append <filename> onto the end, and done
end
end
--[[--------------------------< S C L A S S >------------------------------------------------------------------
implements {{sclass}} and {{sclass2}}
{{#invoke:WPSHIPS utilities|sclass}}
]]
local function sclass (frame)
local args_t = get_args (frame);
local parent = frame:getParent();
local template = parent:getTitle():gsub ('^Template:', ''):lower(); -- get the name of the template that called this module (includes namespace so strip that)
local class_name = args_t[1]; -- names to make it easier to understand
local ship_type = args_t[2];
local format = args_t[3];
local ship_type_dab = args_t[4];
local class_name_dab = args_t[5];
local no_cat = 'yes' == args_t['no-cat']; -- make a boolean
if not class_name then -- when omitted, abandon with error message
return error_msg_make ('missing', template, 'class name', no_cat);
end
if not ship_type then -- when omitted, abandon with error message
return error_msg_make ('missing', template, 'ship type', no_cat);
end
if format then
if tonumber (format) then -- if <format> has a value that is a number
format = tonumber (format); -- make it a number for comparisons
if 5 < format then -- is <format> outside of allowed range
return error_msg_make ('format', template, format, no_cat);
end
else -- <format> could not be converted to a number
return error_msg_make ('format', template, format, no_cat);
end
end
local out_t = {}; -- output goes here
table.insert (out_t, '[['); -- open the wikilink
table.insert (out_t, class_name); -- build the wikilink to the class article
table.insert (out_t, '-class ');
table.insert (out_t, ship_type); -- add ship type
if class_name_dab then -- when class article is disambiguated
table.insert (out_t, ' ('); -- add the disambiguator
table.insert (out_t, class_name_dab);
table.insert (out_t, ')');
end
table.insert (out_t, '|');
if 'sclass' == template:gsub ('/sandbox', '') then -- strip '/sandbox' if present; class named for a member of the class
table.insert (out_t, '\'\''); -- class name is italicized
table.insert (out_t, class_name);
table.insert (out_t, '\'\'');
else
table.insert (out_t, class_name); -- class name is a common attribute; plain text
end
if not format or (3 == format) then -- when format is omitted, same as format #3
table.insert (out_t, '-class]] [['); -- open ship-type wikilink
if ship_type_dab then -- when ship-type article is disambiguated
table.insert (out_t, ship_type); -- add ship type
table.insert (out_t, ' ('); -- and the disambiguator
table.insert (out_t, ship_type_dab);
table.insert (out_t, ')|'); -- dab is not displayed so insert a pipe and
end
table.insert (out_t, ship_type); -- add ship type
table.insert (out_t, ']]'); -- close ship-type wikilink
end
if 0 == format then -- no separate ship-type wikilink
table.insert (out_t, '-class]]');
end
if 1 == format then -- no separate ship-type wikilink
table.insert (out_t, '-class ');
table.insert (out_t, ship_type);
table.insert (out_t, ']]');
end
if 2 == format then -- ship-type is not wikilinked
table.insert (out_t, '-class]] ');
table.insert (out_t, ship_type);
end
if 4 == format then -- noun form; no ship type
table.insert (out_t, ' class]]');
end
if 5 == format then -- class name only; no '-class' annotation
table.insert (out_t, ']]');
end
return table.concat (out_t);
end
--[[--------------------------< E X P O R T S >----------------------------------------------------------------
]]
return {
cite_danfs_title = cite_danfs_title, -- external entry points for templates and invokes
hnsa = hnsa,
infobox_ship_career = infobox_ship_career,
infobox_ship_characteristics = infobox_ship_characteristics,
infobox_ship_class_overview = infobox_ship_class_overview,
infobox_ship_flag = infobox_ship_flag,
is_plimsoll_filename = is_plimsoll_filename,
navsource = navsource,
sclass = sclass,
set_plimsoll_subtitle = set_plimsoll_subtitle,
set_plimsoll_date = set_plimsoll_date,
set_plimsoll_url = set_plimsoll_url,
ship = ship, -- experiment
ship_name_format = ship_name_format,
unbulleted_list = unbulleted_list,
_infobox_ship_flag = _infobox_ship_flag, -- external entry points from another module
_ship_name_format = do_ship_name_format,
_synonym_check = synonym_check,
_unbulleted_list = _unbulleted_list,
}