-- Return text with space before and, if wantboth, after.
-- However, no space is added if there is a space or '&nbsp;' or '-'
-- at that position ('-' is for adjectival text).
-- There is also no space if text starts with '&'
-- (e.g. '&deg;' would display a degree symbol with no preceding space).
local char = text:sub(1, 1)
if char == '&' then
return text -- an html entity can be used to specify the exact display
end
if not (char == ' ' or char == '-' or char == '+') then
text = ' ' .. text
end
if wantboth then
char = text:sub(-1, -1)
if not (char == ' ' or char == '-' or text:sub(-6, -1) == '&nbsp;') then
text = text .. ' '
end
end
return text
end
local PLUS = '+ '
preunit1 = preunit1 or ''
local trim1 = strip(preunit1)
if count == 1 then
if trim1 == '' then
return nil
end
if trim1 == '+' then
return PLUS
end
return withspace(preunit1, true)
end
preunit1 = withspace(preunit1)
preunit2 = preunit2 or ''
local trim2 = strip(preunit2)
if trim1 == '+' then
if trim2 == '' or trim2 == '+' then
return PLUS, PLUS
end
preunit1 = PLUS
end
if trim2 == '' then
if trim1 == '' then
return nil, nil
end
preunit2 = preunit1
elseif trim2 == '+' then
preunit2 = PLUS
elseif trim2 == '&#32;' then -- trick to make preunit2 empty
preunit2 = nil
else
preunit2 = withspace(preunit2)
end
return preunit1, preunit2
end  
local function range_text(range, want_name, parms, before, after, inout, options)
-- Return before .. rtext .. after
-- where rtext is the text that separates two values in a range.
local rtext, adj_text, exception
options = options or {}
if type(range) == 'table' then
-- Table must specify range text for ('off' and 'on') or ('input' and 'output'),
-- and may specify range text for 'adj=on',
-- and may specify exception = true.
rtext = range[want_name and 'off' or 'on'] or
range[((inout == 'in') == (parms.opt_flip == true)) and 'output' or 'input']
adj_text = range['adj']
exception = range['exception']
else
rtext = range
end
if parms.opt_adjectival then
if want_name or (exception and parms.abbr_org == 'on') then
rtext = adj_text or rtext:gsub(' ', '-'):gsub('&nbsp;', '-')
end
end
if rtext == '–' and (options.spaced or after:sub(1, #MINUS) == MINUS) then
rtext = '&nbsp;– '
end
return before .. rtext .. after
end  
local function get_composite(parms, iparm, in_unit_table)
-- Look for a composite input unit. For example,
-- would result in a call to this function with
-- iparm = 3 (parms[iparm] = "2", just after the first unit)
-- in_unit_table = (unit table for "yd"; contains value 1 for number of yards)
-- Return true, iparm, unit where
-- iparm = index just after the composite units (7 in above example)
-- unit = composite unit table holding all input units,
-- or return true if no composite unit is present in parms,
-- or return false, t where t is an error message table.
local default, subinfo
local composite_units, count = { in_unit_table }, 1
local fixups = {}
local total = in_unit_table.valinfo[1].value
local subunit = in_unit_table
while subunit.subdivs do -- subdivs is nil or a table of allowed subdivisions
local subcode = strip(parms[iparm+1])
local subdiv = subunit.subdivs[subcode] or subunit.subdivs[(all_units[subcode] or {}).target]
if not subdiv then
break
end
local success
success, subunit = lookup(parms, subcode, 'no_combination')
if not success then return false, subunit end -- should never occur
success, subinfo = extract_number(parms, parms[iparm])
if not success then return false, subinfo end
iparm = iparm + 2
subunit.inout = 'in'
subunit.valinfo = { subinfo }
-- Recalculate total as a number of subdivisions.
-- subdiv[1] = number of subdivisions per previous unit (integer > 1).
total = total * subdiv[1] + subinfo.value
if not default then -- set by the first subdiv with a default defined
default = subdiv.default
end
count = count + 1
composite_units[count] = subunit
if subdiv.unit or subdiv.name then
fixups[count] = { unit = subdiv.unit, name = subdiv.name, valinfo = subunit.valinfo }
end
end
if count == 1 then
return true -- no error and no composite unit
end
for i, fixup in pairs(fixups) do
local unit = fixup.unit
local name = fixup.name
if not unit or (count > 2 and name) then
composite_units[i].fixed_name = name
else
local success, alternate = lookup(parms, unit, 'no_combination')
if not success then return false, alternate end -- should never occur
alternate.inout = 'in'
alternate.valinfo = fixup.valinfo
composite_units[i] = alternate
end
end
return true, iparm, {
utype = in_unit_table.utype,
scale = subunit.scale, -- scale of last (least significant) unit
valinfo = { { value = total, clean = subinfo.clean, denominator = subinfo.denominator } },
composite = composite_units,
default = default or in_unit_table.default
}
end  
local function translate_parms(parms, kv_pairs)
-- Update fields in parms by translating each key:value in kv_pairs to terms
-- used by this module (may involve translating from local language to English).
-- Also, checks are performed which may display warnings, if enabled.
-- Return true if successful or return false, t where t is an error message table.
currency_text = nil -- local testing can hold module in memory; must clear globals
if kv_pairs.adj and kv_pairs.sing then
-- For enwiki (before translation), warn if attempt to use adj and sing
-- as the latter is a deprecated alias for the former.
if kv_pairs.adj ~= kv_pairs.sing and kv_pairs.sing ~= '' then
add_warning(parms, 1, 'cvt_unknown_option', 'sing=' .. kv_pairs.sing)
end
kv_pairs.sing = nil
end
kv_pairs.comma = kv_pairs.comma or config.comma -- for plwiki who want default comma=5
for loc_name, loc_value in pairs(kv_pairs) do
local en_name = text_code.en_option_name[loc_name]
if en_name then
local en_value = text_code.en_option_value[en_name]
if en_value == 'INTEGER' then -- altitude_ft, altitude_m, frac, sigfig
en_value = nil
if loc_value == '' then
add_warning(parms, 2, 'cvt_empty_option', loc_name)
else
local minimum
local number, is_integer = get_number(loc_value)
if en_name == 'sigfig' then
minimum = 1
elseif en_name == 'frac' then
minimum = 2
if number and number < 0 then
parms.opt_fraction_horizontal = true
number = -number
end
else
minimum = -1e6
end
if number and is_integer and number >= minimum then
en_value = number
else
local m
if en_name == 'frac' then
m = 'cvt_bad_frac'
elseif en_name == 'sigfig' then
m = 'cvt_bad_sigfig'
else
m = 'cvt_bad_altitude'
end
add_warning(parms, 1, m, loc_name .. '=' .. loc_value)
end
end
elseif en_value == 'TEXT' then -- $, input, qid, qual, stylein, styleout, tracking
en_value = loc_value ~= '' and loc_value or nil -- accept non-empty user text with no validation
if not en_value and (en_name == '$' or en_name == 'qid' or en_name == 'qual') then
add_warning(parms, 2, 'cvt_empty_option', loc_name)
elseif en_name == '$' then
-- Value should be a single character like "€" for the euro currency symbol, but anything is accepted.
currency_text = (loc_value == 'euro') and '€' or loc_value
elseif en_name == 'input' then
-- May have something like (empty input) if source is an infobox
-- with optional fields. In that case, want to output nothing rather than an error.
parms.input_text = loc_value -- keep input because parms.input is nil if loc_value == ''
end
else
en_value = en_value[loc_value]
if en_value and en_value:sub(-1) == '?' then
en_value = en_value:sub(1, -2)
add_warning(parms, -1, 'cvt_deprecated', loc_name .. '=' .. loc_value)
end
if en_value == nil then
if loc_value == '' then
add_warning(parms, 2, 'cvt_empty_option', loc_name)
else
add_warning(parms, 1, 'cvt_unknown_option', loc_name .. '=' .. loc_value)
end
elseif en_value == '' then
en_value = nil -- an ignored option like adj=off
elseif type(en_value) == 'string' and en_value:sub(1, 4) == 'opt_' then
for _, v in ipairs(split(en_value, ',')) do
local lhs, rhs = v:match('^(.-)=(.+)$')
if rhs then
parms[lhs] = tonumber(rhs) or rhs
else
parms[v] = true
end
end
en_value = nil
end
end
parms[en_name] = en_value
else