Module:Navbox: Difference between revisions

no edit summary
m>Toohool
(fix for empty params)
No edit summary
 
(77 intermediate revisions by 30 users not shown)
Line 1: Line 1:
--
--
-- This module will implement {{Navbox}}
-- This module implements {{Navbox}}
--
--
 
local p = {}
local p = {}
 
local HtmlBuilder = require('Module:HtmlBuilder')
local navbar = require('Module:Navbar')._navbar
local getArgs -- lazily initialized


local args
local args
local frame
local tableRowAdded = false
local border
local border
local listnums = {}
local listnums
local ODD_EVEN_MARKER = '\127_ODDEVEN_\127'
function trim(s)
local RESTART_MARKER = '\127_ODDEVEN0_\127'
    return (mw.ustring.gsub(s, "^%s*(.-)%s*$", "%1"))
local REGEX_MARKER = '\127_ODDEVEN(%d?)_\127'
 
local function striped(wikitext)
-- Return wikitext with markers replaced for odd/even striping.
-- Child (subgroup) navboxes are flagged with a category that is removed
-- by parent navboxes. The result is that the category shows all pages
-- where a child navbox is not contained in a parent navbox.
local orphanCat = '[[Category:Navbox orphans]]'
if border == 'subgroup' and args.orphan ~= 'yes' then
-- No change; striping occurs in outermost navbox.
return wikitext .. orphanCat
end
local first, second = 'odd', 'even'
if args.evenodd then
if args.evenodd == 'swap' then
first, second = second, first
else
first = args.evenodd
second = first
end
end
local changer
if first == second then
changer = first
else
local index = 0
changer = function (code)
if code == '0' then
-- Current occurrence is for a group before a nested table.
-- Set it to first as a valid although pointless class.
-- The next occurrence will be the first row after a title
-- in a subgroup and will also be first.
index = 0
return first
end
index = index + 1
return index % 2 == 1 and first or second
end
end
local regex = orphanCat:gsub('([%[%]])', '%%%1')
return (wikitext:gsub(regex, ''):gsub(REGEX_MARKER, changer)) -- () omits gsub count
end
end
 
function addTableRow(tbl)
local function processItem(item, nowrapitems)
    -- If any other rows have already been added, then we add a 2px gutter row.
if item:sub(1, 2) == '{|' then
    if tableRowAdded then
-- Applying nowrap to lines in a table does not make sense.
        tbl
-- Add newlines to compensate for trim of x in |parm=x in a template.
            .tag('tr')
return '\n' .. item ..'\n'
                .css('height', '2px')
end
                .tag('td')
if nowrapitems == 'yes' then
    end
local lines = {}
   
for line in (item .. '\n'):gmatch('([^\n]*)\n') do
    tableRowAdded = true
local prefix, content = line:match('^([*:;#]+)%s*(.*)')
   
if prefix and not content:match('^<span class="nowrap">') then
    return tbl.tag('tr')
line = prefix .. '<span class="nowrap">' .. content .. '</span>'
end
table.insert(lines, line)
end
item = table.concat(lines, '\n')
end
if item:match('^[*:;#]') then
return '\n' .. item ..'\n'
end
return item
end
end


local function renderNavBar(titleCell)
if args.navbar ~= 'off' and args.navbar ~= 'plain' and not (not args.name and mw.getCurrentFrame():getParent():getTitle():gsub('/sandbox$', '') == 'Template:Navbox') then
titleCell:wikitext(navbar{
args.name,
mini = 1,
fontstyle = (args.basestyle or '') .. ';' .. (args.titlestyle or '') .. ';background:none transparent;border:none;box-shadow:none;padding:0;'
})
end
end


--
--
--  Title row
--  Title row
--
--
function renderTitleRow(tbl)
local function renderTitleRow(tbl)
    if not args.title then return end
if not args.title then return end
 
    local titleRow = addTableRow(tbl)
local titleRow = tbl:tag('tr')
   
 
    if args.titlegroup then
if args.titlegroup then
        titleRow
titleRow
            .tag('th')
:tag('th')
                .attr('scope', 'row')
:attr('scope', 'row')
                .addClass('navbox-group')
:addClass('navbox-group')
                .addClass(args.titlegroupclass)
:addClass(args.titlegroupclass)
                .cssText(args.basestyle)
:cssText(args.basestyle)
                .cssText(args.groupstyle)
:cssText(args.groupstyle)
                .cssText(args.titlegroupstyle)
:cssText(args.titlegroupstyle)
                .wikitext(args.titlegroup)
:wikitext(args.titlegroup)
    end
end
   
 
    local titleCell = titleRow.tag('th').attr('scope', 'col')
local titleCell = titleRow:tag('th'):attr('scope', 'col')
           
 
    if args.titlegroup then
if args.titlegroup then
        titleCell
titleCell
            .css('border-left', '2px solid #fdfdfd')
:css('border-left', '2px solid #fdfdfd')
            .css('width', '100%')
:css('width', '100%')
    end
end
   
 
    local titleColspan = 2
local titleColspan = 2
    if args.imageleft then titleColspan = titleColspan + 1 end
if args.imageleft then titleColspan = titleColspan + 1 end
    if args.image then titleColspan = titleColspan + 1 end
if args.image then titleColspan = titleColspan + 1 end
    if args.titlegroup then titleColspan = titleColspan - 1 end
if args.titlegroup then titleColspan = titleColspan - 1 end
   
 
    titleCell
titleCell
        .cssText(args.basestyle)
:cssText(args.basestyle)
        .cssText(args.titlestyle)
:cssText(args.titlestyle)
        .addClass('navbox-title')
:addClass('navbox-title')
        .attr('colspan', titleColspan)
:attr('colspan', titleColspan)
    renderNavBar(titleCell)
    titleCell
        .tag('div')
            .addClass(args.titleclass)
            .css('font-size', '110%')
            .newline()
            .wikitext(args.title)
end
function renderNavBar(titleCell)
    -- Depending on the presence of the navbar and/or show/hide link, we may need to add a spacer div on the left
    -- or right to keep the title centered.
    local spacerSide = nil


    if args.navbar == 'off' then
renderNavBar(titleCell)
        -- No navbar, and client wants no spacer, i.e. wants the title to be shifted to the left. If there's
        -- also no show/hide link, then we need a spacer on the right to achieve the left shift.
        if args.state == 'plain' then spacerSide = 'right' end
    elseif args.navbar == 'plain' or args.navbar == 'off' or (not args.name and (border == 'subgroup' or border == 'child' or border == 'none')) then
        -- No navbar. Need a spacer on the left to balance out the width of the show/hide link.
        if args.state ~= 'plain' then spacerSide = 'left' end
    else
        -- Will render navbar (or error message). If there's no show/hide link, need a spacer on the right
        -- to balance out the width of the navbar.
        if args.state == 'plain' then spacerSide = 'right' end


        if args.name then
titleCell
            titleCell.wikitext(mw.getCurrentFrame():expandTemplate{ title = 'navbar', args = {
:tag('div')
                args.name,
-- id for aria-labelledby attribute
                mini = 1,
:attr('id', mw.uri.anchorEncode(args.title))
                fontstyle = (args.basestyle or '') .. ';' .. (args.titlestyle or '') ..  ';background:none transparent;border:none;'
:addClass(args.titleclass)
            }})
:css('font-size', '114%')
        else
:css('margin', '0 4em')
            titleCell
:wikitext(processItem(args.title))
                .tag('span')
                    .addClass('error')
                    .css('float', 'left')
                    .css('white-space', 'nowrap')
                    .wikitext('Error: No name provided')
        end
    end
   
    -- Render the spacer div.
    if spacerSide then
        titleCell
            .tag('span')
                .css('float', spacerSide)
                .css('width', '6em')
                .wikitext('&nbsp;')
    end
end
end


--
--
--  Above/Below rows
--  Above/Below rows
--
--
function renderAboveRow(tbl)
 
    if not args.above then return end
local function getAboveBelowColspan()
local ret = 2
    addTableRow(tbl)
if args.imageleft then ret = ret + 1 end
        .tag('td')
if args.image then ret = ret + 1 end
            .addClass('navbox-abovebelow')
return ret
            .addClass(args.aboveclass)
            .cssText(args.basestyle)
            .cssText(args.abovestyle)
            .attr('colspan', getAboveBelowColspan())
            .tag('div')
                .newline()
                .wikitext(args.above)
end
end


function renderBelowRow(tbl)
local function renderAboveRow(tbl)
    if not args.below then return end
if not args.above then return end
   
 
    addTableRow(tbl)
tbl:tag('tr')
        .tag('td')
:tag('td')
            .addClass('navbox-abovebelow')
:addClass('navbox-abovebelow')
            .addClass(args.belowclass)
:addClass(args.aboveclass)
            .cssText(args.basestyle)
:cssText(args.basestyle)
            .cssText(args.belowstyle)
:cssText(args.abovestyle)
            .attr('colspan', getAboveBelowColspan())
:attr('colspan', getAboveBelowColspan())
            .tag('div')
:tag('div')
                .newline()
-- id for aria-labelledby attribute, if no title
                .wikitext(args.below)
:attr('id', args.title and nil or mw.uri.anchorEncode(args.above))
:wikitext(processItem(args.above, args.nowrapitems))
end
end


function getAboveBelowColspan()
local function renderBelowRow(tbl)
    local ret = 2
if not args.below then return end
    if args.imageleft then ret = ret + 1 end
 
    if args.image then ret = ret + 1 end
tbl:tag('tr')
    return ret
:tag('td')
:addClass('navbox-abovebelow')
:addClass(args.belowclass)
:cssText(args.basestyle)
:cssText(args.belowstyle)
:attr('colspan', getAboveBelowColspan())
:tag('div')
:wikitext(processItem(args.below, args.nowrapitems))
end
end
 
--
--
--  List rows
--  List rows
--
--
function renderListRow(tbl, listnum)
local function renderListRow(tbl, index, listnum)
    local row = addTableRow(tbl)
local row = tbl:tag('tr')
   
 
    if listnum == 1 and args.imageleft then
if index == 1 and args.imageleft then
        row
row
            .tag('td')
:tag('td')
                .addClass('navbox-image')
:addClass('noviewer')
                .addClass(args.imageclass)
:addClass('navbox-image')
                .css('width', '0%')
:addClass(args.imageclass)
                .css('padding', '0px 2px 0px 0px')
:css('width', '1px')               -- Minimize width
                .cssText(args.imageleftstyle)
:css('padding', '0px 2px 0px 0px')
                .attr('rowspan', 2 * #listnums - 1)
:cssText(args.imageleftstyle)
                .tag('div')
:attr('rowspan', #listnums)
                    .newline()
:tag('div')
                    .wikitext(args.imageleft)
:wikitext(processItem(args.imageleft))
    end
end
 
    if args['group' .. listnum] then
if args['group' .. listnum] then
        local groupCell = row.tag('th')
local groupCell = row:tag('th')
       
 
        groupCell
-- id for aria-labelledby attribute, if lone group with no title or above
              .attr('scope', 'row')
if listnum == 1 and not (args.title or args.above or args.group2) then
              .addClass('navbox-group')
groupCell
              .addClass(args.groupclass)
:attr('id', mw.uri.anchorEncode(args.group1))
              .cssText(args.basestyle)
end
             
 
        if args.groupwidth then
groupCell
            groupCell.css('width', args.groupwidth)
:attr('scope', 'row')
        end
:addClass('navbox-group')
         
:addClass(args.groupclass)
        groupCell
:cssText(args.basestyle)
            .cssText(args.groupstyle)
:css('width', args.groupwidth or '1%') -- If groupwidth not specified, minimize width
            .cssText(args['group' .. listnum .. 'style'])
 
            .wikitext(args['group' .. listnum])
groupCell
    end
:cssText(args.groupstyle)
   
:cssText(args['group' .. listnum .. 'style'])
    local listCell = row.tag('td')
:wikitext(args['group' .. listnum])
end
 
local listCell = row:tag('td')
 
if args['group' .. listnum] then
listCell
:css('text-align', 'left')
else
listCell:attr('colspan', 2)
end


    if args['group' .. listnum] then
if not args.groupwidth then
        listCell
listCell:css('width', '100%')
            .css('text-align', 'left')
end
            .css('border-left-width', '2px')
            .css('border-left-style', 'solid')
    else
        listCell.attr('colspan', 2)
    end
   
    if not args.groupwidth then  
        listCell.css('width', '100%')
    end
   
    local isOdd = (listnum % 2) == 1
    local rowstyle = args.evenstyle
    if isOdd then rowstyle = args.oddstyle end
    local evenOdd
    if args.evenodd == 'swap' then
        if isOdd then evenOdd = 'even' else evenOdd = 'odd' end
    else
        if isOdd then evenOdd = args.evenodd or 'odd' else evenOdd = args.evenodd or 'even' end
    end
   
    listCell
        .css('padding', '0px')
        .cssText(args.liststyle)
        .cssText(rowstyle)
        .cssText(args['list' .. listnum .. 'style'])
        .addClass('navbox-list')
        .addClass('navbox-' .. evenOdd)
        .addClass(args.listclass)
        .tag('div')
            .css('padding', args.listpadding or '0em 0.25em')
            .newline()
            .wikitext(args['list' .. listnum])


    if listnum == 1 and args.image then
local rowstyle  -- usually nil so cssText(rowstyle) usually adds nothing
        row
if index % 2 == 1 then
            .tag('td')
rowstyle = args.oddstyle
                .addClass('navbox-image')
else
                .addClass(args.imageclass)
rowstyle = args.evenstyle
                .css('width', '0%')
end
                .css('padding', '0px 0px 0px 2px')
 
                .cssText(args.imagestyle)
local listText = args['list' .. listnum]
                .attr('rowspan', 2 * #listnums - 1)
local oddEven = ODD_EVEN_MARKER
                .tag('div')
if listText:sub(1, 12) == '</div><table' then
                    .newline()
-- Assume list text is for a subgroup navbox so no automatic striping for this row.
                    .wikitext(args.image)
oddEven = listText:find('<th[^>]*"navbox%-title"') and RESTART_MARKER or 'odd'
    end
end
listCell
:css('padding', '0px')
:cssText(args.liststyle)
:cssText(rowstyle)
:cssText(args['list' .. listnum .. 'style'])
:addClass('navbox-list')
:addClass('navbox-' .. oddEven)
:addClass(args.listclass)
:addClass(args['list' .. listnum .. 'class'])
:tag('div')
:css('padding', (index == 1 and args.list1padding) or args.listpadding or '0em 0.25em')
:wikitext(processItem(listText, args.nowrapitems))
 
if index == 1 and args.image then
row
:tag('td')
:addClass('noviewer')
:addClass('navbox-image')
:addClass(args.imageclass)
:css('width', '1px')               -- Minimize width
:css('padding', '0px 0px 0px 2px')
:cssText(args.imagestyle)
:attr('rowspan', #listnums)
:tag('div')
:wikitext(processItem(args.image))
end
end
end


Line 263: Line 283:
--  Tracking categories
--  Tracking categories
--
--
function renderTrackingCategories(builder)
 
    if not frame then return end
local function needsHorizontalLists()
   
if border == 'subgroup' or args.tracking == 'no' then
    local s = frame:preprocess('{{#ifeq:{{NAMESPACE}}|{{ns:10}}|1|0}}{{SUBPAGENAME}}')
return false
    if mw.ustring.sub(s, 1, 1) == '0' then return end -- not in template space
end
    local subpage = mw.ustring.lower(mw.ustring.sub(s, 2))
local listClasses = {
    if subpage == 'doc' or subpage == 'sandbox' or subpage == 'testcases' then return end
['plainlist'] = true, ['hlist'] = true, ['hlist hnum'] = true,
   
['hlist hwrap'] = true, ['hlist vcard'] = true, ['vcard hlist'] = true,
    for i, cat in ipairs(getTrackingCategories()) do
['hlist vevent'] = true,
        builder.wikitext('[[Category:' .. cat .. ']]')  
}
    end
return not (listClasses[args.listclass] or listClasses[args.bodyclass])
end
end


function getTrackingCategories()
local function hasBackgroundColors()
    local cats = {}
for _, key in ipairs({'titlestyle', 'groupstyle', 'basestyle', 'abovestyle', 'belowstyle'}) do
    if needsHorizontalLists() then table.insert(cats, 'Navigational boxes without horizontal lists') end
if tostring(args[key]):find('background', 1, true) then
    if hasCustomListSpacing() then table.insert(cats, 'Navigational boxes with custom list spacing') end
return true
    if hasBackgroundColors() then table.insert(cats, 'Navboxes using background colours') end
end
    return cats
end
end
end


function needsHorizontalLists()
local function hasBorders()
    if border == 'child' or border == 'subgroup'  or args.tracking == 'no' then return false end
for _, key in ipairs({'groupstyle', 'basestyle', 'abovestyle', 'belowstyle'}) do
   
if tostring(args[key]):find('border', 1, true) then
    local listClasses = {'plainlist', 'hlist', 'hlist hnum', 'hlist vcard', 'vcard hlist'}
return true
    for i, cls in ipairs(listClasses) do
end
        if args.listclass == cls or args.bodyclass == cls then
end
            return false
end
        end
    end


    return true
local function isIllegible()
local styleratio = require('Module:Color contrast')._styleratio
 
for key, style in pairs(args) do
if tostring(key):match("style$") then
if styleratio{mw.text.unstripNoWiki(style)} < 4.5 then
return true
end
end
end
return false
end
end


function hasCustomListSpacing()
local function getTrackingCategories()
    return args.liststyle == 'padding: 0.25em 0; line-height: 1.3em;' or
local cats = {}
          args.liststyle == 'padding:0.25em 0; line-height:1.4em; width:auto;' or
if needsHorizontalLists() then table.insert(cats, 'Navigational boxes without horizontal lists') end
          args.liststyle == 'padding:0.4em 0; line-height:1.4em;'
if hasBackgroundColors() then table.insert(cats, 'Navboxes using background colours') end
if isIllegible() then table.insert(cats, 'Potentially illegible navboxes') end
if hasBorders() then table.insert(cats, 'Navboxes using borders') end
return cats
end
end


function hasBackgroundColors()
local function renderTrackingCategories(builder)
    return args.titlestyle or args.groupstyle
local title = mw.title.getCurrentTitle()
if title.namespace ~= 10 then return end -- not in template space
local subpage = title.subpageText
if subpage == 'doc' or subpage == 'sandbox' or subpage == 'testcases' then return end
 
for _, cat in ipairs(getTrackingCategories()) do
builder:wikitext('[[Category:' .. cat .. ']]')
end
end
end


--
--
--  Main navbox tables
--  Main navbox tables
--
--
function renderMainTable()
local function renderMainTable()
    local tbl = HtmlBuilder.create('table')
local tbl = mw.html.create('table')
        .attr('cellspacing', 0)
:addClass('nowraplinks')
        .addClass('nowraplinks')
:addClass(args.bodyclass)
        .addClass(args.bodyclass)
 
           
if args.title and (args.state ~= 'plain' and args.state ~= 'off') then
    if args.title and (args.state ~= 'plain' and args.state ~= 'off') then
if args.state == 'collapsed' then args.state = 'mw-collapsed' end
        tbl
tbl
            .addClass('collapsible')
:addClass('mw-collapsible')
            .addClass(args.state or 'autocollapse')
:addClass(args.state or 'autocollapse')
    end
end
 
    tbl.css('border-spacing', 0)
if border == 'subgroup' or border == 'none' then
    if border == 'subgroup' or border == 'child' or border == 'none' then
tbl
        tbl
:addClass('navbox-subgroup')
            .addClass('navbox-subgroup')
:cssText(args.bodystyle)
            .cssText(args.bodyStyle)
:cssText(args.style)
            .cssText(args.style)
else -- regular navbox - bodystyle and style will be applied to the wrapper table
    else -- regular navobx - bodyStyle and style will be applied to the wrapper table
tbl
        tbl
:addClass('navbox-inner')
            .addClass('navbox-inner')
:css('background', 'transparent')
            .css('background', 'transparent')
:css('color', 'inherit')
            .css('color', 'inherit')
end
    end
tbl:cssText(args.innerstyle)
    tbl.cssText(args.innerstyle)
 
renderTitleRow(tbl)
    renderTitleRow(tbl)
renderAboveRow(tbl)
    renderAboveRow(tbl)
for i, listnum in ipairs(listnums) do
    for i, listnum in ipairs(listnums) do
renderListRow(tbl, i, listnum)
        renderListRow(tbl, listnum)  
end
    end
renderBelowRow(tbl)
    renderBelowRow(tbl)
 
   
return tbl
    return tbl
end
end


function p._navbox(navboxArgs)
function p._navbox(navboxArgs)
    args = navboxArgs
args = navboxArgs
   
listnums = {}
    for k, v in pairs(args) do
        if v == '' then
            -- ParserFunctions considers the empty string to be false, so to preserve the previous
            -- behavior of {{navbox}}, change any empty arguments to nil, so Lua will consider
            -- them false too.
            args[k] = nil
        else
            local listnum = ('' .. k):match('^list(%d+)$')
            if listnum then table.insert(listnums, tonumber(listnum)) end
        end
    end
    table.sort(listnums)
    border = trim(args.border or args[1] or '')


    -- render the main body of the navbox
for k, _ in pairs(args) do
    local tbl = renderMainTable()
if type(k) == 'string' then
local listnum = k:match('^list(%d+)$')
if listnum then table.insert(listnums, tonumber(listnum)) end
end
end
table.sort(listnums)


    -- render the appropriate wrapper around the navbox, depending on the border param
border = mw.text.trim(args.border or args[1] or '')
    local res = HtmlBuilder.create()
if border == 'child' then
    if border == 'none' then
border = 'subgroup'
        res.node(tbl)
end
    elseif border == 'subgroup' or border == 'child' then
 
        -- We assume that this navbox is being rendered in a list cell of a parent navbox, and is
-- render the main body of the navbox
        -- therefore inside a div with padding:0em 0.25em. We start with a </div> to avoid the
local tbl = renderMainTable()
        -- padding being applied, and at the end add a <div> to balance out the parent's </div>
 
        res
-- render the appropriate wrapper around the navbox, depending on the border param
            .tag('/div', {unclosed = true})
local res = mw.html.create()
                .done()
if border == 'none' then
            .node(tbl)
local nav = res:tag('div')
            .tag('div', {unclosed = true})
:attr('role', 'navigation')
    else
:node(tbl)
        res
-- aria-labelledby title, otherwise above, otherwise lone group
            .tag('table')
if args.title or args.above or (args.group1 and not args.group2) then
                .attr('cellspacing', 0)
nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
                .addClass('navbox')
else
                .css('border-spacing', 0)
nav:attr('aria-label', 'Navbox')
                .cssText(args.bodystyle)
end
                .cssText(args.style)
elseif border == 'subgroup' then
                .tag('tr')
-- We assume that this navbox is being rendered in a list cell of a parent navbox, and is
                    .tag('td')
-- therefore inside a div with padding:0em 0.25em. We start with a </div> to avoid the
                        .css('padding', '2px')
-- padding being applied, and at the end add a <div> to balance out the parent's </div>
                        .node(tbl)
res
    end
:wikitext('</div>')
:node(tbl)
    renderTrackingCategories(res)
:wikitext('<div>')
else
    return tostring(res)
local nav = res:tag('div')
:attr('role', 'navigation')
:addClass('navbox')
:addClass(args.navboxclass)
:cssText(args.bodystyle)
:cssText(args.style)
:node(tbl)
-- aria-labelledby title, otherwise above, otherwise lone group
if args.title or args.above or (args.group1 and not args.group2) then
nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
else
nav:attr('aria-label', 'Navbox')
end
end
 
if (args.nocat or 'false'):lower() == 'false' then
renderTrackingCategories(res)
end
return striped(tostring(res))
end
end
 
function p.navbox(frm)
function p.navbox(frame)
    frame = frm
if not getArgs then
    return p._navbox(frame:getParent().args)
getArgs = require('Module:Arguments').getArgs
end
args = getArgs(frame, {wrappers = {'Template:Navbox'}})
 
-- Read the arguments in the order they'll be output in, to make references number in the right order.
local _
_ = args.title
_ = args.above
for i = 1, 20 do
_ = args["group" .. tostring(i)]
_ = args["list" .. tostring(i)]
end
_ = args.below
 
return p._navbox(args)
end
end
 
return p
return p