Difference between revisions of "Module:Track gauge"

From blackwiki
Jump to navigation Jump to search
blackwiki>DePiep
blackwiki>DePiep
(Add auto gauge documentation, and maintenance categories. See Template_talk:RailGauge#Code_changes_2_May_2014)
Line 1: Line 1:
 
-- This module implements the {{RailGauge}} template.
 
-- This module implements the {{RailGauge}} template.
 +
-- Input can be predefined aliases (see the /data module page)
 +
local p = {}
 +
local gaugeDataAll = nil
 +
local dataPageName = 'Module:RailGauge/data'
 +
 +
-----------------------------------------------------------------------------------
 +
-- formatUnitPlaintext
 +
-- Pattern '00016.5 mm' for tablesort and catsort. Can be wrapped in class="sortkey"
 +
-----------------------------------------------------------------------------------
 +
local function formatUnitPlaintext( rgData, unit, fmtZeroPadding, toFracChar )
 +
    -- Returns plaintext (ASCII) only. No css.
 +
    if rgData == nil then
 +
        return ''
 +
    end
 +
    if ( unit or rgData.def ) == 'imp' then
 +
        local ft = ''
 +
        local inch = ''
 +
        local frac = ''
 +
        if rgData.ft then
 +
            ft = rgData.ft .. ' ft'
 +
        end
 +
        if rgData.num then
 +
            frac = ' ' .. rgData.num .. '/' .. rgData.den
 +
            if toFracChar then
 +
                -- as used in contentCat pagenames
 +
                if frac == ' 1/8' then
 +
                    frac = '⅛'
 +
                elseif frac == ' 1/4' then
 +
                    frac = '¼'
 +
                elseif frac == ' 3/8' then
 +
                    frac = '⅜'
 +
                elseif frac == ' 1/2' then
 +
                    frac = '½'
 +
                elseif frac == ' 3/4' then
 +
                    frac = '¾'
 +
                elseif frac == ' 7/8' then
 +
                    frac = '⅞'
 +
                else
 +
                    frac = frac .. ' (error: fraction character missing in module:RailGauge)'
 +
                end
 +
            end
 +
            if rgData["in"] then
 +
                frac = ' ' .. rgData["in"] .. frac .. ' in'
 +
            else
 +
                frac = frac .. ' in'
 +
            end
 +
        else
 +
            if rgData["in"] then
 +
                inch = ' ' .. rgData["in"] .. ' in'
 +
            end
 +
        end
 +
        return mw.text.trim( ft .. inch .. frac )
 +
    else
 +
        -- metric ( mm )
 +
        if fmtZeroPadding == nil or tonumber( fmtZeroPadding ) <= 0 then
 +
            return rgData.id .. ' mm'
 +
        else
 +
            return string.rep( '0',
 +
                  fmtZeroPadding - string.len( math.floor( tonumber( rgData.id ) ) ) )
 +
                  .. rgData.id .. ' mm'
 +
        end
 +
    end
 +
end
 +
 +
-----------------------------------------------------------------------------------
 +
-- anchor -- Anchor text *here* is: <span id="1000 mm">; anchor *there* is: #1000 mm.
 +
-----------------------------------------------------------------------------------
 +
local function anchor( rgEntry, unit, herethere )
 +
    local html = require( 'Module:HtmlBuilder' )
 +
    if rgEntry == nil then
 +
        return ''
 +
    end
 +
    unit = unit or rgEntry.def1
 +
    local anch = formatUnitPlaintext( rgEntry,unit, 0 )
 +
    if herethere == 'there' then -- Untested, April 2014
 +
        anch = '#' .. anch
 +
    else
 +
        anch = html.create().tag( 'span' ).attr( 'id', anch )
 +
    end
 +
    return tostring( anch )
 +
end
 +
 +
-----------------------------------------------------------------------------------
 +
-- catMentionsGaugeSize -- category name for "article mentions gauge" categories
 +
-----------------------------------------------------------------------------------
 +
local function catMentionsGaugeSize( mmSize, catSort, pagename, show, plaintext )
 +
    local cat -- hereo
 +
    -- Category page name. mmSize has format of id ( like '16.5' ).
 +
    cat = 'Category:Articles that mention rail gauge size' .. ' ' .. mmSize .. '&nbsp;mm'
 +
    if plaintext then
 +
        return cat
 +
    elseif show then
 +
        -- uses colon and wikilabel; no catsort
 +
        local label = '|cat:mentionings'
 +
        cat = '[[:'.. cat .. label .. ']]'
 +
    else
 +
        if ( catSort or '' ) ~= '' then
 +
            catSort = '|' .. catSort
 +
        else
 +
            catSort = ''
 +
        end
 +
        cat = '[['.. cat .. catSort .. ']]'
 +
    end
 +
    return cat
 +
end
 +
 +
-----------------------------------------------------------------------------------
 +
-- catMentionsGaugeSizeParent -- Parent of all "mentions" categories
 +
-----------------------------------------------------------------------------------
 +
local function catMentionsGaugeSizeParent( show )
 +
    local cat
 +
    -- Category page name
 +
    cat = 'Category:Articles that mention a specific rail gauge'
 +
    if show then
 +
        -- uses colon and wikilabel
 +
        cat = '[[:'.. cat .. '|Cat:Articles mentioning RailGauge]]'
 +
    end
 +
    return cat
 +
end
 +
 +
-----------------------------------------------------------------------------------
 +
-- prepareArgs -- Arguments coming from an #invoke or from a module
 +
-----------------------------------------------------------------------------------
 +
local function prepareArgs( frame )
 +
    -- If called via #invoke, use the args passed into the invoking
 +
    -- template, or the args passed to #invoke if any exist. Otherwise
 +
    -- assume args are being passed directly in from the debug console
 +
    -- or from another Lua module.
 +
    local origArgs
 +
    if frame == mw.getCurrentFrame() then
 +
        origArgs = frame:getParent().args
 +
        for k, v in pairs( frame.args ) do
 +
            origArgs = frame.args
 +
            break
 +
        end
 +
    else
 +
        origArgs = frame
 +
    end
  
local p = {}
+
    -- Trim whitespace, make lower-case and remove blank arguments for all arguments
 +
    -- searchAlias is the cleaned value of [1]. [1] is kept as rawInput for error message
 +
    local args = {}
 +
    args["searchAlias"] = ''
 +
    args["rawInput"] = origArgs[ 1 ] or ''
 +
 
 +
    for k, v in pairs( origArgs ) do
 +
        v = mw.text.trim( v )
 +
        if tonumber( k ) == nil then
 +
            -- Named argment
 +
            args[ k ] = mw.ustring.lower( v )
 +
        else
 +
            -- Unnamed argument, alias to be searched
 +
            -- Into lowercase, remove all whitespace, commas to create the search key
 +
            v = mw.ustring.lower( mw.ustring.gsub( v, '[%s%,]', '' ) )
 +
            args[ k ] = v
 +
            if k == 1 then
 +
                args["searchAlias"] = v
 +
            end
 +
        end
 +
    end
 +
 
 +
    return args
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- maintReportArgs -- debug feedback, convenience.
 +
-----------------------------------------------------------------------------------
 +
local function maintReportArgs()
 +
    local args = prepareArgs( frame )
 +
    local report = 'args: '
 +
    for k, v in pairs( args ) do
 +
        report = report .. k .. '='.. v .. '; '
 +
    end
 +
    return '<tt>' .. report .. '</tt><br>'
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- getGaugeDataSet -- Find entry data for a single gauge
 +
-----------------------------------------------------------------------------------
 +
local function getGaugeDataSet( searchAlias )
 +
    if gaugeDataAll == nil then
 +
        gaugeDataAll = mw.loadData( dataPageName )
 +
    end
 +
    local rgEntry = nil
 +
    for i, rgEntry in ipairs( gaugeDataAll ) do
 +
        for j, alias in ipairs( rgEntry.aliases ) do
 +
            if alias == searchAlias then
 +
                return rgEntry
 +
            end
 +
        end
 +
    end
 +
end
  
-- Adds span tags to prevent a string from wrapping.
+
-----------------------------------------------------------------------------------
 +
-- noWrap -- Add span tags to prevent a string from wrapping.
 +
-----------------------------------------------------------------------------------
 
local function noWrap( s )
 
local function noWrap( s )
 
     return mw.ustring.format( '<span class="nowrap">%s</span>', s )
 
     return mw.ustring.format( '<span class="nowrap">%s</span>', s )
 
end
 
end
  
-- A slimmed-down version of the {{frac}} template. (A nowrap be added that includes the unit)
+
-----------------------------------------------------------------------------------
 +
-- frac -- A slimmed-down version of the {{frac}} template ( a nowrap to be added with the unit )
 +
-----------------------------------------------------------------------------------
 
local function frac( whole, num, den )
 
local function frac( whole, num, den )
 
     return mw.ustring.format(
 
     return mw.ustring.format(
 
         '<span class="frac">%s%s<sup>%s</sup>&frasl;<sub>%s</sub></span>',
 
         '<span class="frac">%s%s<sup>%s</sup>&frasl;<sub>%s</sub></span>',
 
         whole or '', whole and '<span class="visualhide">&nbsp;</span>' or '', num, den
 
         whole or '', whole and '<span class="visualhide">&nbsp;</span>' or '', num, den
    )
+
        )
 
end
 
end
  
-- Formats imperial measurements. Originally same functionality as {{RailGauge/format imp}}.
+
-----------------------------------------------------------------------------------
local function formatImp( data, ulink, articleLink, pageName )
+
-- formatImp -- Formats imperial units size into a single text element
 +
-----------------------------------------------------------------------------------
 +
local function formatImp( rgData, ulink, articleLink, pageName )
 
     local ret = {}
 
     local ret = {}
     local ft = data.ft
+
     local ft = rgData.ft
 +
 
 
     if ft then
 
     if ft then
 
         local ftlink = ulink and not articleLink and '[[Foot (unit)|ft]]' or 'ft'
 
         local ftlink = ulink and not articleLink and '[[Foot (unit)|ft]]' or 'ft'
 
         table.insert( ret, mw.ustring.format( '%s&nbsp;%s', ft, ftlink ) )
 
         table.insert( ret, mw.ustring.format( '%s&nbsp;%s', ft, ftlink ) )
 
     end
 
     end
     local inches = data['in']
+
     local inches = rgData['in']
     local num = data.num
+
     local num = rgData.num
     local den = data.den
+
     local den = rgData.den
 
     if inches and not num and not den then
 
     if inches and not num and not den then
 
         table.insert( ret, inches )
 
         table.insert( ret, inches )
Line 37: Line 234:
 
     end
 
     end
 
     local gaugeSize = noWrap( table.concat( ret, '&nbsp;' ) )
 
     local gaugeSize = noWrap( table.concat( ret, '&nbsp;' ) )
 +
 
     if articleLink then
 
     if articleLink then
    return '[[' .. pageName .. '|' .. gaugeSize .. ']]'
+
        return '[[' .. pageName .. '|' .. gaugeSize .. ']]'
 
     else
 
     else
 
         return gaugeSize
 
         return gaugeSize
Line 44: Line 242:
 
end
 
end
  
-- Formats metric measurements. Originally same functionality as {{RailGauge/format met}}
+
-----------------------------------------------------------------------------------
local function formatMet( data, ulink, articleLink, pageName )
+
-- formatMet -- Formats metric measurements into a single formatted element
     local m = data.m
+
-----------------------------------------------------------------------------------
 +
local function formatMet( rgData, ulink, articleLink, pageName )
 +
     local m = rgData.m
 
     local gaugeSize
 
     local gaugeSize
 +
 
     if m then
 
     if m then
         local munit = link and not articleLink and '[[metre|m]]' or 'm'
+
         local munit = ulink and not articleLink and '[[metre|m]]' or 'm'
 
         gaugeSize = noWrap( mw.ustring.format( '%s&nbsp;%s', m, munit ) )
 
         gaugeSize = noWrap( mw.ustring.format( '%s&nbsp;%s', m, munit ) )
 
     else
 
     else
         local mm = data.mm
+
         local mm = rgData.mm
 
         mm = tonumber( mm )
 
         mm = tonumber( mm )
 
         if mm then
 
         if mm then
Line 60: Line 261:
 
         gaugeSize = noWrap( mw.ustring.format( '%s&nbsp;%s', mm, mmunit ) )
 
         gaugeSize = noWrap( mw.ustring.format( '%s&nbsp;%s', mm, mmunit ) )
 
     end
 
     end
 +
 
     if articleLink then
 
     if articleLink then
 
         return '[[' .. pageName .. '|' .. gaugeSize .. ']]'
 
         return '[[' .. pageName .. '|' .. gaugeSize .. ']]'
Line 67: Line 269:
 
end
 
end
  
-- Composes the initial output from the gauge data taken from [[Module:RailGauge/data]].
+
-----------------------------------------------------------------------------------
-- Same functionality as {{RailGauge/compose}}.
+
-- compose -- Puts together the two metric, imperial measures into an output string.
local function compose( args, data )
+
-----------------------------------------------------------------------------------
 +
local function compose( args, rgData )
 +
    local definition = rgData.def1
 +
    local pageName = rgData.pagename or nil
 +
    local imp = formatImp( rgData, args.unitlink == 'on',
 +
        args.lk=='on' and definition=='imp' and pageName, pageName )
 +
    local met = formatMet( rgData, args.unitlink == 'on',
 +
        args.lk=='on' and definition=='met' and pageName, pageName )
 +
    local first = args.first or rgData.def1
  
    local definition = data.def1
 
    local pageName = data.pagename or nil
 
    local imp = formatImp( data, args.unitlink == 'on', args.lk=='on' and definition=='imp' and pageName, pageName)
 
    local met = formatMet( data, args.unitlink == 'on', args.lk=='on' and definition=='met' and pageName, pageName)
 
    local first = args.first or data.def1
 
 
     if first == 'met' or first == 'metric' then
 
     if first == 'met' or first == 'metric' then
 
         first = 'met'
 
         first = 'met'
Line 106: Line 311:
 
     end
 
     end
 
     ret = table.concat( ret )
 
     ret = table.concat( ret )
 +
 
     if args.wrap == 'y' then
 
     if args.wrap == 'y' then
 
         return ret
 
         return ret
Line 113: Line 319:
 
end
 
end
  
-- The basic data flow of the module.
+
-----------------------------------------------------------------------------------
local function _main( args )
+
-- main -- MAIN: the basic module
     local gaugeData = mw.loadData( 'Module:RailGauge/data' )
+
-----------------------------------------------------------------------------------
     local searchKey = mw.ustring.lower( args[ 1 ] or '' )
+
function p.main( frame )
    searchKey = mw.ustring.gsub( searchKey, '[%s%,]', '' ) -- Remove all whitespace, commas.
+
     local args = nil
 +
     local rgData = nil
 
     local title = mw.title.getCurrentTitle()
 
     local title = mw.title.getCurrentTitle()
  
     -- Get the gauge information from the /data subpage.
+
    args = prepareArgs( frame )
     local data
+
     -- Get the data entry for this gauge from /data subpage
    for i, t in ipairs( gaugeData ) do
+
     rgData = getGaugeDataSet( args.searchAlias )
        for j, alias in ipairs( t.aliases ) do
 
            if alias == searchKey then
 
                data = t
 
            end
 
        end
 
    end
 
  
 
     -- Categorise the page if no gauge information was found.
 
     -- Categorise the page if no gauge information was found.
     if not data then
+
     if rgData == nil then
 
         local category = ''
 
         local category = ''
        local unknownAlias = args[ 1 ]
+
         if title:inNamespaces( 0, 14 ) then --main=0, cat=14
         if title.namespace == 0 or title.namespace == 14 then
 
 
             category = mw.ustring.format(
 
             category = mw.ustring.format(
                '[[Category:Articles with template RailGauge with unrecognized input|%s, %s]]',
+
              '[[Category:Articles with template RailGauge with unrecognized input|%s, %s]]',
                unknownAlias or ' ', title.text
+
              args["rawInput"] or ' ', title.text
 
             )
 
             )
 
         end
 
         end
         return ( unknownAlias or '' ) .. category
+
         return ( args["rawInput"] or '' ) .. category
 +
    end
 +
 
 +
    local catMentions = ''
 +
    if rgData.id == '1435' then
 +
    -- no s.g. categorization
 +
    else
 +
        local addMentionsCat = args.addcat or 'yes'
 +
        if addMentionsCat ~= 'no' then
 +
            if title:inNamespaces( 0, 14 ) then
 +
                catMentions = catMentionsGaugeSize( rgData.id )
 +
            end
 +
        end
 
     end
 
     end
  
 
     -- Assemble the output.
 
     -- Assemble the output.
 
     local ret = {}
 
     local ret = {}
     table.insert( ret, compose( args, data ) )
+
     table.insert( ret, compose( args, rgData ) )
     local gaugeName = data.name
+
     local gaugeName = rgData.name
     local gaugeLink = data.link
+
     local gaugeLink = rgData.link
 
     if args.allk == 'on' and gaugeLink then
 
     if args.allk == 'on' and gaugeLink then
 
         table.insert( ret, ' ' .. noWrap( gaugeLink ) )
 
         table.insert( ret, ' ' .. noWrap( gaugeLink ) )
Line 153: Line 365:
 
         table.insert( ret, ' ' .. noWrap( gaugeName ) )
 
         table.insert( ret, ' ' .. noWrap( gaugeName ) )
 
     end
 
     end
 
+
     table.insert( ret, catMentions )
     local maintCatSortDepr = data.maintcatsort --category to be deprecated, see 'Categorize all' topic (3 April 2014)
 
    if maintCatSortDepr ~= nil then
 
        local addMaintCat =  args.addcat
 
        if addMaintCat ~= 'off' and addMaintCat ~= 'no' then
 
            if title.namespace == 0 or title.namespace == 14 then
 
                local category = ''
 
                category = mw.ustring.format(
 
                    '[[Category:Articles with template RailGauge that may need attention|%s, %s]]',
 
                    maintCatSortDepr, title.text)
 
                table.insert( ret, ' ' .. category )
 
            end
 
        end
 
    end
 
  
 
     return table.concat( ret )
 
     return table.concat( ret )
 
end
 
end
  
function p.main( frame )
+
-----------------------------------------------------------------------------------
    -- If called via #invoke, use the args passed into the invoking
+
-- checkData -- Public. Performs various checks on the /data subpage.
    -- template, or the args passed to #invoke if any exist. Otherwise
+
-----------------------------------------------------------------------------------
    -- assume args are being passed directly in from the debug console
 
    -- or from another Lua module.
 
    local origArgs
 
    if frame == mw.getCurrentFrame() then
 
        origArgs = frame:getParent().args
 
        for k, v in pairs( frame.args ) do
 
            origArgs = frame.args
 
            break
 
        end
 
    else
 
        origArgs = frame
 
    end
 
 
 
    -- Trim whitespace, make lower-case and remove blank arguments for all arguments but [1].
 
    -- [1] is trimmed and blank values are removed, but capitalization is preserved when
 
    -- when no gauge data is found.
 
    local args = {}
 
    for k, v in pairs( origArgs ) do
 
        v = mw.text.trim( v )
 
        if k == 1 and v ~= '' then
 
            args[ 1 ] = v
 
        elseif v ~= '' then
 
            args[ k ] = mw.ustring.lower( v )
 
        end
 
    end
 
    return _main( args )
 
end
 
 
 
-- Performs various checks on the /data subpage.
 
 
function p.checkData( frame )
 
function p.checkData( frame )
     local dataPage = frame and frame.args and frame.args[1] or 'Module:RailGauge/data'
+
     local dataPage = frame and frame.args and frame.args[1] or dataPageName
 
     local data = mw.loadData( dataPage )
 
     local data = mw.loadData( dataPage )
 
     local exists, dupes, dupeSort, ret = {}, {}, {}, {}
 
     local exists, dupes, dupeSort, ret = {}, {}, {}, {}
 +
 
     -- Check for duplicate aliases.
 
     -- Check for duplicate aliases.
 
     for ti, t in ipairs( data ) do
 
     for ti, t in ipairs( data ) do
Line 227: Line 398:
 
         local positions = {}
 
         local positions = {}
 
         for i2, aliasKeys in ipairs( dupes[ alias ] ) do
 
         for i2, aliasKeys in ipairs( dupes[ alias ] ) do
             local position = mw.ustring.format( 'gauge %d, alias %d (gauge id: <code>%s</code>)', aliasKeys[ 1 ], aliasKeys[ 2 ], data[ aliasKeys[ 1 ] ].id or '' )
+
             local position = mw.ustring.format( 'gauge %d, alias %d ( gauge id: <code>%s</code> )', aliasKeys[ 1 ], aliasKeys[ 2 ], data[ aliasKeys[ 1 ] ].id or '' )
 
             table.insert( positions, position )
 
             table.insert( positions, position )
 
         end
 
         end
Line 238: Line 409:
 
         local den = t.den
 
         local den = t.den
 
         if num and not den then
 
         if num and not den then
             table.insert( ret, mw.ustring.format( 'Numerator "%s" with no denominator detected at gauge %d (id: <code>%s</code>).', num, ti, t.id or '' ) )
+
             table.insert( ret, mw.ustring.format( 'Numerator "%s" with no denominator detected at gauge %d ( id: <code>%s</code> ).', num, ti, t.id or '' ) )
 
         elseif den and not num then
 
         elseif den and not num then
             table.insert( ret, mw.ustring.format( 'Denominator "%s" with no numerator detected at gauge %d (id: <code>%s</code>).', den, ti, t.id or '' ) )
+
             table.insert( ret, mw.ustring.format( 'Denominator "%s" with no numerator detected at gauge %d ( id: <code>%s</code> ).', den, ti, t.id or '' ) )
 
         end
 
         end
 
     end
 
     end
Line 246: Line 417:
 
     for ti, t in ipairs( data ) do
 
     for ti, t in ipairs( data ) do
 
         if not ( t.ft or t['in'] or t.num or t.den ) then
 
         if not ( t.ft or t['in'] or t.num or t.den ) then
             table.insert( ret, mw.ustring.format( 'No imperial measurements found for gauge %d (id: <code>%s</code>).', ti, t.id or '' ) )
+
             table.insert( ret, mw.ustring.format( 'No imperial measurements found for gauge %d ( id: <code>%s</code> ).', ti, t.id or '' ) )
 
         end
 
         end
 
         if not ( t.m or t.mm ) then
 
         if not ( t.m or t.mm ) then
             table.insert( ret, mw.ustring.format( 'No metric measurements found for gauge %d (id: <code>%s</code>).', ti, t.id or '' ) )
+
             table.insert( ret, mw.ustring.format( 'No metric measurements found for gauge %d ( id: <code>%s</code> ).', ti, t.id or '' ) )
 
         end
 
         end
 
     end
 
     end
Line 258: Line 429:
 
             local measurementVal = t[ measurement ]
 
             local measurementVal = t[ measurement ]
 
             if measurementVal and not tonumber( measurementVal ) then
 
             if measurementVal and not tonumber( measurementVal ) then
                 table.insert( ret, mw.ustring.format( 'Non-numeric <code>%s</code> measurement ("%s") found for gauge %d (id: <code>%s</code>).', measurement, measurementVal, ti, t.id or '' ) )
+
                 table.insert( ret, mw.ustring.format( 'Non-numeric <code>%s</code> measurement ( "%s" ) found for gauge %d ( id: <code>%s</code> ).', measurement, measurementVal, ti, t.id or '' ) )
 
             end
 
             end
 
         end
 
         end
Line 269: Line 440:
 
                 table.insert( aliases, mw.ustring.format( '<code>%s</code>', alias ) )
 
                 table.insert( aliases, mw.ustring.format( '<code>%s</code>', alias ) )
 
             end
 
             end
             aliases = mw.ustring.format( ' (aliases: %s)', mw.text.listToText( aliases ) )
+
             aliases = mw.ustring.format( ' ( aliases: %s )', mw.text.listToText( aliases ) )
 
             table.insert( ret, mw.ustring.format( 'No id found for gauge %d%s.', ti, aliases or '' ) )
 
             table.insert( ret, mw.ustring.format( 'No id found for gauge %d%s.', ti, aliases or '' ) )
 
         end
 
         end
Line 276: Line 447:
 
     for ti, t in ipairs( data ) do
 
     for ti, t in ipairs( data ) do
 
         if type( t.aliases ) ~= 'table' then
 
         if type( t.aliases ) ~= 'table' then
             table.insert( ret, mw.ustring.format( 'No aliases found for gauge %d (id: <code>%s</code>).', ti, t.id or '' ) )
+
             table.insert( ret, mw.ustring.format( 'No aliases found for gauge %d ( id: <code>%s</code> ).', ti, t.id or '' ) )
 
         else
 
         else
 
             local isAlias = false
 
             local isAlias = false
Line 284: Line 455:
 
             end
 
             end
 
             if not isAlias then
 
             if not isAlias then
                 table.insert( ret, mw.ustring.format( 'No aliases found for gauge %d (id: <code>%s</code>).', ti, t.id or '' ) )
+
                 table.insert( ret, mw.ustring.format( 'No aliases found for gauge %d ( id: <code>%s</code> ).', ti, t.id or '' ) )
 
             end
 
             end
 
         end
 
         end
Line 291: Line 462:
 
     for ti, t in ipairs( data ) do
 
     for ti, t in ipairs( data ) do
 
         if t.name and not t.link then
 
         if t.name and not t.link then
             table.insert( ret, mw.ustring.format( 'No link found for the named gauge "%s" at position %d (id: <code>%s</code>).', t.name, ti, t.id or '' ) )
+
             table.insert( ret, mw.ustring.format( 'No link found for the named gauge "%s" at position %d ( id: <code>%s</code> ).', t.name, ti, t.id or '' ) )
 
         elseif t.link and not t.name then
 
         elseif t.link and not t.name then
             table.insert( ret, mw.ustring.format( 'No name found for the gauge with link "%s" at position %d (id: <code>%s</code>).', t.link, ti, t.id or '' ) )
+
             table.insert( ret, mw.ustring.format( 'No name found for the gauge with link "%s" at position %d ( id: <code>%s</code> ).', t.link, ti, t.id or '' ) )
 
         end
 
         end
 
     end
 
     end
 
     -- Check for invalid def1 values.
 
     -- Check for invalid def1 values.
 
     for ti, t in ipairs( data ) do
 
     for ti, t in ipairs( data ) do
         local def1 = t.def1
+
         local def = t.def1
         if def1 ~= 'imp' and def1 ~= 'met' then
+
         if def ~= 'imp' and def ~= 'met' then
             table.insert( ret, mw.ustring.format( 'Invalid def1 value "%s" found for gauge %d (id: <code>%s</code>).', def1 or '', ti, t.id or '' ) )
+
             table.insert( ret, mw.ustring.format( 'Invalid def1 value "%s" found for gauge %d ( id: <code>%s</code> ).', def or '', ti, t.id or '' ) )
 
         end
 
         end
 
     end
 
     end
Line 309: Line 480:
 
                 for ai, alias in ipairs( tval ) do
 
                 for ai, alias in ipairs( tval ) do
 
                     if mw.ustring.find( alias, '%s' ) then
 
                     if mw.ustring.find( alias, '%s' ) then
                         table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in gauge %d alias %d ("%s", gauge id: <code>%s</code>).', ti, ai, alias, t.id or '' ) )
+
                         table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in gauge %d alias %d ( "%s", gauge id: <code>%s</code> ).', ti, ai, alias, t.id or '' ) )
 
                     end
 
                     end
 
                 end
 
                 end
             elseif tkey == 'name' or tkey == 'link' or tkey == 'pagename' then
+
             elseif tkey == 'name' or tkey == 'link' or tkey == 'pagename' or tkey == 'contentcat' then
 
                 if tval ~= mw.text.trim( tval ) then
 
                 if tval ~= mw.text.trim( tval ) then
                     table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in <code>%s</code> field of gauge %d ("%s", gauge id: <code>%s</code>).', tkey, ti, tval, t.id or '' ) )
+
                     table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in <code>%s</code> field of gauge %d ( "%s", gauge id: <code>%s</code> ).', tkey, ti, tval, t.id or '' ) )
 
                 end
 
                 end
 
             elseif mw.ustring.find( tval, '%s' ) then
 
             elseif mw.ustring.find( tval, '%s' ) then
                 table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in <code>%s</code> field of gauge %d ("%s", gauge id: <code>%s</code>).', tkey, ti, tval, t.id or '' ) )
+
                 table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in <code>%s</code> field of gauge %d ( "%s", gauge id: <code>%s</code> ).', tkey, ti, tval, t.id or '' ) )
 +
            end
 +
        end
 +
    end
 +
    -- Added April 2014: alias should not double with another id ( imp and mm not ambiguous )
 +
    local self_id = ''
 +
    local self_def = ''
 +
    for ti, t in ipairs( data ) do
 +
        self_id = t.id
 +
        self_def = t.def1
 +
        for iC, aliasCheck in ipairs( t.aliases ) do
 +
            if tonumber( aliasCheck ) ~= nil then
 +
                if self_id ~= aliasCheck then
 +
                    for iTwo, tTwo in ipairs( data ) do
 +
                        if aliasCheck == tTwo.id then
 +
table.insert( ret, mw.ustring.format( 'Input alias %s ( %s ) from <code>id=%s mm</code> ambiguous with gauge id=<code>%s mm</code> ( %s )', aliasCheck, self_def, self_id, tTwo.id, tTwo.def1 ) )
 +
                        end
 +
                    end
 +
                end
 
             end
 
             end
 
         end
 
         end
 
     end
 
     end
 +
 
     -- Return any errors found.
 
     -- Return any errors found.
 
     for i, msg in ipairs( ret ) do
 
     for i, msg in ipairs( ret ) do
 
         ret[ i ] = mw.ustring.format( '<span class="error">%s</span>', msg )
 
         ret[ i ] = mw.ustring.format( '<span class="error">%s</span>', msg )
 
     end
 
     end
 +
 
     if #ret > 0 then
 
     if #ret > 0 then
         return mw.ustring.format( 'Found the following errors in %s:\n* %s', dataPage, table.concat( ret, '\n* ' ) )
+
         return mw.ustring.format( 'Found the following errors in %s:\n* %s', dataPageName, table.concat( ret, '\n* ' ) )
 +
    else
 +
        return mw.ustring.format( 'No errors found in %s.', dataPageName )
 +
    end
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- documentHeader
 +
-----------------------------------------------------------------------------------
 +
local function documentHeader( numberOfEntries, docTitle, docState )
 +
    if docTitle == '' then
 +
        if ( numberOfEntries or 0 ) <= 1 then
 +
            docTitle = 'Rail gauge'
 +
        else
 +
            docTitle = 'Rail gauges'
 +
        end
 +
    end
 +
 
 +
    if docState == '' then
 +
        docState = 'collapsible uncollapsed'
 +
    else
 +
        docState = 'collapsible ' .. docState
 +
    end
 +
 
 +
    local sortColHeaders = ''
 +
    local sortClass = ''
 +
    if ( numberOfEntries or 0 ) > 1 then
 +
        sortClass = 'sortable'
 +
        sortColHeaders = '' ..
 +
        '\n|- ' ..
 +
        '\n! style="line-height:90%;" | &nbsp; || || || || || || ||'
 +
    end
 +
 
 +
    -- 8 columns
 +
    local catMparent = catMentionsGaugeSizeParent( false )
 +
    return '' ..
 +
    '\n{| class="wikitable ' .. sortClass .. ' ' .. docState .. '" '
 +
    .. 'style="text-align:right; width:100%;" ' ..
 +
    '\n|+ style="background:#d8d8d8;" | ' .. docTitle ..
 +
    '\n|-' ..
 +
    '\n!'
 +
    ..    ' style="background:#d8d8d8;" | Size<br>(mm)' --1
 +
    .. '\n! style="background:#d8d8d8;" | Size<br>(ft,&nbsp;in)' --2
 +
    .. '\n! style="background:#d8d8d8;" | Size<br>(inches)' --3
 +
    .. '\n! style="background:#d8d8d8;" | Units<br>&nbsp;' --4
 +
    .. '\n! style="background:#d8d8d8; width:6em;" | Aliases<br>(input&nbsp;options)' --5
 +
    .. '\n! style="background:#d8d8d8; min-width:8em;" | Link<br>&nbsp;' --6
 +
    .. '\n! style="background:#d8d8d8;" | [[:Category:Track gauges by size|Category]]<br>(content)' --7
 +
    .. '\n! style="background:#d8d8d8;" | [[:' .. catMparent .. '|Mentionings]]<br>(maintenance)'
 +
    .. sortColHeaders
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- documentFooter
 +
-----------------------------------------------------------------------------------
 +
local function documentFooter()
 +
    return '\n|}' .. '\n'
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- createcatMentionsGaugeSize -- Create and use preload. Not used after initial creation.
 +
-----------------------------------------------------------------------------------
 +
function createCatMentionsGaugeSize( id )
 +
    local preloadUrl = 'action=edit&preload=Template:RailGauge/preload-categorypage-railgauge-mentionings'
 +
    local catM = catMentionsGaugeSize( id, nil, nil, nil, true )
 +
    local qryString = mw.uri.parseQueryString( preloadUrl )
 +
    return tostring( mw.uri.fullUrl( catM, qryString ) )
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- fromInputToId -- Used cleaned Alias as searchkey
 +
-----------------------------------------------------------------------------------
 +
local function fromInputToId( searchAlias )
 +
    if gaugeDataAll == nil then
 +
        gaugeDataAll = mw.loadData( dataPageName )
 +
    end
 +
    for i, rgEntry in ipairs( gaugeDataAll ) do
 +
        for j, alias in ipairs( rgEntry.aliases ) do
 +
            if alias == searchAlias then
 +
                return rgEntry.id
 +
            end
 +
        end
 +
    end
 +
    -- Second search: by id
 +
    if tonumber( searchAlias ) ~= nil then
 +
        for i, rgEntry in ipairs( gaugeDataAll ) do
 +
          if rgEntry.id == searchAlias then
 +
                return rgEntry.id
 +
            end
 +
        end
 +
    end
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- documentInchCount -- Gives the number of inches in decimals.
 +
-----------------------------------------------------------------------------------
 +
local function documentInchCount( rgEntry )
 +
    local inches = 0
 +
    if rgEntry["num"] ~= nil then
 +
    inches = tonumber( ( rgEntry["num"] or 0 ) / ( rgEntry["den"] or 1 ) )
 +
    end
 +
    inches = tostring( inches + ( tonumber( rgEntry["ft"] or 0 ) * 12 )
 +
            + tonumber( rgEntry["in"] or 0 ) )
 +
    return inches
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- documentInchToMm -- Not used lately
 +
-----------------------------------------------------------------------------------
 +
local function documentInchToMm( inchCount )
 +
    return tonumber( inchCount or 0 ) * 25.4
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- fromIdToEntrySet
 +
-- From one id, make the set withall one-two-three-more entries ( met, inp, variants )
 +
-----------------------------------------------------------------------------------
 +
local function fromIdToEntrySet( id, searchedAlias )
 +
local TableTools = require( 'Module:TableTools' )
 +
local html = require( 'Module:HtmlBuilder' )
 +
local htmlString = ''
 +
local rowSplit = '<div style="border-top:1px solid #ccc; height:1px;"/>'
 +
 
 +
    -- From the id, build the set of existing entries ( met, imp, and variants )
 +
    local entry = {}
 +
    local defType = 0
 +
    for i, rgEntry in ipairs( gaugeDataAll ) do
 +
        if id == rgEntry.id then
 +
            if rgEntry.def1 == 'met' and entry[ 1 ] == nil then
 +
                entry[ 1 ] = rgEntry
 +
                defType = defType +1
 +
            elseif rgEntry.def1 == 'imp' and entry[ 2 ] == nil then
 +
                entry[ 2 ] = rgEntry
 +
                defType = defType + 2
 +
            else
 +
                entry[ 3 + TableTools.size( entry ) ] = rgEntry
 +
            end
 +
        end
 +
    end
 +
    entry = TableTools.compressSparseArray( entry )
 +
    -- Entry set is now complete & clean
 +
    -- Result: the entry table with entries present in data,
 +
    -- In sequence if present ( 1. met, 2. imp, any extra )
 +
    if entry[ 1 ] == nil then
 +
        -- A non-existent id is entered? Unexpected here.
 +
        return '' ..
 +
        '\n|-' ..
 +
        '\n| colspan=7 style="color:red; text-align:left;" |' ..
 +
        'Error using [[Template:RailGauge/document gauge|RailGauge/document gauge]]:' ..
 +
        ' No rail gauge defined for: "' .. ( searchedAlias or '' ) .. '"'
 +
    end
 +
 
 +
    -- Build row from data ( all entries )
 +
    -- String together various data elements per cell
 +
    local inchCount = documentInchCount( entry[ 1 ] )
 +
    -- sortKey
 +
    local sortKey = formatUnitPlaintext( entry[ 1 ], 'met', 5 )
 +
    htmlString = html.create()
 +
        .tag( 'span' ).addClass( 'sortkey' ).wikitext( sortKey )
 +
        sortKey = tostring( htmlString )
 +
    -- aliases -- listing the input options
 +
    local aliasList = {}
 +
    for i, e in ipairs( entry ) do
 +
        local alis = {}
 +
        for j, v in ipairs( e.aliases ) do
 +
            if tonumber( v ) == nil then -- No plain numbers
 +
                table.insert( alis, tostring( v ) )
 +
            end
 +
        end
 +
        -- cannot sort aliases here ( ? )
 +
        for j, v in ipairs( alis ) do
 +
            if string.match( v, '^%d' ) == nil then -- textual so to italic. Todo: better uc
 +
                local txt = v
 +
                htmlString = html.create()
 +
                    .tag( 'span' ).wikitext( txt ).css( 'font-style', 'italic' )
 +
                alis[ j ] = tostring( htmlString )
 +
            end
 +
        end
 +
        table.insert ( aliasList, table.concat( alis, '; ' ) )
 +
    end
 +
    -- def -- Definition unit ( def1 in data )
 +
    local def = {} -- definition code ( 'met' or 'imp' )
 +
    local defText = {}
 +
    for i, v in ipairs ( entry ) do
 +
        table.insert( def, v.def1 )
 +
        if v.def1 == 'imp' then
 +
            table.insert( defText, 'imperial' )
 +
        else
 +
            table.insert( defText, 'metric' )
 +
        end
 +
    end
 +
    -- mm; ft in -- Measurement ( number & unit; met and imp; anchor to here )
 +
    local measure = {}
 +
    local unitanchor = { '', '' }
 +
    measure[ 1 ] = formatMet( entry[ 1 ] )
 +
    measure[ 2 ] = formatImp( entry[ 1 ] ) -- both met and imp from entry[ 1 ]
 +
    if math.fmod( defType, 2 ) == 1 then
 +
        htmlString = html.create()
 +
            .tag( 'span' ).wikitext( measure[ 1 ] ).css( 'font-weight', 'bold' )
 +
        measure[ 1 ] = tostring( htmlString )
 +
        unitanchor[ 1 ] = anchor( entry[ 1 ], 'met' )
 +
    end
 +
    if defType >= 2 then
 +
        htmlString = html.create()
 +
            .tag( 'span' ).wikitext( measure[ 2 ] ).css( 'font-weight', 'bold' )
 +
        measure[ 2 ] = tostring( htmlString )
 +
        unitanchor[ 2 ] = anchor( entry[ 1 ], 'imp' )
 +
    end
 +
    -- Linked page
 +
    local linkPage = {}
 +
    for i, e in ipairs( entry ) do
 +
        table.insert( linkPage, e.pagename )
 +
    end
 +
    if #linkPage == 2 then
 +
        if linkPage[ 1 ] == linkPage[ 2 ] then
 +
            linkPage[ 2 ] = nil
 +
        end
 +
    end
 +
    for i, lp in ipairs( linkPage ) do
 +
        local fmtLp = ''
 +
        if string.len( lp ) > 15 then
 +
            fmtLp = '[[' .. lp .. ']]'
 +
        else
 +
            htmlString = html.create()
 +
                .tag( 'span' ).wikitext( lp ).addClass( 'nowrap' )
 +
            htmlString = tostring( htmlString )
 +
            fmtLp = '[[' .. lp .. '|' .. htmlString .. ']]'
 +
        end
 +
        htmlString = html.create()
 +
            .tag( 'span' ).css( 'text-align', 'left' ).wikitext( fmtLp )
 +
        linkPage[ i ] = tostring( htmlString )
 +
    end
 +
    -- catContent -- This is a possible category with content (not maintenance)
 +
    -- can be hardcoded in the data, or test by size pattern
 +
    local catContent = {}
 +
    local catTtl
 +
    local label
 +
    local catCsuffix = ' gauge railways'
 +
    for i, e in ipairs( entry ) do
 +
      if e.contentcat ~= nil then
 +
          label = string.match( e.contentcat, '([%S]*)' ) or 'nomatch'
 +
          table.insert( catContent,
 +
                      '[[:Category:' .. e.contentcat .. '|cat:' .. label .. ']]' )
 +
      end
 +
    end
 +
    if #catContent == 0 then
 +
        if math.fmod( defType, 2 ) == 1 then
 +
            label = formatUnitPlaintext( entry[ 1 ], 'met' )
 +
            catTtl = mw.title.makeTitle( 14, label .. catCsuffix )
 +
            if catTtl.exists then
 +
                table.insert( catContent,
 +
                            '[[:' .. catTtl.fullText .. '|cat:' .. noWrap( label ) .. ']]' )
 +
            end
 +
        end
 +
        if defType >= 2 then
 +
            label = formatUnitPlaintext( entry[ 1 ], 'imp', nil, true )
 +
            catTtl = mw.title.makeTitle( 14, label .. catCsuffix )
 +
            if catTtl.exists then
 +
                table.insert( catContent,
 +
                            '[[:' .. catTtl.fullText .. '|cat:' .. noWrap( label ) .. ']]' )
 +
            end
 +
        end
 +
    end
 +
    -- Mentions category
 +
    local catMentions = catMentionsGaugeSize( id, catSort, title, true )
 +
    htmlString = html.create()
 +
        .tag( 'span' ).css( 'text-align', 'left' ).wikitext( catMentions )
 +
    catMentions = tostring( htmlString )
 +
    -- Create this cat (temporal, initial only)
 +
    local testTitle = mw.title.getCurrentTitle()
 +
    local createCat = ''
 +
    if testTitle.namespace == 2 then
 +
        createCat = '[' .. createCatMentionsGaugeSize( id ) .. ' c]'
 +
    end
 +
 
 +
    -- Compose the id-row with all cell values
 +
    local row = {}
 +
    table.insert( row, sortKey .. unitanchor[ 1 ] .. measure[ 1 ] ) --1 met
 +
    table.insert( row, sortKey .. unitanchor[ 2 ] .. measure[ 2 ] ) --2 imp
 +
    table.insert( row, sortKey .. inchCount ) --3
 +
    table.insert( row, table.concat( defText, rowSplit ) ) --4
 +
    table.insert( row, table.concat( aliasList, rowSplit ) ) --5
 +
    table.insert( row, table.concat( linkPage, rowSplit ) ) --6
 +
    table.insert( row, table.concat( catContent, rowSplit ) ) --7
 +
    table.insert( row, catMentions ) --8
 +
 
 +
    return '' ..
 +
    '\n|- style="background:#e8e8e8; border-top:2px solid #aaa;" | ' ..
 +
    '\n|' .. table.concat( row, ' || ' )
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- documentGauge -- Selfdocument gauge data ( one, multiple, range, all )
 +
-----------------------------------------------------------------------------------
 +
function p.documentGauge( frame )
 +
    local TableTools = require( 'Module:TableTools' )
 +
    local args = prepareArgs( frame )
 +
 
 +
    gaugeDataAll = mw.loadData( dataPageName )
 +
    -- Series from the list
 +
    -- idFrom and idTo are numerical
 +
    local rgList = {}
 +
    local idFrom = -1
 +
    local idTo = -1
 +
    for i, v in ipairs( args ) do
 +
        if v == 'all' then
 +
            idFrom = -math.huge
 +
            idTo = math.huge
 +
            break
 +
        end
 +
    end
 +
    if args.docfrom ~= nil then
 +
        idFrom = tonumber( fromInputToId( args.docfrom )
 +
                or mw.ustring.gsub( '0' .. args.docfrom, 'mm', '' ) )
 +
        idTo = math.huge
 +
    end
 +
    if args.docto ~= nil then
 +
        idTo = tonumber( fromInputToId( args.docto )
 +
              or mw.ustring.gsub( '0' .. args.docto, 'mm', '' ) )
 +
    end
 +
    if idTo > 0 then -- some list is requested from the whole data set
 +
        if idFrom > idTo then
 +
            local dummy = idFrom
 +
            idFrom = idTo
 +
            idTo = dummy
 +
        end
 +
        for i, rgEntry in ipairs( gaugeDataAll ) do
 +
            if ( tonumber( rgEntry.id ) >= idFrom )
 +
            and ( tonumber( rgEntry.id ) <= idTo ) then
 +
                table.insert( rgList, tonumber( rgEntry.id ) )
 +
            end
 +
        end
 +
        rgList = TableTools.removeDuplicates( rgList )
 +
        table.sort( rgList )
 +
    end
 +
 
 +
    -- Individual entries can be mentioned in args ( all unnamed = numbered params )
 +
    -- Need a straight table to keep sequence right
 +
    local argsAliases = TableTools.compressSparseArray( args )
 +
    for i, argsAlias in ipairs( argsAliases ) do
 +
        id = fromInputToId( argsAlias )
 +
        if id ~= nil then
 +
            -- Add to the top
 +
            table.insert( rgList, i, tonumber( id ) )
 +
        end
 +
    end
 +
    -- Now loop through the prepared rgList[id] and add rows to result table
 +
    -- One row contains all available entries for the id ( met, imp, a third variant )
 +
    local rowRGid = {}
 +
    for i, numId in ipairs( rgList ) do
 +
        table.insert( rowRGid, fromIdToEntrySet( tostring( numId ) ) )
 +
    end
 +
 
 +
    return documentHeader( #rgList, args.doctitle or '', args.docstate or '' ) ..
 +
        table.concat( rowRGid, '' ) .. documentFooter()
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- gaugeSizeFromTitle
 +
-- Currently finds "1620 mm" when at end of title, then returns "1620". Blank when not found.
 +
-- Used for cat:mentions category page.
 +
-----------------------------------------------------------------------------------
 +
function p.gaugeSizeFromTitle()
 +
    local title = mw.title.getCurrentTitle()
 +
    return string.match( title.text, '%s(%d+%.?%d*)%smm$' ) or ''
 +
end
 +
 
 +
-----------------------------------------------------------------------------------
 +
-- catSortFromTitle
 +
-- Currently finds "600 mm" when at end of title, then returns "0600 mm" (for catSort).
 +
-- Blank when not found. Used for cat:mentions category page.
 +
-----------------------------------------------------------------------------------
 +
function p.catSortFromTitle()
 +
    local title = mw.title.getCurrentTitle()
 +
    local catSort = string.match( title.text, '%s(%d+%.?%d*)%smm$' ) or ''
 +
    if catSort ~= '' then
 +
        catSort = string.rep( '0',
 +
                  4 - string.len( math.floor( tonumber( catSort ) ) ) )
 +
                  .. catSort .. ' mm'
 +
    end
 +
    if catSort == '' then
 +
        return '*'
 
     else
 
     else
         return mw.ustring.format( 'No errors found in %s.', dataPage )
+
         return catSort
 
     end
 
     end
 
end
 
end
  
 
return p
 
return p

Revision as of 00:13, 3 May 2014

This module implements the {{Track gauge}} template. Please see the template page for documentation on how to use the main TrackGauge function.

Gauge data

The gauge information is stored at Module:Track gauge/data; to add new gauges, see the instructions there.

Data checks

This module includes a function that checks the data page for errors. It is used with the following code:

{{#invoke:Track gauge/autodocument|checkData|name of data page}}

The first positional parameter is the name of the data page that you want to be checked. If this is omitted, the module checks Module:Track gauge/data.

Tracking/maintenance category


-- This module implements the {{RailGauge}} template.
-- Input can be predefined aliases (see the /data module page)
local p = {}
local gaugeDataAll = nil
local dataPageName = 'Module:RailGauge/data'

-----------------------------------------------------------------------------------
-- formatUnitPlaintext
-- Pattern '00016.5 mm' for tablesort and catsort. Can be wrapped in class="sortkey"
-----------------------------------------------------------------------------------
local function formatUnitPlaintext( rgData, unit, fmtZeroPadding, toFracChar )
    -- Returns plaintext (ASCII) only. No css.
    if rgData == nil then
        return ''
    end
    if ( unit or rgData.def ) == 'imp' then
        local ft = ''
        local inch = ''
        local frac = ''
        if rgData.ft then
            ft = rgData.ft .. ' ft'
        end
        if rgData.num then
            frac = ' ' .. rgData.num .. '/' .. rgData.den
            if toFracChar then
                -- as used in contentCat pagenames
                if frac == ' 1/8' then
                    frac = '⅛'
                elseif frac == ' 1/4' then
                    frac = '¼'
                elseif frac == ' 3/8' then
                    frac = '⅜'
                elseif frac == ' 1/2' then
                    frac = '½'
                elseif frac == ' 3/4' then
                    frac = '¾'
                elseif frac == ' 7/8' then
                    frac = '⅞'
                else
                    frac = frac .. ' (error: fraction character missing in module:RailGauge)'
                end
            end
            if rgData["in"] then
                 frac = ' ' .. rgData["in"] .. frac .. ' in'
            else
                 frac = frac .. ' in'
            end
        else
            if rgData["in"] then
                 inch = ' ' .. rgData["in"] .. ' in'
            end
        end
        return mw.text.trim( ft .. inch .. frac )
    else
        -- metric ( mm )
        if fmtZeroPadding == nil or tonumber( fmtZeroPadding ) <= 0 then
            return rgData.id .. ' mm'
        else
            return string.rep( '0',
                   fmtZeroPadding - string.len( math.floor( tonumber( rgData.id ) ) ) )
                   .. rgData.id .. ' mm'
        end
    end
end

-----------------------------------------------------------------------------------
-- anchor -- Anchor text *here* is: <span id="1000 mm">; anchor *there* is: #1000 mm.
-----------------------------------------------------------------------------------
local function anchor( rgEntry, unit, herethere )
    local html = require( 'Module:HtmlBuilder' )
    if rgEntry == nil then
        return ''
    end
    unit = unit or rgEntry.def1
    local anch = formatUnitPlaintext( rgEntry,unit, 0 )
    if herethere == 'there' then -- Untested, April 2014
        anch = '#' .. anch
    else
        anch = html.create().tag( 'span' ).attr( 'id', anch )
    end
    return tostring( anch )
end

-----------------------------------------------------------------------------------
-- catMentionsGaugeSize -- category name for "article mentions gauge" categories
-----------------------------------------------------------------------------------
local function catMentionsGaugeSize( mmSize, catSort, pagename, show, plaintext )
    local cat -- hereo
    -- Category page name. mmSize has format of id ( like '16.5' ).
    cat = 'Category:Articles that mention rail gauge size' .. ' ' .. mmSize .. '&nbsp;mm'
    if plaintext then
        return cat
    elseif show then
        -- uses colon and wikilabel; no catsort
        local label = '|cat:mentionings'
        cat = '[[:'.. cat .. label .. ']]'
    else
        if ( catSort or '' ) ~= '' then
            catSort = '|' .. catSort
        else
            catSort = ''
        end
        cat = '[['.. cat .. catSort .. ']]'
    end
    return cat
end

-----------------------------------------------------------------------------------
-- catMentionsGaugeSizeParent -- Parent of all "mentions" categories
-----------------------------------------------------------------------------------
local function catMentionsGaugeSizeParent( show )
    local cat
    -- Category page name
    cat = 'Category:Articles that mention a specific rail gauge'
    if show then
        -- uses colon and wikilabel
        cat = '[[:'.. cat .. '|Cat:Articles mentioning RailGauge]]'
    end
    return cat
end

-----------------------------------------------------------------------------------
-- prepareArgs -- Arguments coming from an #invoke or from a module
-----------------------------------------------------------------------------------
local function prepareArgs( frame )
    -- If called via #invoke, use the args passed into the invoking
    -- template, or the args passed to #invoke if any exist. Otherwise
    -- assume args are being passed directly in from the debug console
    -- or from another Lua module.
    local origArgs
    if frame == mw.getCurrentFrame() then
        origArgs = frame:getParent().args
        for k, v in pairs( frame.args ) do
            origArgs = frame.args
            break
        end
    else
        origArgs = frame
    end

    -- Trim whitespace, make lower-case and remove blank arguments for all arguments
    -- searchAlias is the cleaned value of [1]. [1] is kept as rawInput for error message
    local args = {}
    args["searchAlias"] = ''
    args["rawInput"] = origArgs[ 1 ] or ''

    for k, v in pairs( origArgs ) do
        v = mw.text.trim( v )
        if tonumber( k ) == nil then
            -- Named argment
            args[ k ] = mw.ustring.lower( v )
        else
            -- Unnamed argument, alias to be searched
            -- Into lowercase, remove all whitespace, commas to create the search key
            v = mw.ustring.lower( mw.ustring.gsub( v, '[%s%,]', '' ) )
            args[ k ] = v
            if k == 1 then
                args["searchAlias"] = v
            end
        end
    end

    return args
end

-----------------------------------------------------------------------------------
-- maintReportArgs -- debug feedback, convenience.
-----------------------------------------------------------------------------------
local function maintReportArgs()
    local args = prepareArgs( frame )
    local report = 'args: '
    for k, v in pairs( args ) do
        report = report .. k .. '='.. v .. '; '
    end
    return '<tt>' .. report .. '</tt><br>'
end

-----------------------------------------------------------------------------------
-- getGaugeDataSet -- Find entry data for a single gauge
-----------------------------------------------------------------------------------
local function getGaugeDataSet( searchAlias )
    if gaugeDataAll == nil then
        gaugeDataAll = mw.loadData( dataPageName )
    end
    local rgEntry = nil
    for i, rgEntry in ipairs( gaugeDataAll ) do
        for j, alias in ipairs( rgEntry.aliases ) do
            if alias == searchAlias then
                return rgEntry
            end
        end
    end
end

-----------------------------------------------------------------------------------
-- noWrap -- Add span tags to prevent a string from wrapping.
-----------------------------------------------------------------------------------
local function noWrap( s )
    return mw.ustring.format( '<span class="nowrap">%s</span>', s )
end

-----------------------------------------------------------------------------------
-- frac -- A slimmed-down version of the {{frac}} template ( a nowrap to be added with the unit )
-----------------------------------------------------------------------------------
local function frac( whole, num, den )
    return mw.ustring.format(
        '<span class="frac">%s%s<sup>%s</sup>&frasl;<sub>%s</sub></span>',
        whole or '', whole and '<span class="visualhide">&nbsp;</span>' or '', num, den
        )
end

-----------------------------------------------------------------------------------
-- formatImp -- Formats imperial units size into a single text element
-----------------------------------------------------------------------------------
local function formatImp( rgData, ulink, articleLink, pageName )
    local ret = {}
    local ft = rgData.ft

    if ft then
        local ftlink = ulink and not articleLink and '[[Foot (unit)|ft]]' or 'ft'
        table.insert( ret, mw.ustring.format( '%s&nbsp;%s', ft, ftlink ) )
    end
    local inches = rgData['in']
    local num = rgData.num
    local den = rgData.den
    if inches and not num and not den then
        table.insert( ret, inches )
    elseif num and den then
        table.insert( ret, frac( inches, num, den ) )
    end
    if inches or num and den then
        local incheslink = ulink and not articleLink and '[[inch|in]]' or 'in'
        table.insert( ret, incheslink )
    end
    local gaugeSize = noWrap( table.concat( ret, '&nbsp;' ) )

    if articleLink then
        return '[[' .. pageName .. '|' .. gaugeSize .. ']]'
    else
        return gaugeSize
    end
end

-----------------------------------------------------------------------------------
-- formatMet -- Formats metric measurements into a single formatted element
-----------------------------------------------------------------------------------
local function formatMet( rgData, ulink, articleLink, pageName )
    local m = rgData.m
    local gaugeSize

    if m then
        local munit = ulink and not articleLink and '[[metre|m]]' or 'm'
        gaugeSize = noWrap( mw.ustring.format( '%s&nbsp;%s', m, munit ) )
    else
        local mm = rgData.mm
        mm = tonumber( mm )
        if mm then
            mm = mw.getContentLanguage():formatNum( mm )
        end
        local mmunit = ulink and not articleLink and '[[millimetre|mm]]' or 'mm'
        gaugeSize = noWrap( mw.ustring.format( '%s&nbsp;%s', mm, mmunit ) )
    end

    if articleLink then
        return '[[' .. pageName .. '|' .. gaugeSize .. ']]'
    else
        return gaugeSize
    end
end

-----------------------------------------------------------------------------------
-- compose -- Puts together the two metric, imperial measures into an output string.
-----------------------------------------------------------------------------------
local function compose( args, rgData )
    local definition = rgData.def1
    local pageName = rgData.pagename or nil
    local imp = formatImp( rgData, args.unitlink == 'on',
        args.lk=='on' and definition=='imp' and pageName, pageName )
    local met = formatMet( rgData, args.unitlink == 'on',
        args.lk=='on' and definition=='met' and pageName, pageName )
    local first = args.first or rgData.def1

    if first == 'met' or first == 'metric' then
        first = 'met'
    else
        first = 'imp'
    end
    local ret = {}
    if first == 'met' then
        table.insert( ret, met )
    else
        table.insert( ret, imp )
    end
    local disp = args.disp
    if disp ~= '1' then
        local formatText
        if disp == 's' or disp == '/' then
            formatText = '/&#x200b;%s'
        elseif disp == 'or' then
            formatText = ' or %s'
        elseif disp == '=' then
            formatText = ' = %s'
        else
            formatText = ' (%s)'
        end
        if first == 'met' then
            table.insert( ret, mw.ustring.format( formatText, imp ) )
        else
            table.insert( ret, mw.ustring.format( formatText, met ) )
        end
    end
    ret = table.concat( ret )

    if args.wrap == 'y' then
        return ret
    else
        return noWrap( ret )
    end
end

-----------------------------------------------------------------------------------
-- main -- MAIN: the basic module
-----------------------------------------------------------------------------------
function p.main( frame )
    local args = nil
    local rgData = nil
    local title = mw.title.getCurrentTitle()

    args = prepareArgs( frame )
    -- Get the data entry for this gauge from /data subpage
    rgData = getGaugeDataSet( args.searchAlias )

    -- Categorise the page if no gauge information was found.
    if rgData == nil then
        local category = ''
        if title:inNamespaces( 0, 14 ) then --main=0, cat=14
            category = mw.ustring.format(
               '[[Category:Articles with template RailGauge with unrecognized input|%s, %s]]',
               args["rawInput"] or ' ', title.text
            )
        end
        return ( args["rawInput"] or '' ) .. category
    end

    local catMentions = ''
    if rgData.id == '1435' then
    	-- no s.g. categorization
    else
        local addMentionsCat = args.addcat or 'yes'
        if addMentionsCat ~= 'no' then
            if title:inNamespaces( 0, 14 ) then
                catMentions = catMentionsGaugeSize( rgData.id )
            end
        end
    end

    -- Assemble the output.
    local ret = {}
    table.insert( ret, compose( args, rgData ) )
    local gaugeName = rgData.name
    local gaugeLink = rgData.link
    if args.allk == 'on' and gaugeLink then
        table.insert( ret, ' ' .. noWrap( gaugeLink ) )
    elseif args.al == 'on' and gaugeName then
        table.insert( ret, ' ' .. noWrap( gaugeName ) )
    end
    table.insert( ret, catMentions )

    return table.concat( ret )
end

-----------------------------------------------------------------------------------
-- checkData -- Public. Performs various checks on the /data subpage.
-----------------------------------------------------------------------------------
function p.checkData( frame )
    local dataPage = frame and frame.args and frame.args[1] or dataPageName
    local data = mw.loadData( dataPage )
    local exists, dupes, dupeSort, ret = {}, {}, {}, {}

    -- Check for duplicate aliases.
    for ti, t in ipairs( data ) do
        for ai, alias in ipairs( t.aliases or {} ) do
            if not exists[ alias ] then
                exists[ alias ] = { ti, ai }
            else
                if not dupes[ alias ] then
                    dupes[ alias ] = { exists[ alias ] }
                end
                table.insert( dupes[ alias ], { ti, ai } )
            end
        end
    end
    for alias in pairs( dupes ) do
        table.insert( dupeSort, alias )
    end
    table.sort( dupeSort )
    for i1, alias in ipairs( dupeSort ) do
        local positions = {}
        for i2, aliasKeys in ipairs( dupes[ alias ] ) do
            local position = mw.ustring.format( 'gauge %d, alias %d ( gauge id: <code>%s</code> )', aliasKeys[ 1 ], aliasKeys[ 2 ], data[ aliasKeys[ 1 ] ].id or '' )
            table.insert( positions, position )
        end
        local aliasText = mw.ustring.format( 'Duplicate aliases "%s" detected at the following positions: %s.', alias, mw.text.listToText( positions, '; ' ) )
        table.insert( ret, aliasText )
    end
    -- Check for numerators without denominators.
    for ti, t in ipairs( data ) do
        local num = t.num
        local den = t.den
        if num and not den then
            table.insert( ret, mw.ustring.format( 'Numerator "%s" with no denominator detected at gauge %d ( id: <code>%s</code> ).', num, ti, t.id or '' ) )
        elseif den and not num then
            table.insert( ret, mw.ustring.format( 'Denominator "%s" with no numerator detected at gauge %d ( id: <code>%s</code> ).', den, ti, t.id or '' ) )
        end
    end
    -- Check for gauges with no imperial or no metric measurements.
    for ti, t in ipairs( data ) do
        if not ( t.ft or t['in'] or t.num or t.den ) then
            table.insert( ret, mw.ustring.format( 'No imperial measurements found for gauge %d ( id: <code>%s</code> ).', ti, t.id or '' ) )
        end
        if not ( t.m or t.mm ) then
            table.insert( ret, mw.ustring.format( 'No metric measurements found for gauge %d ( id: <code>%s</code> ).', ti, t.id or '' ) )
        end
    end
    -- Check for non-numeric measurements.
    local measurements = { 'ft', 'in', 'num', 'den', 'm', 'mm' }
    for ti, t in ipairs( data ) do
        for mi, measurement in ipairs( measurements ) do
            local measurementVal = t[ measurement ]
            if measurementVal and not tonumber( measurementVal ) then
                table.insert( ret, mw.ustring.format( 'Non-numeric <code>%s</code> measurement ( "%s" ) found for gauge %d ( id: <code>%s</code> ).', measurement, measurementVal, ti, t.id or '' ) )
            end
        end
    end
    -- Check for gauges with no id.
    for ti, t in ipairs( data ) do
        if not t.id then
            local aliases = {}
            for i, alias in ipairs( t.aliases ) do
                table.insert( aliases, mw.ustring.format( '<code>%s</code>', alias ) )
            end
            aliases = mw.ustring.format( ' ( aliases: %s )', mw.text.listToText( aliases ) )
            table.insert( ret, mw.ustring.format( 'No id found for gauge %d%s.', ti, aliases or '' ) )
        end
    end
    -- Check for gauges with no aliases.
    for ti, t in ipairs( data ) do
        if type( t.aliases ) ~= 'table' then
            table.insert( ret, mw.ustring.format( 'No aliases found for gauge %d ( id: <code>%s</code> ).', ti, t.id or '' ) )
        else
            local isAlias = false
            for ai, alias in ipairs( t.aliases ) do
                isAlias = true
                break
            end
            if not isAlias then
                table.insert( ret, mw.ustring.format( 'No aliases found for gauge %d ( id: <code>%s</code> ).', ti, t.id or '' ) )
            end
        end
    end
    -- Check for named gauges with no links and gauges with links but no names.
    for ti, t in ipairs( data ) do
        if t.name and not t.link then
            table.insert( ret, mw.ustring.format( 'No link found for the named gauge "%s" at position %d ( id: <code>%s</code> ).', t.name, ti, t.id or '' ) )
        elseif t.link and not t.name then
            table.insert( ret, mw.ustring.format( 'No name found for the gauge with link "%s" at position %d ( id: <code>%s</code> ).', t.link, ti, t.id or '' ) )
        end
    end
    -- Check for invalid def1 values.
    for ti, t in ipairs( data ) do
        local def = t.def1
        if def ~= 'imp' and def ~= 'met' then
            table.insert( ret, mw.ustring.format( 'Invalid def1 value "%s" found for gauge %d ( id: <code>%s</code> ).', def or '', ti, t.id or '' ) )
        end
    end
    -- Check for unwanted whitespace.
    for ti, t in ipairs( data ) do
        for tkey, tval in pairs( t ) do
            if tkey == 'aliases' and type( tval ) == 'table' then
                for ai, alias in ipairs( tval ) do
                    if mw.ustring.find( alias, '%s' ) then
                        table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in gauge %d alias %d ( "%s", gauge id: <code>%s</code> ).', ti, ai, alias, t.id or '' ) )
                    end
                end
            elseif tkey == 'name' or tkey == 'link' or tkey == 'pagename' or tkey == 'contentcat' then
                if tval ~= mw.text.trim( tval ) then
                    table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in <code>%s</code> field of gauge %d ( "%s", gauge id: <code>%s</code> ).', tkey, ti, tval, t.id or '' ) )
                end
            elseif mw.ustring.find( tval, '%s' ) then
                table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in <code>%s</code> field of gauge %d ( "%s", gauge id: <code>%s</code> ).', tkey, ti, tval, t.id or '' ) )
            end
        end
    end
    -- Added April 2014: alias should not double with another id ( imp and mm not ambiguous )
    local self_id = ''
    local self_def = ''
    for ti, t in ipairs( data ) do
        self_id = t.id
        self_def = t.def1
        for iC, aliasCheck in ipairs( t.aliases ) do
            if tonumber( aliasCheck ) ~= nil then
                if self_id ~= aliasCheck then
                    for iTwo, tTwo in ipairs( data ) do
                        if aliasCheck == tTwo.id then
table.insert( ret, mw.ustring.format( 'Input alias %s ( %s ) from <code>id=%s mm</code> ambiguous with gauge id=<code>%s mm</code> ( %s )', aliasCheck, self_def, self_id, tTwo.id, tTwo.def1 ) )
                        end
                    end
                end
            end
        end
    end

    -- Return any errors found.
    for i, msg in ipairs( ret ) do
        ret[ i ] = mw.ustring.format( '<span class="error">%s</span>', msg )
    end

    if #ret > 0 then
        return mw.ustring.format( 'Found the following errors in %s:\n* %s', dataPageName, table.concat( ret, '\n* ' ) )
    else
        return mw.ustring.format( 'No errors found in %s.', dataPageName )
    end
end

-----------------------------------------------------------------------------------
-- documentHeader
-----------------------------------------------------------------------------------
local function documentHeader( numberOfEntries, docTitle, docState )
    if docTitle == '' then
        if ( numberOfEntries or 0 ) <= 1 then
            docTitle = 'Rail gauge'
        else
            docTitle = 'Rail gauges'
        end
    end

    if docState == '' then
        docState = 'collapsible uncollapsed'
    else
        docState = 'collapsible ' .. docState
    end

    local sortColHeaders = ''
    local sortClass = ''
    if ( numberOfEntries or 0 ) > 1 then
        sortClass = 'sortable'
        sortColHeaders = '' ..
        '\n|- ' ..
        '\n! style="line-height:90%;" | &nbsp; || || || || || || ||'
    end

    -- 8 columns
    local catMparent = catMentionsGaugeSizeParent( false )
    return '' ..
    '\n{| class="wikitable ' .. sortClass .. ' ' .. docState .. '" '
    .. 'style="text-align:right; width:100%;" ' ..
    '\n|+ style="background:#d8d8d8;" | ' .. docTitle ..
    '\n|-' ..
    '\n!'
    ..    ' style="background:#d8d8d8;" | Size<br>(mm)' --1
    .. '\n! style="background:#d8d8d8;" | Size<br>(ft,&nbsp;in)' --2
    .. '\n! style="background:#d8d8d8;" | Size<br>(inches)' --3
    .. '\n! style="background:#d8d8d8;" | Units<br>&nbsp;' --4
    .. '\n! style="background:#d8d8d8; width:6em;" | Aliases<br>(input&nbsp;options)' --5
    .. '\n! style="background:#d8d8d8; min-width:8em;" | Link<br>&nbsp;' --6
    .. '\n! style="background:#d8d8d8;" | [[:Category:Track gauges by size|Category]]<br>(content)' --7
    .. '\n! style="background:#d8d8d8;" | [[:' .. catMparent .. '|Mentionings]]<br>(maintenance)'
    .. sortColHeaders
end

-----------------------------------------------------------------------------------
-- documentFooter
-----------------------------------------------------------------------------------
local function documentFooter()
    return '\n|}' .. '\n'
end

-----------------------------------------------------------------------------------
-- createcatMentionsGaugeSize -- Create and use preload. Not used after initial creation.
-----------------------------------------------------------------------------------
function createCatMentionsGaugeSize( id )
    local preloadUrl = 'action=edit&preload=Template:RailGauge/preload-categorypage-railgauge-mentionings'
    local catM = catMentionsGaugeSize( id, nil, nil, nil, true )
    local qryString = mw.uri.parseQueryString( preloadUrl )
    return tostring( mw.uri.fullUrl( catM, qryString ) )
end

-----------------------------------------------------------------------------------
-- fromInputToId -- Used cleaned Alias as searchkey
-----------------------------------------------------------------------------------
local function fromInputToId( searchAlias )
    if gaugeDataAll == nil then
        gaugeDataAll = mw.loadData( dataPageName )
    end
    for i, rgEntry in ipairs( gaugeDataAll ) do
        for j, alias in ipairs( rgEntry.aliases ) do
            if alias == searchAlias then
                return rgEntry.id
            end
        end
    end
    -- Second search: by id
    if tonumber( searchAlias ) ~= nil then
        for i, rgEntry in ipairs( gaugeDataAll ) do
           if rgEntry.id == searchAlias then
                return rgEntry.id
            end
        end
    end
end

-----------------------------------------------------------------------------------
-- documentInchCount -- Gives the number of inches in decimals.
-----------------------------------------------------------------------------------
local function documentInchCount( rgEntry )
    local inches = 0
    if rgEntry["num"] ~= nil then
    	inches = tonumber( ( rgEntry["num"] or 0 ) / ( rgEntry["den"] or 1 ) )
    end
    inches = tostring( inches + ( tonumber( rgEntry["ft"] or 0 ) * 12 )
             + tonumber( rgEntry["in"] or 0 ) )
    return inches
end

-----------------------------------------------------------------------------------
-- documentInchToMm -- Not used lately
-----------------------------------------------------------------------------------
local function documentInchToMm( inchCount )
    return tonumber( inchCount or 0 ) * 25.4
end

-----------------------------------------------------------------------------------
-- fromIdToEntrySet
-- From one id, make the set withall one-two-three-more entries ( met, inp, variants )
-----------------------------------------------------------------------------------
local function fromIdToEntrySet( id, searchedAlias )
local TableTools = require( 'Module:TableTools' )
local html = require( 'Module:HtmlBuilder' )
local htmlString = ''
local rowSplit = '<div style="border-top:1px solid #ccc; height:1px;"/>'

    -- From the id, build the set of existing entries ( met, imp, and variants )
    local entry = {}
    local defType = 0
    for i, rgEntry in ipairs( gaugeDataAll ) do
        if id == rgEntry.id then
            if rgEntry.def1 == 'met' and entry[ 1 ] == nil then
                entry[ 1 ] = rgEntry
                defType = defType +1
            elseif rgEntry.def1 == 'imp' and entry[ 2 ] == nil then
                entry[ 2 ] = rgEntry
                defType = defType + 2
            else
                entry[ 3 + TableTools.size( entry ) ] = rgEntry
            end
        end
    end
    entry = TableTools.compressSparseArray( entry )
    -- Entry set is now complete & clean
    -- Result: the entry table with entries present in data,
    -- In sequence if present ( 1. met, 2. imp, any extra )
    if entry[ 1 ] == nil then
        -- A non-existent id is entered? Unexpected here.
        return '' ..
        '\n|-' ..
        '\n| colspan=7 style="color:red; text-align:left;" |' ..
        'Error using [[Template:RailGauge/document gauge|RailGauge/document gauge]]:' ..
        ' No rail gauge defined for: "' .. ( searchedAlias or '' ) .. '"'
     end

    -- Build row from data ( all entries )
    -- String together various data elements per cell
    local inchCount = documentInchCount( entry[ 1 ] )
    -- sortKey
    local sortKey = formatUnitPlaintext( entry[ 1 ], 'met', 5 )
    htmlString = html.create()
        .tag( 'span' ).addClass( 'sortkey' ).wikitext( sortKey )
        sortKey = tostring( htmlString )
    -- aliases -- listing the input options
    local aliasList = {}
    for i, e in ipairs( entry ) do
        local alis = {}
        for j, v in ipairs( e.aliases ) do
            if tonumber( v ) == nil then -- No plain numbers
                table.insert( alis, tostring( v ) )
            end
        end
        -- cannot sort aliases here ( ? )
        for j, v in ipairs( alis ) do
            if string.match( v, '^%d' ) == nil then -- textual so to italic. Todo: better uc
                local txt = v
                htmlString = html.create()
                    .tag( 'span' ).wikitext( txt ).css( 'font-style', 'italic' )
                alis[ j ] = tostring( htmlString )
            end
        end
        table.insert ( aliasList, table.concat( alis, '; ' ) )
    end
    -- def -- Definition unit ( def1 in data )
    local def = {} -- definition code ( 'met' or 'imp' )
    local defText = {}
    for i, v in ipairs ( entry ) do
        table.insert( def, v.def1 )
        if v.def1 == 'imp' then
            table.insert( defText, 'imperial' )
        else
            table.insert( defText, 'metric' )
        end
    end
    -- mm; ft in -- Measurement ( number & unit; met and imp; anchor to here )
    local measure = {}
    local unitanchor = { '', '' }
    measure[ 1 ] = formatMet( entry[ 1 ] )
    measure[ 2 ] = formatImp( entry[ 1 ] ) -- both met and imp from entry[ 1 ]
    if math.fmod( defType, 2 ) == 1 then
        htmlString = html.create()
            .tag( 'span' ).wikitext( measure[ 1 ] ).css( 'font-weight', 'bold' )
        measure[ 1 ] = tostring( htmlString )
        unitanchor[ 1 ] = anchor( entry[ 1 ], 'met' )
    end
    if defType >= 2 then
        htmlString = html.create()
            .tag( 'span' ).wikitext( measure[ 2 ] ).css( 'font-weight', 'bold' )
        measure[ 2 ] = tostring( htmlString )
        unitanchor[ 2 ] = anchor( entry[ 1 ], 'imp' )
    end
    -- Linked page
    local linkPage = {}
    for i, e in ipairs( entry ) do
        table.insert( linkPage, e.pagename )
    end
    if #linkPage == 2 then
        if linkPage[ 1 ] == linkPage[ 2 ] then
            linkPage[ 2 ] = nil
        end
    end
    for i, lp in ipairs( linkPage ) do
        local fmtLp = ''
        if string.len( lp ) > 15 then
            fmtLp = '[[' .. lp .. ']]'
        else
            htmlString = html.create()
                .tag( 'span' ).wikitext( lp ).addClass( 'nowrap' )
            htmlString = tostring( htmlString )
            fmtLp = '[[' .. lp .. '|' .. htmlString .. ']]'
        end
        htmlString = html.create()
            .tag( 'span' ).css( 'text-align', 'left' ).wikitext( fmtLp )
        linkPage[ i ] = tostring( htmlString )
    end
    -- catContent -- This is a possible category with content (not maintenance)
    -- can be hardcoded in the data, or test by size pattern
    local catContent = {}
    local catTtl
    local label
    local catCsuffix = ' gauge railways'
    for i, e in ipairs( entry ) do
       if e.contentcat ~= nil then
           label = string.match( e.contentcat, '([%S]*)' ) or 'nomatch'
           table.insert( catContent,
                       '[[:Category:' .. e.contentcat .. '|cat:' .. label .. ']]' )
       end
    end
    if #catContent == 0 then
        if math.fmod( defType, 2 ) == 1 then
            label = formatUnitPlaintext( entry[ 1 ], 'met' )
            catTtl = mw.title.makeTitle( 14, label .. catCsuffix )
            if catTtl.exists then
                table.insert( catContent,
                            '[[:' .. catTtl.fullText .. '|cat:' .. noWrap( label ) .. ']]' )
            end
        end
        if defType >= 2 then
            label = formatUnitPlaintext( entry[ 1 ], 'imp', nil, true )
            catTtl = mw.title.makeTitle( 14, label .. catCsuffix )
            if catTtl.exists then
                table.insert( catContent,
                            '[[:' .. catTtl.fullText .. '|cat:' .. noWrap( label ) .. ']]' )
            end
        end
    end
    -- Mentions category
    local catMentions = catMentionsGaugeSize( id, catSort, title, true )
    htmlString = html.create()
        .tag( 'span' ).css( 'text-align', 'left' ).wikitext( catMentions )
    catMentions = tostring( htmlString )
    -- Create this cat (temporal, initial only)
    local testTitle = mw.title.getCurrentTitle()
    local createCat = ''
    if testTitle.namespace == 2 then
        createCat = '[' .. createCatMentionsGaugeSize( id ) .. ' c]'
    end

    -- Compose the id-row with all cell values
    local row = {}
    table.insert( row, sortKey .. unitanchor[ 1 ] .. measure[ 1 ] ) --1 met
    table.insert( row, sortKey .. unitanchor[ 2 ] .. measure[ 2 ] ) --2 imp
    table.insert( row, sortKey .. inchCount ) --3
    table.insert( row, table.concat( defText, rowSplit ) ) --4
    table.insert( row, table.concat( aliasList, rowSplit ) ) --5
    table.insert( row, table.concat( linkPage, rowSplit ) ) --6
    table.insert( row, table.concat( catContent, rowSplit ) ) --7
    table.insert( row, catMentions ) --8

    return '' ..
    '\n|- style="background:#e8e8e8; border-top:2px solid #aaa;" | ' ..
    '\n|' .. table.concat( row, ' || ' )
end

-----------------------------------------------------------------------------------
-- documentGauge -- Selfdocument gauge data ( one, multiple, range, all )
-----------------------------------------------------------------------------------
function p.documentGauge( frame )
    local TableTools = require( 'Module:TableTools' )
    local args = prepareArgs( frame )

    gaugeDataAll = mw.loadData( dataPageName )
    -- Series from the list
    -- idFrom and idTo are numerical
    local rgList = {}
    local idFrom = -1
    local idTo = -1
    for i, v in ipairs( args ) do
        if v == 'all' then
            idFrom = -math.huge
            idTo = math.huge
            break
        end
    end
    if args.docfrom ~= nil then
        idFrom = tonumber( fromInputToId( args.docfrom )
                 or mw.ustring.gsub( '0' .. args.docfrom, 'mm', '' ) )
        idTo = math.huge
    end
    if args.docto ~= nil then
        idTo = tonumber( fromInputToId( args.docto )
               or mw.ustring.gsub( '0' .. args.docto, 'mm', '' ) )
    end
    if idTo > 0 then -- some list is requested from the whole data set
        if idFrom > idTo then
            local dummy = idFrom
            idFrom = idTo
            idTo = dummy
        end
        for i, rgEntry in ipairs( gaugeDataAll ) do
            if ( tonumber( rgEntry.id ) >= idFrom )
            and ( tonumber( rgEntry.id ) <= idTo ) then
                table.insert( rgList, tonumber( rgEntry.id ) )
            end
        end
        rgList = TableTools.removeDuplicates( rgList )
        table.sort( rgList )
    end

    -- Individual entries can be mentioned in args ( all unnamed = numbered params )
    -- Need a straight table to keep sequence right
    local argsAliases = TableTools.compressSparseArray( args )
    for i, argsAlias in ipairs( argsAliases ) do
        id = fromInputToId( argsAlias )
        if id ~= nil then
            -- Add to the top
            table.insert( rgList, i, tonumber( id ) )
        end
    end
    -- Now loop through the prepared rgList[id] and add rows to result table
    -- One row contains all available entries for the id ( met, imp, a third variant )
    local rowRGid = {}
    for i, numId in ipairs( rgList ) do
        table.insert( rowRGid, fromIdToEntrySet( tostring( numId ) ) )
    end

    return documentHeader( #rgList, args.doctitle or '', args.docstate or '' ) ..
        table.concat( rowRGid, '' ) .. documentFooter()
end

-----------------------------------------------------------------------------------
-- gaugeSizeFromTitle
-- Currently finds "1620 mm" when at end of title, then returns "1620". Blank when not found.
-- Used for cat:mentions category page.
-----------------------------------------------------------------------------------
function p.gaugeSizeFromTitle()
    local title = mw.title.getCurrentTitle()
    return string.match( title.text, '%s(%d+%.?%d*)%smm$' ) or ''
end

-----------------------------------------------------------------------------------
-- catSortFromTitle
-- Currently finds "600 mm" when at end of title, then returns "0600 mm" (for catSort).
-- Blank when not found. Used for cat:mentions category page.
-----------------------------------------------------------------------------------
function p.catSortFromTitle()
    local title = mw.title.getCurrentTitle()
    local catSort = string.match( title.text, '%s(%d+%.?%d*)%smm$' ) or ''
    if catSort ~= '' then
         catSort = string.rep( '0',
                   4 - string.len( math.floor( tonumber( catSort ) ) ) )
                   .. catSort .. ' mm'
    end
    if catSort == '' then
        return '*'
    else
        return catSort
    end
end

return p