Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Navbox: Difference between revisions

From Gensopedia
mNo edit summary
Tag: Reverted
mNo edit summary
Tag: Reverted
Line 1: Line 1:
-- version 1.1.1


-- config table for RANGER.
local canonicalName = {
-- If you want to change the default config, DO NOT change it here,
-- please do it via the `onLoadConfig` hook in [[Module:Navbox/Hooks]].
local config = {
default_navbox_class = "navigation-not-searchable",  -- Base value of the `class` parameter.
default_title_class = nil,    -- Base value of the `title_class` parameter.
default_above_class = nil,    -- Base value of the `above_class` parameter.
default_below_class = nil,    -- Base value of the `below_class` parameter.
default_section_class =nil,  -- Base value of the `section_class` parameter.
default_header_class = nil,  -- Base value of the `header_class` parameter.
default_group_class = nil,    -- Base value of the `group_class` parameter.
default_list_class = 'hlist', -- Base value of the `list_class` parameter.
default_header_state = nil, -- Base value of the `state` parameter.
 
editlink_hover_message_key = 'Navbox-edit-hover', -- The system message name for hover text of the edit icon.
auto_flatten_top_level = true, -- If true, when a section has only one list with no content and no corresponding group but has sublists, these sublists will be moved to top level.
-- This helps make the hierarchy of sections and content clearer.
-- An example:
-- {{navbox
-- ...
--  |header-1 = Items
--  | group-1.1 = Weapons
--  |  list-1.1 = Swords · Guns · Wands
--  | group-1.2 = Armors
--  |  list-1.2 = Head pieces · Capes
--  |header-2 = NPCs
--  | group-2.1 = Town NPCs
--  |  list-2.1 = Guide · Witch
-- ...
-- }}
-- will be equal to:
-- {{navbox
-- ...
--  |header-1 = Items
--  | group-2 = Weapons
--  |  list-2 = Swords · Guns · Wands
--  | group-3 = Armors
--  |  list-3 = Head pieces · Capes
--  |header-5 = NPCs
--  | group-6 = Town NPCs
--  |  list-6 = Guide · Witch
-- ...
-- }}
custom_render_handle = nil, -- usually for debugging purposes only. if set, it should be a function accept 2 parameters: `dataTree` and `args`, and return a string as module output.
}
 
---------------------------------------------------------------------
 
-- Argument alias.
local CANONICAL_NAMES = {
['titlestyle'] = 'title_style',
['titlestyle'] = 'title_style',
['listclass'] = 'list_class',
['listclass'] = 'list_class',
Line 71: Line 18:
}
}


local STATES = {
 
['no'] = '',
 
['off'] = '',
 
['plain'] = '',
local config = {
['collapsed'] = 'mw-collapsible mw-collapsed',
['default_list_class'] = 'hlist', -- base value of the `list_class` parameter.
['editlink_hover_message_key'] = 'Navbox-edit-hover', -- The system message name for hover text of the edit icon.
}
}


local BOOL_FALSE = {
['no'] = true,
['off'] = true,
['false'] = true,
}


local STRIPED = {
local args = {} -- store nomalized args
['odd'] = 'striped-odd',
local tree = {}
['swap'] = 'striped-odd',
 
['y'] = 'striped-even',
local hooks = {}
['yes'] = 'striped-even',
['on'] = 'striped-even',
['even'] = 'striped-even',
['striped'] = 'striped-even',
}


local DEFAULT_ARGS = {
local listClass -- default class for lists
['meta'] = true,
local listCss
}
local groupClass -- default class for groups
local groupCss
local subgroupClass -- default class for subgroups
local subgroupCss
local headerClass -- default class for headers
local headerCss


local NAVBOX_CHILD_INDICATOR = '!!C$H$I$L$D!!'
local headerState -- default state for all headers
local NAVBOX_CHILD_INDICATOR_LENGTH = string.len( NAVBOX_CHILD_INDICATOR )


local CLASS_PREFIX = 'ranger-'
local nvaboxMainClass = 'ranger-navbox'
local classPrefix = 'ranger-'


---------------------------------------------------------------------
local trim = mw.text.trim


local p = {}
local even -- for zebra stripes
local h = {} -- non-public
local hooks = mw.title.new('Module:Navbox/Hooks').exists and require('Module:Navbox/Hooks') or {}


---------------------------------------------------------------------
---Split the `str` on each `div` in it and return the result as a table. Original
---version credit: http://richard.warburton.it.
---@param div string
---@param str string
---@return string[]? strExploded Is `nil` if `div` is an empty string
local function explode(div, str)
if (div=='') then return nil end
local pos,arr = 0,{}
-- for each divider found
for st,sp in function() return string.find(str,div,pos,true) end do
arr[#arr+1] = string.sub(str,pos,st-1) -- Attach chars left of current divider
pos = sp+1 -- Jump past current divider
end
arr[#arr+1] = string.sub(str,pos) -- Attach chars right of last divider
return arr
end


-- For templates: {{#invoke:navbox|main|...}}
-- Normalize the name string of arguments.
function p.main(frame)
-- space character(" ") will be treat as underscore("_"),
local args = p.mergeArgs(frame)
-- and the name string will be converted to lowercase,
args = h.parseArgs(args)
-- and support underscore between numbers (n_m_l format),
return p.build(args)
-- and support such as group1/list1 prefix.
local function normalize(s)
-- camel-case to lowercase underscore-case
s = string.gsub(s, '(%l)(%u)', '%1_%2')
s = string.lower(string.gsub(s, ' ', '_'))
s = string.gsub(s, '(%l)(%d)', '%1_%2') -- group1* to group_1*
s = string.gsub(s, '(%d)(%l)', '%1_%2') -- *1style to *1_style
-- number format x_y_z to x.y.z
s = string.gsub(s, '(%d)_%f[%d]', '%1%.')
-- standardize *_css to *_style
s = string.gsub(s, '_css$', '_style')
-- standardize all aliases to the canonical name
return canonicalName[s] or s
end
end


-- For modules: return require('module:navbox').build(args)
local function parseArgs(inputArgs)
-- By default this method will skip the arguments sanitizing phase
for k,v in pairs(inputArgs) do
-- (and onSanitizeArgsStart/onSanitizeArgsEnd hooks).
args[normalize(k)] = trim(v)
-- Set `doParseArgs` to true to do arguments sanitizing.
-- If `customConfig` table is provided, it will be merged into default config table.
-- If `customHooks` table is provided, all default hook handles will be overrided, unprovided hooks will be empty.
function p.build(args, doParseArgs, customConfig, customHooks)
if customHooks then
hooks = customHooks
end
end
if customConfig then
end
for k,v in pairs(customConfig) do
 
config[k] = v
-- Used to traverses a table following the order of its keys:
--  for key, value in pairsByKeys(array) do
--    print(key, value)
--  end
local function pairsByKeys(t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0      -- iterator variable
local iter = function ()  -- iterator function
i = i + 1
if a[i] == nil then return nil
else return a[i], t[a[i]]
end
end
end
end
if doParseArgs then  
return iter
args = h.parseArgs(args)
end
 
local function normalizeStateValue(state)
if state == 'no' or state == 'off' or state == 'plain' then
return nil
end
if state == 'collapsed' then
return 'collapsed'
end
end
return true
end


h.runHook('onLoadConfig', config, args)
local function normalizeStripedValue(striped)
if striped == 'odd' or striped == 'swap' then
--merge default args
striped = 'striped-odd'
for k,v in pairs(DEFAULT_ARGS) do
elseif striped == 'y' or striped == 'yes' or striped == 'on' or striped == 'even' or striped == 'striped' then
if args[k] == nil then
striped = 'striped-even'
args[k] = DEFAULT_ARGS[k]
else
striped = nil
end
return striped
end
 
local function makeCollapsible(node, state)
if state then
node:addClass('mw-collapsible')
if state == 'collapsed' then  
node:addClass('mw-collapsed')
end
end
end
end
end
local function runHook(key, ...)
if hooks[key] then
hooks[key](...)
end
end


h.runHook('onBuildTreeStart', args)
local function getArg(name)
local dataTree = h.buildDataTree(args)
if args[name] and args[name] ~= '' then
h.runHook('onBuildTreeEnd', dataTree, args)
return args[name]
if type(config.custom_render_handle) == 'function' then
return config.custom_render_handle(dataTree, args)
else
else
return h.render(dataTree)
return nil
end
end
 
local function getArgGroup(prefix)
if not prefix then
return tree
end
local node = tree
for _, s in ipairs(explode('.', prefix)) do
if not node[s] then error('invaild index: '..prefix) end
node = node[s]['sub']
end
end
return node
end
end


-- merge args from frame and frame:getParent()
local function checkForTreeNode(name, key, value)
-- It may be used when creating custom wrapping navbox module.
local pattern = '^'..name..'_([%.%d]+)$'
--
local index = string.match(key, pattern)
-- For example, Module:PillNavbox
if not index then return end
--
if string.match(index, '^%.') or string.match(index, '%.$') or string.match(index, '%.%.') then return end
-- local RANGER = require('Module:Navbox')
local arr = explode('.', index)
-- local p = {}
n = tonumber(table.remove(arr))
-- function p.main(frame)
local node = tree
--    return RANGER.build(RANGER.mergeArgs(frame), true, {
for _, v in ipairs(arr) do
--        default_navbox_class = 'pill', -- use "pill" style by default.
v = tonumber(v)
--    })
if not node[v] then
-- end
node[v] = {['sub'] = {}}
-- return p
elseif not node[v]['sub'] then
--
node[v]['sub'] = {}
function p.mergeArgs(frame)
local inputArgs = {}
for k, v in pairs(frame.args) do
v = mw.text.trim(tostring(v))
if v ~= '' then
inputArgs[k] = v
end
end
node = node[v]['sub']
end
end
if not node[n] then node[n] = {} end
for k, v in pairs(frame:getParent().args) do
if name == 'list' and string.sub(value, 1, 13) == '!!C$H$I$L$D!!' then
v = mw.text.trim(v)
-- it is from {{navbox|child| ... }}
if v ~= '' then
node[n]['sub'] = mw.text.jsonDecode(string.sub(value, 14))
inputArgs[k] = v
else
end
node[n][name] = value
end
end
return inputArgs
return true
end
end


------------------------------------------------------------------------
local function buildTree()
for k, v in pairs(args) do
local _ = checkForTreeNode('list', k, v) or checkForTreeNode('group', k, v) or checkForTreeNode('header', k, v)
end
return tree
end


function h.parseArgs(inputArgs)
local function getMergedStr(...)
local s = ''
h.runHook('onSanitizeArgsStart', inputArgs)
for i=1, select('#', ...) do
local v = trim(select(i, ...) or '')
local args = {}
local str = string.match(v, '^%-%-+(.*)$')
if str then
for k, v in pairs(inputArgs) do
s = trim(str..' '..s)
-- all args have already been trimmed
break
if type(k) == 'string' then
local key = h.normalizeKey(k)
args[key] = h.normalizeValue(key, v)
else
else
args[k] = mw.text.trim(v) -- keep number-index arguments (for {{navbox|child|...}})
s = trim(v..' '..s)
end
end
end
end
if s == '' then s = nil end
h.runHook('onSanitizeArgsEnd', args, inputArgs)
return s
return args
end
end


-- Normalize the name string of arguments.
local function getCssArg(prefix)
-- the normalized form is (index:)?name, in which:
local css = getArg(prefix..'_style')
-- index is number index such as 1, 1.3, 1.2.45,
if css and (string.sub(css, -1) ~= ';') then
-- name is in lowercase underscore-case, such as group, group_style
css = css..';'
-- e.g: header_state, 1.3:list_style
end
-- the input argument name can be:
return css
-- * camel-case: listStyle, ListStyle
end
-- * space separated: list style
-- Applies class/css to the element
-- * prefix+index+postfix?, and can be in camel-case or space/hyphen separated or mixed: list 1 style, list1, list1Style, list1_style
local function applyStyle(node, prefix, baseClass, baseCss)
-- * index.name: 1.3.list
return node:addClass(getMergedStr(getArg(prefix..'_class'), baseClass)):cssText(getMergedStr(getCssArg(prefix), baseCss))
-- * index_name: 1.3_list (Space separated are treated as underscore separated, therefore 1.3 list are vaild too)
function h.normalizeKey(s)
-- camel-case to lowercase underscore-case
s = s:gsub('%l%f[%u]', '%0_') -- listStyle to list_style
s = (s:gsub(' ', '_')):lower() -- space to underscore
s = s:gsub('%l%f[%d]', '%0_') -- group1* to group_1*
s = s:gsub('%d%f[%l]', '%0_') -- *1style to *1_style
-- number format x_y_z to x.y.z
s = s:gsub('(%d)_%f[%d]', '%1%.')
-- move index to the beginning:
-- group_1.2_style to 1.2:group_style
-- group_1 to 1:group
s = s:gsub('^([%l_]+)_([%d%.]+)', '%2:%1')
-- support index.name and index_name:
-- 1.2.group / 1.2_group to 1.2:group
s = s:gsub('^([%d%.]+)[%._]%f[%l]', '%1:')
-- now the key should be in normalized form, if the origin key is vaild
 
-- standardize *_css to *_style
s = s:gsub('_css$', '_style')
-- standardize all aliases to the canonical name
return CANONICAL_NAMES[s] or s
end
end


function h.normalizeValue(k, v)
local function processItem(item)
k = tostring(k)
if item:sub(1, 2) == '{|' then
if k:find('_style$') then
v = (v .. ';'):gsub(';;', ';')
return v
elseif k == 'striped' then
return STRIPED[v]
elseif v:sub(1, 2) == '{|' or v:match('^[*:;#]') then
-- Applying nowrap to lines in a table does not make sense.
-- Applying nowrap to lines in a table does not make sense.
-- Add newlines to compensate for trim of x in |parm=x in a template.
-- Add newlines to compensate for trim of x in |parm=x in a template.
return '\n' .. v ..'\n'
return '\n' .. item ..'\n'
elseif k == 'meta' then
return not BOOL_FALSE[v]
end
end
return v
if item:match('^[*:;#]') then
end
return '\n' .. item ..'\n'
 
end
-- we need a default value for all empty state_* arguments, so we can not do this in h.normalizeValue()
return item
function h.normalizeStateValue(v)
return STATES[v] or 'mw-collapsible'
end
end


-- parse arguments, convert them to structured data tree
local function renderMetaLinks()
function h.buildDataTree(args)
local name = getArg('name') or mw.getCurrentFrame():getParent():getTitle()
local data = {
state = h.normalizeStateValue(args.state),
striped = args.striped,
class = h.mergeAttrs(args.navbox_class, config.default_navbox_class),
style = args.navbox_style,
}
if args.title or args.meta or data.state ~= '' then
local title = mw.title.new(trim(name), 'Template')
data.title = {
if not title then
content = args.title,
error('Invalid title ' .. name)
class = h.mergeAttrs(args.title_class, config.default_title_class),
style = args.title_style,
}
if args.meta then
data.metaLinks = {
template = args.name or mw.getCurrentFrame():getParent():getTitle()
}
end
end
end
if args.above then
local msg = mw.message.new(config['editlink_hover_message_key'])
data.above = {
local hoverText = msg:exists() and msg:plain() or 'View or edit this template'
content = args.above,
class= h.mergeAttrs(args.above_class, config.default_above_class),
return mw.html.create('span'):addClass(classPrefix..'meta')
style = args.above_style,
:tag('span'):addClass('nv nv-view')
}
:wikitext('[['..title.fullText..'|')
end
:tag('span'):wikitext(hoverText):attr('title', hoverText):done()
:wikitext(']]')
:done()
end


if args.below then
local function renderTitleBar(title, collapsible, metaLinks)
data.below = {
local titlebar = applyStyle(mw.html.create('div'):addClass(classPrefix..'title'), 'title', config['default_title_class'])
content = args.below,
if metaLinks then
class= h.mergeAttrs(args.below_class, config.default_below_class),
titlebar:node(renderMetaLinks())
style = args.below_style,
}
end
end
if title then
local tree = h.buildTree(args, {
titlebar:tag('div')
listClass = h.mergeAttrs(args.list_class, config.default_list_class),
:attr('id', mw.uri.anchorEncode(title) or '') -- id for aria-labelledby attribute
listStyle =  args.list_style,
:addClass(classPrefix..'title-text')
groupClass = h.mergeAttrs(args.group_class, config.default_group_class),
:wikitext(processItem(title))
groupStyle = args.group_style,
headerClass = h.mergeAttrs(args.header_class, config.default_header_class),
headerStyle = args.header_style,
})
-- handle {{navbox|child|...}} syntax:
if args[1] == 'child' then
return NAVBOX_CHILD_INDICATOR..mw.text.jsonEncode(tree)
end
end
return titlebar
-- normal case
end
local sectionClass = h.mergeAttrs(args.section_class, config.default_section_class)
 
local sectionStyle = args.section_style
local function renderAboveBox(above, id)
local headerState = args.header_state or config.default_header_state
local node = mw.html.create('div')
data.sections = {}
:addClass(classPrefix..'above mw-collapsible-content')
local section
-- id for aria-labelledby attribute, if no title
for k, v in h.orderedPairs(tree or {}) do
:attr('id', id and mw.uri.anchorEncode(above) or nil)
if v.header or not section then
:wikitext(processItem(above))
--start a new section
return applyStyle(node, 'above', config['default_above_class'])
section = {
end
class = h.mergeAttrs(args[k..':section_class'], sectionClass),
 
style = h.mergeAttrs(args[k..':section_style'], sectionStyle),
local function renderBelowBox(below)
body = {},
local node = mw.html.create('div')
}
:addClass(classPrefix..'below mw-collapsible-content')
-- Section header if needed.
:wikitext(processItem(below))
-- If the value of a `|header_n=` is two or more consecutive "-" characters (e.g. --, -----),
return applyStyle(node, 'below', config['default_below_class'])
-- it means start a new section without header, and the new section will be not collapsable.
end
if v.header and not string.match(v.header.content, '^%-%-+$') then
 
section.header =v.header
local function renderSectionHeader(content, index)
section.state = h.normalizeStateValue(args[k..':state'] or headerState)
local node =  mw.html.create('div'):addClass(classPrefix..'header')
:tag('div'):addClass(classPrefix..'header-text'):wikitext(processItem(content))
:done()
return applyStyle(node, 'header_'..index, headerClass, getCssArg('header'))
end
 
local function renderList(content, index, level)
even = not even -- flip even/odd status
local node = mw.html.create('div'):addClass(classPrefix..'wrap'):addClass(even and classPrefix..'even' or classPrefix..'odd')
:tag('div'):addClass(classPrefix..'list'):wikitext(processItem(content))
:done()
return applyStyle(node, 'list_'..index,
getMergedStr(getArg('list_level_'..level..'_class'), listClass),
getMergedStr(getCssArg('list_level_'..level), config['default_list_level_'..level..'_class'], listCss)
)
end
 
local renderRow, renderSublist
function renderRow(box, v, k, level)
if v['group'] or v['list'] or v['sub'] then
local row = box:tag('div'):addClass(classPrefix..'row')
if v['group'] or (v['sub'] and level > 0 and not v['group'] and not v['list']) then
local groupCell = row:tag('div')
if level == 0 then
groupCell:addClass(classPrefix..'group')
applyStyle(groupCell, 'group_'..k, groupClass, groupCss)
else
groupCell:addClass(classPrefix..'subgroup level-'..level)
:addClass(getMergedStr(
  getArg('group_'..k..'_class'),
  getArg('subgroup_'..k..'_class'),
  getArg('subgroup_level_'..level..'_class'),
  config['default_subgroup_level_'..level..'_class'],
  subgroupClass
))
:cssText(getMergedStr(
  getCssArg('group_'..k),
  getCssArg('subgroup_'..k),
  getCssArg('subgroup_level_'..level),
  subgroupCss
))
end
groupCell:tag('div'):addClass(classPrefix..'wrap'):wikitext(processItem(v['group'] or ''))
if not v['group'] then
groupCell:addClass('empty')
row:addClass('empty-group-list')
end
end
v.header = nil
else
data.sections[#data.sections+1] = section
row:addClass('empty-group')
end
end
-- check for section above/below areas
local listCell = row:tag('div'):addClass(classPrefix..'listbox')
if v.above then
if not v['list'] and not v['sub'] then
section.above = v.above
listCell:addClass('empty')
v.above = nil
row:addClass('empty-list')
end
end
if v.below then
if v['list'] or (v['group'] and not v['sub']) then
section.below = v.below
listCell:node(renderList(v['list'] or '', k, level))
v.below = nil
end
end
if next(v) then -- v is not empty (with group/list/sub)
if v['sub'] then
section.body[#section.body+1] = v
listCell:node(renderSublist(v['sub'], k, level+1))
end
end
return box
end
end
if config.auto_flatten_top_level then
for _, sect in ipairs(data.sections) do
if #sect.body == 1 then
local node = sect.body[1]
if not node.group and not node.list and node.sub then
sect.body = node.sub
end
end
end
end
return data
end
end
 
function renderSublist(l, prefix, level)
function h.buildTree(args, defaults)
local count = 0
local tree = {}
local box = mw.html.create('div'):addClass(classPrefix..'sublist level-'..level)
local check = function(key, value)
for k,v in pairsByKeys(l) do
local index, name = string.match(key, '^([%d%.]+):(.+)$')
count = count + tonumber(renderRow(box, v, prefix..'.'..k, level) and 1 or 0)
 
if not index then return end -- no number index found
if name ~= 'list' and name ~= 'group' and name ~= 'header' and name ~= 'above' and name ~= 'below' then return end -- check only the names we are interested in
if string.match(index, '^%.') or string.match(index, '%.$') or string.match(index, '%.%.') then return end -- invalid number index
-- find the node that matches the index in the tree
local arr = mw.text.split(index, '.', true)
n = tonumber(table.remove(arr))
local node = tree
for _, v in ipairs(arr) do
v = tonumber(v)
if not node[v] then
node[v] = {['sub'] = {}}
elseif not node[v]['sub'] then
node[v]['sub'] = {}
end
node = node[v]['sub']
end
if not node[n] then node[n] = {} end
if name == 'list' and string.sub(value, 1, NAVBOX_CHILD_INDICATOR_LENGTH) == NAVBOX_CHILD_INDICATOR then
-- it is from {{navbox|child| ... }}
node[n]['sub'] = mw.text.jsonDecode(string.sub(value, NAVBOX_CHILD_INDICATOR_LENGTH+1))
else
node[n][name] = {
content = value,
class= h.mergeAttrs(args[key..'_class'], defaults[name..'Class']),
style = h.mergeAttrs(args[key..'_style'], defaults[name..'Style'])
}
end
end
end
for k,v in pairs(args) do
if count > 0 then
check(k, v)
return box:css('--count', count)
end
end
return tree
end
end


function h.render(data)
local function build(inputArgs)
-- handle {{navbox|child|...}} syntax
if mw.title.new('Module:Navbox/Hooks').exists then
if type(data) == 'string' then
hooks = require('Module:Navbox/Hooks')
return data
end
end


----- normal case -----
runHook('onParseArgs', inputArgs)
parseArgs(inputArgs)
buildTree()
listClass = getMergedStr(getArg('list_class'), config['default_list_class'])
listCss = getCssArg('list')
groupClass = getMergedStr(getArg('group_class'), config['default_group_class'])
groupCss = getCssArg('group')
subgroupClass = getMergedStr(getArg('subgroup_class'), config['default_subgroup_class'])
subgroupCss = getCssArg('subgroup')
headerClass = getMergedStr(getArg('header_class'), config['default_header_class'])
headerCss = getCssArg('header')
headerState = getArg('header_state')
local out = mw.html.create()
local res = mw.html.create()
local collapsible = normalizeStateValue(getArg('state'))
local metaLinks = normalizeStateValue(getArg('meta'))
local title = getArg('title')
local above = getArg('above')
local below = getArg('below')
local striped = normalizeStripedValue(getArg('striped'))
-- build navbox container
-- build navbox container
local navbox = out:tag('div')
:attr('role', 'navigation'):attr('aria-label', 'Navbox')
local navboxClass = getMergedStr(getArg('navbox_class'), config['default_navbox_class'])
:addClass(CLASS_PREFIX..'navbox')
local nav = res:tag('div')
:addClass(data.class)
:attr('role', 'navigation')
:addClass(data.striped)
:addClass(nvaboxMainClass)
:addClass(data.state)
:addClass(navboxClass)
:cssText(data.style)
:addClass(striped)
 
:cssText(getCssArg('navbox'))
--title bar
makeCollapsible(nav, collapsible)
-- aria-labelledby title, otherwise above
if title or above then
nav:attr('aria-labelledby', mw.uri.anchorEncode(title or above))
else
nav:attr('aria-label', 'Navbox')
end
-- title bar
if title or collapsible or metaLinks then
if title or collapsible or metaLinks then
nav:node(renderTitleBar(title, collapsible, metaLinks))
nav:node(renderTitleBar(title, collapsible, metaLinks))
end
end
 
--above
-- above
if data.above then
if above then
navbox:tag('div')
nav:node(renderAboveBox(above, not title))
:addClass(CLASS_PREFIX..'above mw-collapsible-content')
:addClass(data.above.class)
:cssText(data.above.style)
:wikitext(data.above.content)
:attr('id', (not data.title) and mw.uri.anchorEncode(data.above.content) or nil) -- id for aria-labelledby attribute, if no title
end
end
-- sections
-- sections
for i,sect in ipairs(data.sections) do
local section, box
--section box
local sectionClass = getMergedStr(getArg('section_class'), config['default_section_class'])
local section = navbox:tag('div')
for k,v in pairsByKeys(tree) do
:addClass(CLASS_PREFIX..'section mw-collapsible-content')
--start a new section
:addClass(sect.class)
if v['header'] or not section then
:addClass(sect.state)
section = nav:tag('div'):addClass(classPrefix..'section mw-collapsible-content')
:cssText(sect.style)
applyStyle(section, 'section_'..k, sectionClass)
-- section header
even = true -- reset even/odd status
if sect.header then
if v['header'] then
section:tag('div')
local state = 'plain'
:addClass(CLASS_PREFIX..'header')
if getMergedStr(v['header']) then
:addClass(sect.header.class)
section:node(renderSectionHeader(v['header'], k))
:cssText(sect.header.style)
state = getArg('state_'..k) or headerState
:tag('div'):addClass('mw-collapsible-toggle-placeholder'):done()
end
:tag('div'):addClass(CLASS_PREFIX..'header-text'):wikitext(sect.header.content)
makeCollapsible(section, normalizeStateValue(state))
end
end
-- above:
box = section:tag('div'):addClass(classPrefix..'section-body mw-collapsible-content')
if sect.above then
section:tag('div')
:addClass(CLASS_PREFIX..'above mw-collapsible-content')
:addClass(sect.above.class)
:cssText(sect.above.style)
:wikitext(sect.above.content)
end
-- body: groups&lists
local box = section:tag('div'):addClass(CLASS_PREFIX..'section-body mw-collapsible-content')
h.renderBody(sect.body, box, 0, true) -- reset even status each section
-- below:
if sect.below then
section:tag('div')
:addClass(CLASS_PREFIX..'below mw-collapsible-content')
:addClass(sect.below.class)
:cssText(sect.below.style)
:wikitext(sect.below.content)
end
end
renderRow(box, v, k, 0)
end
end
-- Insert a blank section for completely empty navbox to ensure it behaves correctly when collapsed.
if #data.sections == 0 and not data.above and not data.below then  
-- Add a blank section for completely empty navbox to ensure it behaves correctly when collapsed.
navbox:tag('div'):addClass(CLASS_PREFIX..'section mw-collapsible-content')
if not section and not above and not below then  
nav:tag('div'):addClass(classPrefix..'section mw-collapsible-content')
end
end
 
--below
-- below
if data.below then
if below then
navbox:tag('div')
nav:node(renderBelowBox(below))
:addClass(CLASS_PREFIX..'below mw-collapsible-content')
:addClass(data.below.class)
:cssText(data.below.style)
:wikitext(data.below.content)
end
end


return out
return tostring(res)
end
end


function h.renderMetaLinks(info)
---------------------------------------------------------------------
local title = mw.title.new(mw.text.trim(info.template), 'Template')
return {
if not title then
navbox = function(frame)
error('Invalid title ' .. info.template)
local inputArgs = {}
end
for k, v in pairs(frame.args) do
if type(k) == 'string' then
local msg = mw.message.new(config.editlink_hover_message_key)
v = trim(tostring(v))
local hoverText = msg:exists() and msg:plain() or 'View or edit this template'
if v ~= '' then
inputArgs[k] = v
return mw.html.create('span'):addClass(CLASS_PREFIX..'meta')
:tag('span'):addClass('nv nv-view')
:wikitext('[['..title.fullText..'|')
:tag('span'):wikitext(hoverText):attr('title', hoverText):done()
:wikitext(']]')
:done()
end
 
function h.renderBody(info, box, level, even)
local count = 0
for _,v in h.orderedPairs(info) do
if v.group or v.list or v.sub then
count = count + 1
-- row container
local row = box:tag('div'):addClass(CLASS_PREFIX..'row')
-- group cell
if v.group or (v.sub and level > 0 and not v.list) then
local groupCell = row:tag('div')
:addClass(CLASS_PREFIX..'group level-'..level)
:addClass((level > 0) and CLASS_PREFIX..'subgroup' or nil)
local groupContentWrap = groupCell:tag('div'):addClass(CLASS_PREFIX..'wrap')
if v.group then
groupCell:addClass(v.group.class):cssText(v.group.style)
groupContentWrap:wikitext(v.group.content)
else
groupCell:addClass('empty')
row:addClass('empty-group-list')
end
end
else
row:addClass('empty-group')
end
end
-- list cell
end
local listCell = row:tag('div'):addClass(CLASS_PREFIX..'listbox')
for k, v in pairs(frame:getParent().args) do
if not v.list and not v.sub then
if type(k) == 'string' then
listCell:addClass('empty')
v = trim(v)
row:addClass('empty-list')
if v ~= '' then
end
inputArgs[k] = v
if v.list or (v.group and not v.sub) then
--listCell:node(h.renderList(v['list'] or '', k, level, args))
even = not even -- flip even/odd status
local cell = listCell:tag('div')
:addClass(CLASS_PREFIX..'wrap')
:addClass(even and CLASS_PREFIX..'even' or CLASS_PREFIX..'odd')
if v.list then
cell:addClass(v.list.class):cssText(v.list.style)
:tag('div'):addClass(CLASS_PREFIX..'list'):wikitext(v.list.content)
end
end
end
end
if v.sub then
local sublistBox = listCell:tag('div'):addClass(CLASS_PREFIX..'sublist level-'..level)
even = h.renderBody(v.sub, sublistBox, level+1, even)
end
end
end
if count > 0 then
box:css('--count', count) -- for flex-grow
end
return even
end
-- pairs, but sort the keys alphabetically
function h.orderedPairs(t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0      -- iterator variable
local iter = function ()  -- iterator function
i = i + 1
if a[i] == nil then return nil
else return a[i], t[a[i]]
end
end
end
if trim(frame.args[1] or frame:getParent().args[1] or '') == 'child' then
return iter
parseArgs(inputArgs)
end
return '!!C$H$I$L$D!!'..mw.text.jsonEncode(buildTree())
 
-- For cascading parameters, such as style or class, they are merged in exact order (from general to specific).
-- Any parameter starting with multiple hyphens(minus signs) will terminate the cascade.
-- An example:
-- For group_1.1, its style is affected by parameters |group_1.1_style=... , |subgroup_level_1_style=... , and |subgroup_style=... .
-- If we have |group_1.1_style= color:red; |subgroup_level_1_style= font-weight: bold; and |subgroup_style= color: green; ,
-- the style of group_1.1 will be style="color:green; font-weight: bold; color: red;" ;
-- if we have |group_1.1_style= -- color:red; |subgroup_level_1_style= font-weight: bold; and |subgroup_style= color: green; ,
-- the style of group_1.1 will be style="color: red;" only, and the cascade is no longer performed for |subgroup_level_1_style and |subgroup_style.
function h.mergeAttrs(...)
local trim = mw.text.trim
local s = ''
for i=1, select('#', ...) do
local v = trim(select(i, ...) or '')
local str = string.match(v, '^%-%-+(.*)$')
if str then
s = trim(str..' '..s)
break
else
else
s = trim(v..' '..s)
return build(inputArgs)
end
end
end
end,
if s == '' then s = nil end
return s
build = build, -- for other modules. e.g: return require('module:navbox').build(args)
end
}
 
function h.runHook(key, ...)
if hooks[key] then
hooks[key](...)
end
end


-----------------------------------------------
-- version: r59 2024.10.28 --
return p

Revision as of 03:25, 1 January 2025

Documentation for this module may be created at Module:Navbox/doc


local canonicalName = {
	['titlestyle'] = 'title_style',
	['listclass'] = 'list_class',
	['groupstyle'] = 'group_style',
	['collapsible'] = 'state',
	['editlink'] = 'meta',
	['editlinks'] = 'meta',
	['editicon'] = 'meta',
	['edit_link'] = 'meta',
	['edit_links'] = 'meta',
	['edit_icon'] = 'meta',
	['navbar'] = 'meta',
	['evenodd'] = 'striped',
	['class'] = 'navbox_class',
	['css'] = 'navbox_style',
	['style'] = 'navbox_style',
}




local config = {
	['default_list_class'] = 'hlist', -- base value of the `list_class` parameter.
	['editlink_hover_message_key'] = 'Navbox-edit-hover', -- The system message name for hover text of the edit icon. 
}


local args = {} -- store nomalized args
local tree = {}

local hooks = {}

local listClass -- default class for lists
local listCss
local groupClass -- default class for groups
local groupCss
local subgroupClass -- default class for subgroups
local subgroupCss
local headerClass -- default class for headers
local headerCss

local headerState -- default state for all headers

local nvaboxMainClass = 'ranger-navbox'
local classPrefix = 'ranger-'

local trim = mw.text.trim

local even -- for zebra stripes

---Split the `str` on each `div` in it and return the result as a table. Original
---version credit: http://richard.warburton.it.
---@param div string
---@param str string
---@return string[]? strExploded Is `nil` if `div` is an empty string
local function explode(div, str)
	if (div=='') then return nil end
	local pos,arr = 0,{}
	-- for each divider found
	for st,sp in function() return string.find(str,div,pos,true) end do
		arr[#arr+1] = string.sub(str,pos,st-1) -- Attach chars left of current divider
		pos = sp+1 -- Jump past current divider
	end
	arr[#arr+1] = string.sub(str,pos) -- Attach chars right of last divider
	return arr
end

-- Normalize the name string of arguments.
-- space character(" ") will be treat as underscore("_"),
-- and the name string will be converted to lowercase,
-- and support underscore between numbers (n_m_l format),
-- and support such as group1/list1 prefix.
local function normalize(s)
	-- camel-case to lowercase underscore-case
	s = string.gsub(s, '(%l)(%u)', '%1_%2') 
	s = string.lower(string.gsub(s, ' ', '_'))
	s = string.gsub(s, '(%l)(%d)', '%1_%2') -- group1* to group_1*
	s = string.gsub(s, '(%d)(%l)', '%1_%2') -- *1style to *1_style
	
	-- number format x_y_z to x.y.z
	s = string.gsub(s, '(%d)_%f[%d]', '%1%.')
	
	-- standardize *_css to *_style
	s = string.gsub(s, '_css$', '_style')
	
	-- standardize all aliases to the canonical name
	return canonicalName[s] or s
end

local function parseArgs(inputArgs)
	for k,v in pairs(inputArgs) do
		args[normalize(k)] = trim(v)
	end
end

-- Used to traverses a table following the order of its keys:
--  for key, value in pairsByKeys(array) do
--    print(key, value)
--  end
local function pairsByKeys(t, f)
	local a = {}
	for n in pairs(t) do table.insert(a, n) end
	table.sort(a, f)
	local i = 0      -- iterator variable
	local iter = function ()   -- iterator function
		i = i + 1
		if a[i] == nil then return nil
		else return a[i], t[a[i]]
		end
	end
	return iter
end

local function normalizeStateValue(state)
	if state == 'no' or state == 'off' or state == 'plain' then
		return nil
	end
	if state == 'collapsed' then
		return 'collapsed'
	end
	return true
end

local function normalizeStripedValue(striped)
	if striped == 'odd' or striped == 'swap' then
		striped = 'striped-odd'
	elseif striped == 'y' or striped == 'yes' or striped == 'on' or striped == 'even' or striped == 'striped' then
		striped = 'striped-even'
	else
		striped = nil
	end
	return striped
end

local function makeCollapsible(node, state)
	if state then
		node:addClass('mw-collapsible')
		if state == 'collapsed' then 
			node:addClass('mw-collapsed')
		end
	end
end


local function runHook(key, ...)
	if hooks[key] then
		hooks[key](...)
	end
end

local function getArg(name)
	if args[name] and args[name] ~= '' then
		return args[name]
	else
		return nil
	end
end

local function getArgGroup(prefix)
	if not prefix then
		return tree
	end
	local node = tree
	for _, s in ipairs(explode('.', prefix)) do
		if not node[s] then error('invaild index: '..prefix) end
		node = node[s]['sub']
	end
	return node
end

local function checkForTreeNode(name, key, value)
	local pattern = '^'..name..'_([%.%d]+)$'
	local index = string.match(key, pattern)
	if not index then return end
	if string.match(index, '^%.') or string.match(index, '%.$') or string.match(index, '%.%.') then return end
	local arr = explode('.', index)
	n = tonumber(table.remove(arr))
	local node = tree
	for _, v in ipairs(arr) do
		v = tonumber(v)
		if not node[v] then
			node[v] = {['sub'] = {}} 
		elseif not node[v]['sub'] then
			node[v]['sub'] = {}
		end
		node = node[v]['sub']
	end
	if not node[n] then node[n] = {} end
	
	if name == 'list' and string.sub(value, 1, 13) == '!!C$H$I$L$D!!' then
		-- it is from {{navbox|child| ... }}
		node[n]['sub'] = mw.text.jsonDecode(string.sub(value, 14))
	else
		node[n][name] = value
	end
	
	return true
end

local function buildTree()
	for k, v in pairs(args) do
		local _ = checkForTreeNode('list', k, v) or checkForTreeNode('group', k, v) or checkForTreeNode('header', k, v)
	end
	return tree
end

local function getMergedStr(...)
	local s = ''
	for i=1, select('#', ...) do
		local v = trim(select(i, ...) or '')
		local str = string.match(v, '^%-%-+(.*)$')
		if str then
			s = trim(str..' '..s)
			break
		else
			s = trim(v..' '..s)
		end
	end
	if s == '' then s = nil end
	return s
end

local function getCssArg(prefix)
	local css = getArg(prefix..'_style')
	if css and (string.sub(css, -1) ~= ';') then
		css = css..';'
	end
	return css
end
-- Applies class/css to the element
local function applyStyle(node, prefix, baseClass, baseCss)
	return node:addClass(getMergedStr(getArg(prefix..'_class'), baseClass)):cssText(getMergedStr(getCssArg(prefix), baseCss))
end

local function processItem(item)
	if item:sub(1, 2) == '{|' then
		-- Applying nowrap to lines in a table does not make sense.
		-- Add newlines to compensate for trim of x in |parm=x in a template.
		return '\n' .. item ..'\n'
	end
	if item:match('^[*:;#]') then
		return '\n' .. item ..'\n'
	end
	return item
end

local function renderMetaLinks()
	local name = getArg('name') or mw.getCurrentFrame():getParent():getTitle()
	
	local title = mw.title.new(trim(name), 'Template')
	if not title then
		error('Invalid title ' .. name)
	end
	
	local msg = mw.message.new(config['editlink_hover_message_key'])
	local hoverText = msg:exists() and msg:plain() or 'View or edit this template'
	
	return mw.html.create('span'):addClass(classPrefix..'meta')
		:tag('span'):addClass('nv nv-view')
			:wikitext('[['..title.fullText..'|')
			:tag('span'):wikitext(hoverText):attr('title', hoverText):done()
			:wikitext(']]')
		:done()
end

local function renderTitleBar(title, collapsible, metaLinks)
	local titlebar = applyStyle(mw.html.create('div'):addClass(classPrefix..'title'), 'title', config['default_title_class'])
	if metaLinks then
		titlebar:node(renderMetaLinks())
	end
	if title then
		titlebar:tag('div')
			:attr('id', mw.uri.anchorEncode(title) or '') -- id for aria-labelledby attribute
			:addClass(classPrefix..'title-text')
			:wikitext(processItem(title))
	end
	return titlebar
end

local function renderAboveBox(above, id)
	local node = mw.html.create('div')
		:addClass(classPrefix..'above mw-collapsible-content')
		-- id for aria-labelledby attribute, if no title
		:attr('id', id and mw.uri.anchorEncode(above) or nil)
		:wikitext(processItem(above))
	return applyStyle(node, 'above', config['default_above_class'])
end

local function renderBelowBox(below)
	local node = mw.html.create('div')
		:addClass(classPrefix..'below mw-collapsible-content')
		:wikitext(processItem(below))
	return applyStyle(node, 'below', config['default_below_class'])
end

local function renderSectionHeader(content, index)
	local node =  mw.html.create('div'):addClass(classPrefix..'header')
		:tag('div'):addClass(classPrefix..'header-text'):wikitext(processItem(content))
		:done()
	return applyStyle(node, 'header_'..index, headerClass, getCssArg('header'))
end

local function renderList(content, index, level)
	even = not even -- flip even/odd status
	local node = mw.html.create('div'):addClass(classPrefix..'wrap'):addClass(even and classPrefix..'even' or classPrefix..'odd')
		:tag('div'):addClass(classPrefix..'list'):wikitext(processItem(content))
		:done()
	return applyStyle(node, 'list_'..index, 
		getMergedStr(getArg('list_level_'..level..'_class'), listClass),
		getMergedStr(getCssArg('list_level_'..level), config['default_list_level_'..level..'_class'], listCss)
	)
end

local renderRow, renderSublist
function renderRow(box, v, k, level)
	if v['group'] or v['list'] or v['sub'] then
		local row = box:tag('div'):addClass(classPrefix..'row')
		if v['group'] or (v['sub'] and level > 0 and not v['group'] and not v['list']) then
			local groupCell = row:tag('div')
			if level == 0 then
				groupCell:addClass(classPrefix..'group')
				applyStyle(groupCell, 'group_'..k, groupClass, groupCss)
			else
				groupCell:addClass(classPrefix..'subgroup level-'..level)
				:addClass(getMergedStr(
				  getArg('group_'..k..'_class'),
				  getArg('subgroup_'..k..'_class'),
				  getArg('subgroup_level_'..level..'_class'),
				  config['default_subgroup_level_'..level..'_class'],
				  subgroupClass
				))
				:cssText(getMergedStr(
				  getCssArg('group_'..k),
				  getCssArg('subgroup_'..k),
				  getCssArg('subgroup_level_'..level),
				  subgroupCss
				))
			end
			groupCell:tag('div'):addClass(classPrefix..'wrap'):wikitext(processItem(v['group'] or ''))
			if not v['group'] then
				groupCell:addClass('empty')
				row:addClass('empty-group-list')
			end
		else
				row:addClass('empty-group')
		end
		local listCell = row:tag('div'):addClass(classPrefix..'listbox')
		if not v['list'] and not v['sub'] then
			listCell:addClass('empty')
			row:addClass('empty-list')
		end
		if v['list'] or (v['group'] and not v['sub']) then
			listCell:node(renderList(v['list'] or '', k, level))
		end
		if v['sub'] then
			listCell:node(renderSublist(v['sub'], k, level+1))
		end
		return box
	end
end
function renderSublist(l, prefix, level)
	local count = 0
	local box = mw.html.create('div'):addClass(classPrefix..'sublist level-'..level)
	for k,v in pairsByKeys(l) do
		count = count + tonumber(renderRow(box, v, prefix..'.'..k, level) and 1 or 0)
	end
	if count > 0 then
		return box:css('--count', count)
	end
end

local function build(inputArgs)
	if mw.title.new('Module:Navbox/Hooks').exists then
		hooks = require('Module:Navbox/Hooks')
	end

	runHook('onParseArgs', inputArgs)
	parseArgs(inputArgs)
	
	buildTree()
	listClass = getMergedStr(getArg('list_class'), config['default_list_class'])
	listCss =  getCssArg('list')
	groupClass = getMergedStr(getArg('group_class'), config['default_group_class'])
	groupCss = getCssArg('group')
	subgroupClass = getMergedStr(getArg('subgroup_class'), config['default_subgroup_class'])
	subgroupCss = getCssArg('subgroup')
	headerClass = getMergedStr(getArg('header_class'), config['default_header_class'])
	headerCss = getCssArg('header')
	
	headerState = getArg('header_state')
	
	local res = mw.html.create()
	
	local collapsible = normalizeStateValue(getArg('state'))
	local metaLinks = normalizeStateValue(getArg('meta'))
	local title = getArg('title')
	local above = getArg('above')
	local below = getArg('below')
	local striped = normalizeStripedValue(getArg('striped'))

	-- build navbox container
	
	local navboxClass = getMergedStr(getArg('navbox_class'), config['default_navbox_class'])
	local nav = res:tag('div')
		:attr('role', 'navigation')
		:addClass(nvaboxMainClass)
		:addClass(navboxClass)
		:addClass(striped)
		:cssText(getCssArg('navbox'))
	makeCollapsible(nav, collapsible)
	-- aria-labelledby title, otherwise above
	if title or above then
		nav:attr('aria-labelledby', mw.uri.anchorEncode(title or above))
	else
		nav:attr('aria-label', 'Navbox')
	end
	
	-- title bar
	if title or collapsible or metaLinks then
		nav:node(renderTitleBar(title, collapsible, metaLinks))
	end
	
	-- above
	if above then
		nav:node(renderAboveBox(above, not title))
	end
	
	-- sections
	local section, box
	local sectionClass = getMergedStr(getArg('section_class'), config['default_section_class'])
	for k,v in pairsByKeys(tree) do
		--start a new section
		if v['header'] or not section then
			section = nav:tag('div'):addClass(classPrefix..'section mw-collapsible-content')
			applyStyle(section, 'section_'..k, sectionClass)
			even = true -- reset even/odd status
			if v['header'] then
				local state = 'plain'
				if getMergedStr(v['header']) then
					section:node(renderSectionHeader(v['header'], k))
					state = getArg('state_'..k) or headerState
				end
				makeCollapsible(section, normalizeStateValue(state))
			end
			box = section:tag('div'):addClass(classPrefix..'section-body mw-collapsible-content')
		end
		renderRow(box, v, k, 0)
	end
	
	-- Add a blank section for completely empty navbox to ensure it behaves correctly when collapsed.
	if not section and not above and not below then 
		nav:tag('div'):addClass(classPrefix..'section mw-collapsible-content')
	end
	
	-- below
	if below then
		nav:node(renderBelowBox(below))
	end

	return tostring(res)
end

---------------------------------------------------------------------
return {
	navbox = function(frame)
		local inputArgs = {}
		for k, v in pairs(frame.args) do
			if type(k) == 'string' then
				v = trim(tostring(v))
				if v ~= '' then
					inputArgs[k] = v
				end
			end
		end
		for k, v in pairs(frame:getParent().args) do
			if type(k) == 'string' then
				v = trim(v)
				if v ~= '' then
					inputArgs[k] = v
				end
			end
		end
		if trim(frame.args[1] or frame:getParent().args[1] or '') == 'child' then
			parseArgs(inputArgs)
			return '!!C$H$I$L$D!!'..mw.text.jsonEncode(buildTree())
		else
			return build(inputArgs)
		end
	end,
	
	build = build, -- for other modules. e.g: return require('module:navbox').build(args)
}

-- version: r59 2024.10.28 --