Module:Convert/makeunits explained

-- This module generates the wikitext required at Module:Convert/data-- by reading and processing the wikitext of the master list of units-- (see conversion_data for the page title).---- Script method:-- * Read lines, ignoring everything before "

Conversions

".-- * Process the following lines:-- * Find next level-3 heading like "

Length

".-- * Parse each following line starting with "|"-- (but ignore lines starting with "|-" or "|}".-- * Split such lines into fields (delimiter "||") and trim-- leading/trailing whitespace from each field.-- Remove any "colspan" at front of second field (symbol).-- * Remove thousand separators (commas) from the scale field.-- If the scale is a number, do not change it.-- Otherwise, it should be an expression like "5/9", in-- which case it is replaced by the value of the expression.-- * Remove wiki formatting '...' from the link field.-- * Remove redundant fields from the unit to reduce size of data table.-- * Create alternative forms of a unit such as an alias or a combination.-- * Stop processing when encounter end of text or a line starting-- with a level-2 heading ("

" but not "

").-- * Repeat above for each heading listed at prepare_data.-- * Output Lua source for the units table.---- -- Output has the following form.-- local all_units =

local ulower = mw.ustring.lowerlocal usub = mw.ustring.sublocal text_code

local specials =

-- Module text for the local language (localization).-- A default table of text for enwiki is provided here.-- If needed for another wiki, wanted sections from the table can be-- copied into translation_table in Module:Convert/text.-- For example, copying and modifying only the titles section may give:---- local translation_table = local mtext =

local function message(key, ...) -- Return a message from the message table, which can be localized. -- '$1', '$2', ... are replaced with the first, second, ... parameters, -- each of which must be a string or a number. -- The global variable is_test_run can be set by a testing program to -- check the messages generated by this program. local rep = for i, v in ipairs do rep['$' .. i] = v end key = key or '???' local extra if is_test_run and key ~= 'm_line_num' then extra = key .. ': ' else extra = end return extra .. string.gsub(mtext.messages[key] or key, '$%d+', rep)end

local function quit(key, ...) -- Use error to pass an error message to the surrounding pcall. error(message(key, ...), 0)end

local function quit_no_message -- Throw an error. -- This is used in some functions which can throw an error with a message, -- but where the message is in fact never displayed because the calling -- function uses pcall to catch errors, and any message is ignored. -- Using this function documents that the message (which may be useful in -- some other application) does not need translation as it never appears. error('this message is not displayed', 0)end

local function collection -- Return a table to hold items. return end

local warnings = collectionlocal function add_warning(key, ...) -- Add a warning that will be inserted before the final result. warnings:add(message(key, ...))end

---Begin code to evaluate expressions------------------------------------- This is needed because Lua's loadstring is not available in Scribunto,-- and each scale value can be specifed as an expression such as "5/9".-- More complex expressions are supported, including use of parentheses-- and the binary operators: + - * / ^

local operators =

local function tokenizer(text) -- Function 'next' returns the next token which is one of: -- number -- table (operator) -- string ('(' or ')') -- nil (end of text) -- If invalid, an error is thrown. -- The number is unsigned (unary operators are not supported). return end

local function evaluate_tokens(tokens, inparens) -- Return the value from evaluating tokenized expression, or throw an error. local numstack, opstack = collection, collection local function perform_ops(precedence, associativity) while opstack.n > 0 and (opstack[opstack.n].precedence > precedence or (opstack[opstack.n].precedence

precedence and associativity

1)) do local rhs = numstack:pop local lhs = numstack:pop if not (rhs and lhs) then quit_no_message('missing number') end local op = opstack:pop numstack:add(op.func(lhs, rhs)) end end local token_last local function set_state(token_type) if token_last

token_type then local missing = (token_type

'number') and 'operator' or 'number' quit_no_message('missing ' .. missing) end token_last = token_type end while true do local token = tokens:next if type(token)

'number' then set_state('number') numstack:add(token) elseif type(token)

'table' then set_state('operator') perform_ops(token.precedence, token.associativity) opstack:add(token) elseif token

'(' then set_state('number') numstack:add(evaluate_tokens(tokens, true)) elseif token

')' then if inparens then break end quit_no_message('unbalanced parentheses') else break end end perform_ops(0) if numstack.n > 1 then quit_no_message('missing operator') end if numstack.n < 1 then quit_no_message('missing number') end return numstack:popend

local function evaluate(expression) -- Return value (a number) from evaluating expression (a string), -- or throw an error if invalid. -- This is not bullet proof, but it should support the expressions used. return evaluate_tokens(tokenizer(expression))end---End code to evaluate expressions----------------------------------------Begin code adapted from Module:Convert-------------------------------

local plural_suffix = 's' -- may be changed from translation.plural_suffix below

local function shallow_copy(t) -- Return a shallow copy of t. -- Do not need the features and overhead of mw.clone provided by Scribunto. local result = for k, v in pairs(t) do result[k] = v end return resultend

local function split(text, delimiter) -- Return a numbered table with fields from splitting text. -- The delimiter is used in a regex without escaping (for example, '.' would fail). -- Each field has any leading/trailing whitespace removed. local t = text = text .. delimiter -- to get last item for item in text:gmatch('%s*(.-)%s*' .. delimiter) do table.insert(t, item) end return tend

local unit_mt =

local function prefixed_name(unit, name, index) -- Return unit name with SI prefix inserted at correct position. -- index = 1 (name1), 2 (name2), 3 (name1_us), 4 (name2_us). -- The position is a byte (not character) index, so use Lua's sub. local pos = rawget(unit, 'prefix_position') if type(pos)

'string' then pos = tonumber(split(pos, ',')[index]) end if pos then return name:sub(1, pos - 1) .. unit.si_name .. name:sub(pos) end return unit.si_name .. nameend

local unit_prefixed_mt =

local function lookup(units, unitcode, sp, what) -- Return a copy of the unit if found, or return nil. -- In this cut-down code, sp is always nil, and what is ignored. local t = units[unitcode] if t then if t.shouldbe then return nil end local result = shallow_copy(t) if result.prefixes then result.si_name = result.si_prefix = return setmetatable(result, unit_prefixed_mt) end return setmetatable(result, unit_mt) end local SIprefixes = text_code.SIprefixes for plen = SIprefixes[1] or 2, 1, -1 do -- Look for an SI prefix; should never occur with an alias. -- Check for longer prefix first ('dam' is decametre). -- SIprefixes[1] = prefix maximum #characters (as seen by mw.ustring.sub). local prefix = usub(unitcode, 1, plen) local si = SIprefixes[prefix] if si then local t = units[usub(unitcode, plen+1)] if t and t.prefixes then local result = shallow_copy(t) if (sp

'us' or t.sp_us) and si.name_us then result.si_name = si.name_us else result.si_name = si.name end result.si_prefix = si.prefix or prefix -- In this script, each scale is a string. result.scale = tostring(tonumber(t.scale) * 10 ^ (si.exponent * t.prefixes)) result.prefixes = nil -- a prefixed unit does not take more prefixes (in this script, the returned unit may be added to the list of units) return setmetatable(result, unit_prefixed_mt) end end end local exponent, baseunit = unitcode:match('^e(%d+)(.*)') if exponent then local engscale = text_code.eng_scales[exponent] if engscale then local result = lookup(units, baseunit, sp, 'no_combination') if not result then return nil end if not (result.offset or result.builtin or result.engscale) then result.defkey = unitcode -- key to lookup default exception result.engscale = engscale -- Do not set result.scale as this code is called for units where that is not set. return result end end end return nilend

local function evaluate_condition(value, condition) -- Return true or false from applying a conditional expression to value, -- or throw an error if invalid. -- A very limited set of expressions is supported: -- v < 9 -- v * 9 < 9 -- where -- 'v' is replaced with value -- 9 is any number (as defined by Lua tonumber) -- '<' can also be '<=' or '>' or '>=' -- In addition, the following form is supported: -- LHS and RHS -- where -- LHS, RHS = any of above expressions. local function compare(value, text) local arithop, factor, compop, limit = text:match('^%s*v%s*([*]?)(.-)([<>]=?)(.*)$') if arithop

nil then quit_no_message('Invalid default expression.') elseif arithop

'*' then factor = tonumber(factor) if factor

nil then quit_no_message('Invalid default expression.') end value = value * factor end limit = tonumber(limit) if limit

nil then quit_no_message('Invalid default expression.') end if compop

'<' then return value < limit elseif compop

'<=' then return value <= limit elseif compop

'>' then return value > limit elseif compop

'>=' then return value >= limit end quit_no_message('Invalid default expression.') -- should not occur end local lhs, rhs = condition:match('^(.-%W)and(%W.*)') if lhs

nil then return compare(value, condition) end return compare(value, lhs) and compare(value, rhs)end

---End adapted code-----------------------------------------------------

local function strip(text) -- Return text with no leading/trailing whitespace. return text:match("^%s*(.-)%s*$")end

local function empty(text) -- Return true if text is nil or empty (assuming a string). return text

nil or text

end

-- Tables of units: k = unit code, v = unit table.local units_index = -- all units: normal, alias, per, combination, or multiplelocal alias_index = -- all aliases (to detect attempts to define more than once)local per_index = -- all "per" units (to detect attempts to define more than once)

local function get_unit(ucode, utype) -- Look up unit code in our cache of units. -- If utype

nil, the unit should already have been defined. -- Otherwise, ucode may represent an automatically generated combination -- where each component must have the given utype; a dummy unit is returned. if empty(ucode) then return nil end local unit = lookup(units_index, ucode) if unit or not utype then return unit end local combo = collection if ucode:find('+', 1, true) then for item in (ucode .. '+'):gmatch('%s*(.-)%s*%+') do if item ~= then combo:add(item) end end elseif ucode:find('%s') then for item in ucode:gmatch('%S+') do combo:add(item) end end if combo.n > 1 then local result = setmetatable for _, v in ipairs(combo) do local component = lookup(units_index, v) if not component or component.shouldbe or component.combination then return nil end if utype ~= component.utype then result.utype = component.utype -- set wrong type which caller will detect break end end return result endend

local overrides = -- read from input for unit codes that should not be checked for a duplicate

local function insert_unique_unit(data, unit, index) -- After inserting any required built-in data, insert the unit into the -- data table and (if index not nil) add to index, -- but not if the unit code is already defined. local ucode = unit.unitcode local known = get_unit(ucode) if known and not overrides[ucode] then quit('m_dup_code', ucode) end for item, t in pairs(specials.ucode) do unit[item] = t[ucode] end if index then index[ucode] = unit end table.insert(data, unit)end

local function check_condition(condition) -- Return true if condition appears to be valid; otherwise return false. for _, value in ipairs do local success, result = pcall(evaluate_condition, value, condition) if not success then return false end end return trueend

local function check_default_expression(default, ucode) -- Return a numbered table of names present in param default -- (two names if an expression, or one name (param default) otherwise). -- Throw an error if a problem occurs. -- An expression uses pipe-delimited fields with 'v' representing -- the input value for the conversion. -- Example (suffix is optional): 'v < 120 ! small ! big ! suffix' -- returns . if not default:find('!', 1, true) then return end local t = for item in (default .. '!'):gmatch('%s*(.-)%s*!') do t[#t+1] = item -- split on '!', removing leading/trailing whitespace end if not (#t

3 or #t

4) then quit('m_def_fmt', default, ucode) end local condition, default1, default2 = t[1], t[2], t[3] if #t

4 then default1 = default1 .. t[4] default2 = default2 .. t[4] end if not check_condition(condition) then quit('m_def_cond', default, ucode) end return end

local function check_default(default, ucode, utype, unit_table) -- Check the given name (or expression) of a default output. -- Normally a unit must not define itself as its default. However, -- some units are defined merely for use in per units, and they have -- the same ucode, utype and default. -- Example: unit cent which cannot be converted to anything other than -- a cent, but which can work, for example, in cent/km and cent/mi. -- Throw an error if a problem occurs. local done = for _, default in ipairs(check_default_expression(default, ucode)) do if done[default] then quit('m_def_rpt', default, ucode) end if default

ucode and ucode ~= utype then quit('m_def_same', ucode) end local default_table = get_unit(default, utype) if not default_table then quit('m_def_undef', default, ucode) end if not (utype

unit_table.utype and utype

default_table.utype) then quit('m_def_type', default, ucode) end done[default] = true endend

local function check_all_defaults(cfg, units) -- Check each default in units and warn if needed. -- This is done after all input data has been processed. -- Throw an error if a problem occurs. local errors = collection local missing = collection -- unitcodes with missing defaults for _, unit in ipairs(units) do if not unit.shouldbe and not unit.combination then -- This is a standard unit or an alias/per (not shouldbe, combo). -- An alias may have a default defined, but it is optional. local default = unit.default local ucode = unit.unitcode if empty(default) then if not unit.target then -- unit should have a default missing:add(ucode) end else local ok, msg = pcall(check_default, default, ucode, unit.utype, unit) if not ok then errors:add(msg) if errors.n >= cfg.maxerrors then break end end end end end if errors.n > 0 then error(errors:join, 0) end if missing.n > 0 then add_warning('m_wrn_nodef') local limit = cfg.maxerrors for _, v in ipairs(missing) do limit = limit - 1 if limit < 0 then add_warning('m_wrn_more') break end add_warning('m_wrn_ucode', v) end endend

local function check_all_pers(cfg, units) -- Check each component of each "per" unit and warn if needed. -- In addition, add any required extra fields for some types of units. -- This is done after all input data has been processed. -- Throw an error if a problem occurs. local errors = collection local function errmsg(key, ...) errors:add(message(key, ...)) end for _, unit in ipairs(units) do local per = unit.per if per then local ucode = unit.unitcode if #per ~= 2 then errmsg('m_per_two', ucode) else local types = for i, v in ipairs(per) do if empty(v) then errmsg('m_per_empty', ucode) end if not text_code.currency[v] then local t = get_unit(v) if t then types[i] = t.utype else errmsg('m_per_undef', ucode, v) end end end if specials.utype[unit.utype]

'type_fuel_efficiency' then local expected = local top_type = expected[specials.utype[types[1]]] local bot_type = expected[specials.utype[types[2]]] if top_type and bot_type and top_type ~= bot_type then unit.iscomplex = true if top_type

1 then unit.invert = 1 else unit.invert = -1 end else errmsg('m_per_fuel', ucode) end end end end if errors.n >= cfg.maxerrors then break end end if errors.n > 0 then error(errors:join, 0) endend

local function update_units(units, composites, varnames) -- Update some unit definitions with extra data defined in other sections. -- This is done after all input data has been processed. for _, unit in ipairs(units) do local comp = composites[unit.unitcode] if comp then unit.subdivs = '' end local vn = varnames[unit.unitcode] if vn then unit.varname = vn end endend

local function make_override(cfg, data) -- Return a function which, when called, stores a unit code that is not to be -- checked for a duplicate. The table is stored in data (also a table). return function (utype, fields) local ucode = fields[1] if empty(ucode) then quit('m_ovr_miss') end if data[ucode] then quit('m_ovr_dup', ucode) end data[ucode] = true endend

local function make_default(cfg, data) -- Return a function which, when called, stores a table that defines a -- default output unit. The table is stored in data (also a table). local defaults_index = -- to detect attempts to define a default twice return function (utype, fields) -- Store a table defining a unit. -- This is for a unit such as 'kg' that has a default output unit -- different from what is defined for the base unit ('g'). -- Throw an error if a problem occurs. local ucode = fields[1] local default = fields[2] if empty(ucode) then quit('m_dfs_code') end if empty(default) then quit('m_dfs_none', ucode) end if #fields ~= 2 then quit('m_dfs_two', ucode) end local unit_table = get_unit(ucode) if not unit_table then quit('m_dfs_undef', ucode) end local symbol = unit_table.defkey or unit_table.symbol if empty(symbol) then quit('m_dfs_sym', ucode) end check_default(default, ucode, utype, unit_table) if defaults_index[ucode] then quit('m_dfs_dup', ucode) end defaults_index[ucode] = default table.insert(data,) endend

local function clean_link(link, name) -- Return link, customary where: -- link = given link after removing any '...' wiki formatting -- and removing any leading '+' or '*' or '@'; -- customary = 1 if leading '+', or 2 if '*' or 3 if '@', or nil -- (for extra "US" or "U.S." or "Imperial" customary units link). -- Result has leading/trailing whitespace removed, and is nil if empty -- or if link matches the name, if a name is specified. -- Exception: If the link is empty and the name starts with '2|ft|6|in'. -- The target units must be defined first. -- Throw an error if a problem occurs. local unitcode -- dummy code required for simplicity, but which is not used in output local alternate_code -- an alternative unit code can be specified to replace convert input local fixed_name -- a fixed name can be specified to replace the unit's normal symbol/name local default_code local ucodes, scales =, for i, v in ipairs(fields) do -- 1=composite, 2=ucode1, 3=ucode2, 4=default, 5=alternate, 6=name if i

1 then if v

then quit('m_cmp_miss') end unitcode = v elseif 2 <= i and i <= 5 then if not (i

5 and v

) then local target = get_unit(v, (i

4) and utype or nil) -- the default may be an auto combination if not target then quit('m_cmp_undef', v, unitcode) end if target.utype ~= utype then quit('m_cmp_type', v, unitcode) end if i < 4 then if not target.scale then quit('m_mul_std', v, unitcode) end table.insert(ucodes, v) table.insert(scales, target.scale) elseif i

4 then default_code = v else if scales[#scales] ~= target.scale then quit('m_cmp_scale', v, unitcode) end alternate_code = v end end elseif i

6 then if v ~= then fixed_name = v end else quit('m_cmp_many', unitcode) end end if #ucodes ~= 2 then quit('m_cmp_two', unitcode) end if not default_code then quit('m_cmp_def', unitcode) end -- Component units must be specified from most-significant to least-significant, -- and each ratio of a pair of scales must be very close to an integer. -- Currently, there will be exactly two scales and one ratio. local ratios, count =, #scales for i = 1, count do local scale = tonumber(scales[i]) if scale

nil or scale <= 0 then quit('m_cmp_inval', unitcode, scales[i]) end scales[i] = scale end for i = 1, count - 1 do local ratio = scales[i] / scales[i + 1] local rounded = math.floor(ratio + 0.5) if rounded < 2 then quit('m_cmp_order', unitcode) end if math.abs(ratio - rounded)/ratio > 1e-6 then quit('m_cmp_int', unitcode) end ratios[i] = rounded end local text = local function add_text(key, value) table.insert(text, string.format('%s = %q', key, value)) end if default_code then add_text('default', default_code) end if alternate_code then add_text('unit', alternate_code) end if fixed_name then add_text('name', fixed_name) end local subdiv = string.format('["%s"] = ', ucodes[2], table.concat(text, ', ')) local main_code = ucodes[1] local item = data[main_code] if item then table.insert(item.subdivs, subdiv) else data[main_code] = end endend

local function make_outputmultiple(cfg, data) -- Return a function which, when called, stores a table that defines a -- single multiple output unit. The table is stored in data (also a table). return function (utype, fields) -- Store a table defining a unit. -- This is for a multiple unit like 'ydftin' (result in yards, feet, inches). -- The target units must be defined first. -- Throw an error if a problem occurs. local unit = local ucodes, scales =, for i, v in ipairs(fields) do if i

1 then -- unitcode if v

then quit('m_mul_miss') end unit.unitcode = v elseif v

then -- Ignore empty fields. else local target = get_unit(v) if not target then quit('m_mul_undef', v, unit.unitcode) end if target.utype ~= utype then quit('m_mul_type', v, unit.unitcode) end if not target.scale then quit('m_mul_std', v, unit.unitcode) end table.insert(ucodes, v) table.insert(scales, target.scale) end end if #ucodes < 2 then quit(#ucodes

0 and 'm_mul_none' or 'm_mul_one', unit.unitcode) end -- Component units must be specified from most-significant to least-significant -- (so scale values will be in descending order), -- and each ratio of a pair of scales must be very close to an integer. -- The componenets and ratios are stored in reverse order (least significant first). -- This script stores a unit scale as a string (might be an expression like "5/9"), -- but scales in a multiple are handled as numbers (should never be expressions). local ratios, count =, #scales for i = 1, count do local scale = tonumber(scales[i]) if scale

nil or scale <= 0 then quit('m_mul_scale', unit.unitcode, scales[i]) end scales[i] = scale end for i = 1, count - 1 do local ratio = scales[i] / scales[i + 1] local rounded = math.floor(ratio + 0.5) if rounded < 2 then quit('m_mul_order', unit.unitcode) end if math.abs(ratio - rounded)/ratio > 1e-6 then quit('m_mul_int', unit.unitcode) end ratios[i] = rounded end unit.combination = reversed(ucodes) unit.multiple = reversed(ratios) insert_unique_unit(data, unit, units_index) endend

-- To make updating the data module easier, this script inserts a preamble-- and a postamble so the result can be used to replace the whole page.local data_preamble = [=[ -- Conversion data used by [[Module:Convert]] which uses mw.loadData for-- read-only access to this module so that it is loaded only once per page.-- See if copying to another wiki.---- These data tables follow:-- all_units all properties for a unit, including default output-- default_exceptions exceptions for default output ('kg' and 'g' have different defaults)-- link_exceptions exceptions for links ('kg' and 'g' have different links)---- These tables are generated by a script which reads the wikitext of a page that-- documents the required properties of each unit; see .]=]

local data_postamble = [=[ return { all_units = all_units, default_exceptions = default_exceptions, link_exceptions = link_exceptions, per_unit_fixups = per_unit_fixups, }]=]

local out_unit_prefix = |". -- Each item has leading/trailing whitespace removed, and any encoded pipe -- characters are decoded. -- The second field (for symbol when processing units) is adjusted to -- remove any "colspan" at the front of lines like: -- "| unitcode || colspan="11" | !Text to display for an error message". local t = line = line .. "||" -- to get last field for item in line:gmatch("%s*(.-)%s*||") do table.insert(t, (item:gsub('|', '|'))) end if t[2] then local cleaned = t[2]:match('^%s*colspan%s*=.-|%s*(.*)$') if cleaned then t[2] = cleaned end end return tend

local function prepare_section(cfg, maker, lines, section, need_section, need_utype) -- Process the first level-two section with the given section name -- in the given table of lines of wikitext. -- If successful, maker inserts each item into a table. -- Otherwise, an error is thrown. local skip = true local errors = collection local utype -- unit type (from level-three heading) local nbsp = '\194\160' -- nonbreaking space is utf-8 encoded as hex c2 a0 for linenumber, line in ipairs(lines) do if skip then -- Skip down to and including the starting heading. local level, heading = extract_heading(line) if level

2 and heading

section then skip = false end else -- Accummulate unit definitions. local c1 = line:sub(1, 1) local c2 = line:sub(2, 2) if c1

'|' and not (c2

'-' or c2

'}') then if need_utype and empty(utype) then quit('m_hdg_lev3', line) end if line:find(nbsp, 1, true) then -- For example, "acre ft" does not work if it contains nbsp. add_warning('m_wrn_nbsp', linenumber) end local ok, msg = pcall(maker, utype, fields(line:sub(2))) if not ok then if msg:sub(-1)

'.' then msg = msg:sub(1, -2) end errors:add(msg .. message('m_line_num', linenumber)) if errors.n >= cfg.maxerrors then break end end else local level, heading = extract_heading(line) if level

3 then utype = ulower(heading) elseif level

2 then break end end end end if skip and need_section then quit('m_hdg_lev2', section) end if errors.n > 0 then error(errors:join, 0) endend

local function get_page_lines(page_title) -- Read the wikitext of the page at the given title; split the text into -- lines with leading and trailing space removed from each line. -- Return a numbered table of the lines, or throw an error. if empty(page_title) then quit('m_no_title') end local t = mw.title.new(page_title) if t then local content = t:getContent if content then if content:sub(-1) ~= '\n' then content = content .. '\n' end local lines = collection for line in string.gmatch(content, '[\t ]*(.-)[\t\r ]*\n') do lines:add(line) end return lines end end quit('m_ftl_read', page_title)end

local function prepare_data(cfg, is_sandbox) -- Read the page of conversion data, and process the wikitext -- in the sections with wanted level-two headings. -- Return units, defaults, links (three tables). -- Throw an error if a problem occurs. local composites, defaults, links, units, perunits, varnames =,,,,, local sections = local lines = get_page_lines(cfg.data_title) for _, section in ipairs(sections) do local heading = mtext.section_names[section[1]] local maker = section[2](cfg, section[3]) local code = section[4] local need_section, need_utype if code

0 and not is_sandbox then need_section = true end if code

0 then need_utype = true end prepare_section(cfg, maker, lines, heading, need_section, need_utype) end check_all_defaults(cfg, units) check_all_pers(cfg, units) update_units(units, composites, varnames) return units, defaults, links, perunitsend

local function _makeunits(cfg, results) -- Read the wikitext for the conversion data. -- Append output to given results collection, or throw error if a problem. text_code = require(cfg.text_title) for _, name in ipairs do if type(text_code[name]) ~= 'table' then quit('m_ftl_table', cfg.text_title, name) end end local translation = text_code.translation_table if translation then if translation.plural_suffix then plural_suffix = translation.plural_suffix end local ts = translation.specials if ts then if ts.utype then specials.utype = ts.utype end if ts.ucode then specials.ucode = ts.ucode end end local tm = translation.mtext if tm then if tm.section_names then mtext.section_names = tm.section_names end if tm.titles then mtext.titles = tm.titles end if tm.messages then mtext.messages = tm.messages end end end local is_sandbox local conversion_data_title = mtext.titles.conversion_data if cfg.data_title and cfg.data_title ~= conversion_data_title then if is_test_run then is_sandbox = true data_preamble = nil data_postamble = nil out_unit_prefix = 'local all_units = ' out_default_prefix = '\nlocal default_exceptions = ' out_default_item = '\t["{symbol}"] = "",' out_link_prefix = '\nlocal link_exceptions = ' out_link_item = '\t["{symbol}"] = "",' out_perunit_prefix = '\nlocal per_unit_fixups = ' out_perunit_item = '\t["{lhs}"] =,' end else cfg.data_title = conversion_data_title end local units, defaults, links, perunits = prepare_data(cfg, is_sandbox) if data_preamble then results:add(data_preamble) end results:add(out_unit_prefix) for _, unit in ipairs(units) do local spec if unit.target then spec = alias_specification elseif unit.per then spec = per_specification unit.per = numbered_table_as_string(unit.per, unit) elseif unit.shouldbe then spec = shouldbe_specification elseif unit.combination then spec = combination_specification unit.combination = numbered_table_as_string(unit.combination, unit) if unit.multiple then unit.multiple = numbered_table_as_string(unit.multiple, unit) end else spec = unit_specification end add_unit_lines(results, unit, spec) end results:add(out_unit_suffix) for _, t in ipairs do local data, prefix, item, suffix = t[1], t[2], t[3], t[4] if #data > 0 or not is_sandbox then results:add(prefix) for _, unit in ipairs(data) do results:add((item:gsub('', unit))) end results:add(suffix) end end if data_postamble then results:add(data_postamble) endend

local function makeunits(frame) local args = frame.args local config = local results = collection local ok, msg = pcall(_makeunits, config, results) if not ok then results:add(message('m_error')) results:add() results:add(msg) end local warn = if warnings.n > 0 then warn = message('m_warning') .. '\n\n' .. warnings:join .. '\n\n' end -- Pre tags returned by a module are html tags, not like wikitext

...
. -- The following renders the text as is, and preserves tab characters. return '
\n' .. mw.text.nowiki(warn .. results:join) .. '\n
\n'end

return