Please note that some information on this wiki is still based on the demo version.
If you notice any errors, feel free to correct them.

Module:Navbox: Difference between revisions

From Super Fantasy Kingdom Official Wiki
wgg>Cadaei
from https://ark.wiki.gg/wiki/Module:Navbox
 
m 7 revisions imported
 
(6 intermediate revisions by 3 users not shown)
Line 1: Line 1:
local p = {}
-- version 1.2.5.1
local navbar = require('Module:Navbar')._navbar
 
local cfg = mw.loadData('Module:Navbox/configuration')
-- config table for RANGER.
local getArgs -- lazily initialized
-- If you want to change the default config, DO NOT change it here,
local args
-- please do it via the `onLoadConfig` hook in [[Module:Navbox/Hooks]].
local format = string.format
local config = {
default_navbox_class = "navigation-not-searchable noexcerpt",  -- 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
-- ...
--  |header1 = Items
--  | group1.1 = Weapons
--  |  list1.1 = Swords · Guns · Wands
--  | group1.2 = Armors
--  |  list1.2 = Head pieces · Capes
--  |header2 = NPCs
--  | group2.1 = Town NPCs
--  |  list2.1 = Guide · Witch
-- ...
-- }}
-- will be equal to:
-- {{navbox
-- ...
--  |header1 = Items
--  | group2 = Weapons
--  |  list2 = Swords · Guns · Wands
--  | group3 = Armors
--  |  list3 = Head pieces · Capes
--  |header5 = NPCs
--  | group6 = Town NPCs
--  |  list6 = 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.
}


local function striped(wikitext, border)
---------------------------------------------------------------------
-- 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.
if border == cfg.keyword.border_subgroup and args[cfg.arg.orphan] ~= cfg.keyword.orphan_yes then
-- No change; striping occurs in outermost navbox.
return wikitext
end
local first, second = cfg.class.navbox_odd_part, cfg.class.navbox_even_part
if args[cfg.arg.evenodd] then
if args[cfg.arg.evenodd] == cfg.keyword.evenodd_swap then
first, second = second, first
else
first = args[cfg.arg.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
return (wikitext:gsub(cfg.marker.regex, changer)) -- () omits gsub count
end


local function processItem(item, nowrapitems)
-- Argument alias.
if item:sub(1, 2) == '{|' then
local CANONICAL_NAMES = {
-- Applying nowrap to lines in a table does not make sense.
['titlestyle'] = 'title_style',
-- Add newlines to compensate for trim of x in |parm=x in a template.
['listclass'] = 'list_class',
return '\n' .. item ..'\n'
['groupstyle'] = 'group_style',
end
['collapsible'] = 'state',
if nowrapitems == cfg.keyword.nowrapitems_yes then
['editlink'] = 'meta',
local lines = {}
['editlinks'] = 'meta',
for line in (item .. '\n'):gmatch('([^\n]*)\n') do
['editicon'] = 'meta',
local prefix, content = line:match('^([*:;#]+)%s*(.*)')
['edit_link'] = 'meta',
if prefix and not content:match(cfg.pattern.nowrap) then
['edit_links'] = 'meta',
line = format(cfg.nowrap_item, prefix, content)
['edit_icon'] = 'meta',
end
['navbar'] = 'meta',
table.insert(lines, line)
['name'] = 'template',
end
['evenodd'] = 'striped',
item = table.concat(lines, '\n')
['class'] = 'navbox_class',
end
['css'] = 'navbox_style',
if item:match('^[*:;#]') then
['style'] = 'navbox_style',
return '\n' .. item ..'\n'
['group'] = '1:group',
end
['list'] = '1:list',
return item
}
end


-- we will want this later when we want to add tstyles for hlist/plainlist
local DEFAULT_ARGS = {
local function has_navbar()
['meta'] = true,
return args[cfg.arg.navbar] ~= cfg.keyword.navbar_off
}
and args[cfg.arg.navbar] ~= cfg.keyword.navbar_plain
and (
args[cfg.arg.name]
or mw.getCurrentFrame():getParent():getTitle():gsub(cfg.pattern.sandbox, '')
~= cfg.pattern.navbox
)
end


local function renderNavBar(titleCell)
local STATES = {
if has_navbar() then
['no'] = '',
titleCell:wikitext(navbar{
['off'] = '',
[cfg.navbar.name] = args[cfg.arg.name],
['plain'] = '',
[cfg.navbar.mini] = 1,
['collapsed'] = 'mw-collapsible mw-collapsed',
[cfg.navbar.fontstyle] = (args[cfg.arg.basestyle] or '') .. ';' ..
['expanded'] = 'mw-collapsible',
(args[cfg.arg.titlestyle] or '') ..
}
';background:none transparent;border:none;box-shadow:none;padding:0;'
})
end


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


local function renderTitleRow(tbl)
local STRIPED = {
if not args[cfg.arg.title] then return end
['odd'] = 'striped-odd',
['swap'] = 'striped-odd',
['y'] = 'striped-even',
['yes'] = 'striped-even',
['on'] = 'striped-even',
['even'] = 'striped-even',
['striped'] = 'striped-even',
}


local titleRow = tbl:tag('tr')
local NAVBOX_CHILD_INDICATOR = '!!C$H$I$L$D!!'
local NAVBOX_CHILD_INDICATOR_LENGTH = string.len( NAVBOX_CHILD_INDICATOR )


local titleCell = titleRow:tag('th'):attr('scope', 'col')
local CLASS_PREFIX = 'ranger-'


local titleColspan = 2
---------------------------------------------------------------------
if args[cfg.arg.imageleft] then titleColspan = titleColspan + 1 end
if args[cfg.arg.image] then titleColspan = titleColspan + 1 end


titleCell
local p = {}
:cssText(args[cfg.arg.basestyle])
local h = {} -- non-public
:cssText(args[cfg.arg.titlestyle])
local hooks = mw.title.new('Module:Navbox/Hooks').exists and require('Module:Navbox/Hooks') or {}
:addClass(cfg.class.navbox_title)
:attr('colspan', titleColspan)


renderNavBar(titleCell)
---------------------------------------------------------------------


titleCell
-- For templates: {{#invoke:navbox|main|...}}
:tag('div')
function p.main(frame)
-- id for aria-labelledby attribute
local args = p.mergeArgs(frame)
:attr('id', mw.uri.anchorEncode(args[cfg.arg.title]))
return p.build(args, true)
:addClass(cfg.class.navbox_title_text)
:addClass(args[cfg.arg.titleclass])
:wikitext(processItem(args[cfg.arg.title]))
-- ARK: spacer
tbl:tag('tr')
:addClass('navbox-spacer')
end
end


local function getAboveBelowColspan()
-- For modules: return require('module:navbox').build(args)
local ret = 2
-- By default this method will skip the arguments sanitizing phase
if args[cfg.arg.imageleft] then ret = ret + 1 end
-- (and onSanitizeArgsStart/onSanitizeArgsEnd hooks).
if args[cfg.arg.image] then ret = ret + 1 end
-- Set `doParseArgs` to true to do arguments sanitizing.
return ret
-- If `customConfig` table is provided, it will be merged into default config table (after onLoadConfig()).
end
-- 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
if doParseArgs then
args = h.parseArgs(args)
end


local function renderAboveRow(tbl)
h.runHook('onLoadConfig', config, args)
if not args[cfg.arg.above] then return end


tbl:tag('tr')
if customConfig then
:tag('td')
for k,v in pairs(customConfig) do
:addClass(cfg.class.navbox_abovebelow)
config[k] = v
:addClass(args[cfg.arg.aboveclass])
end
:cssText(args[cfg.arg.basestyle])
end
:cssText(args[cfg.arg.abovestyle])
:attr('colspan', getAboveBelowColspan())
:tag('div')
-- id for aria-labelledby attribute, if no title
:attr('id', args[cfg.arg.title] and nil or mw.uri.anchorEncode(args[cfg.arg.above]))
:wikitext(processItem(args[cfg.arg.above], args[cfg.arg.nowrapitems]))
-- ARK: spacer
--merge default args
tbl:tag('tr')
for k,v in pairs(DEFAULT_ARGS) do
:addClass('navbox-spacer')
if args[k] == nil then
end
args[k] = DEFAULT_ARGS[k]
end
end


local function renderBelowRow(tbl)
h.runHook('onBuildTreeStart', args)
if not args[cfg.arg.below] then return end
local dataTree = h.buildDataTree(args)
h.runHook('onBuildTreeEnd', dataTree, args)
-- ARK: spacer
if type(config.custom_render_handle) == 'function' then
tbl:tag('tr')
return config.custom_render_handle(dataTree, args)
:addClass('navbox-spacer')
else
return h.render(dataTree)
tbl:tag('tr')
end
:tag('td')
:addClass(cfg.class.navbox_abovebelow)
:addClass(args[cfg.arg.belowclass])
:cssText(args[cfg.arg.basestyle])
:cssText(args[cfg.arg.belowstyle])
:attr('colspan', getAboveBelowColspan())
:tag('div')
:wikitext(processItem(args[cfg.arg.below], args[cfg.arg.nowrapitems]))
end
end


local function renderListRow(tbl, index, listnum, listnums_size)
-- merge args from frame and frame:getParent()
if index > 1 then
-- It may be used when creating custom wrapping navbox module.
-- ARK: spacer
--
tbl:tag('tr')
-- For example, Module:PillNavbox
:addClass('navbox-spacer')
--
-- local RANGER = require('Module:Navbox')
-- local p = {}
-- function p.main(frame)
--    return RANGER.build(RANGER.mergeArgs(frame), true, {
--        default_navbox_class = 'pill', -- use "pill" style by default.
--    })
-- end
-- return p
--
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
for k, v in pairs(frame:getParent().args) do
v = mw.text.trim(v)
if v ~= '' then
inputArgs[k] = v
end
end
end
local row = tbl:tag('tr')
return inputArgs
end


if index == 1 and args[cfg.arg.imageleft] then
------------------------------------------------------------------------
row
 
:tag('td')
-- equivalent to mw.text.split(str, div, true) for non-empty separator, but can be over 60x faster, since the latter is Unicode-aware.
:addClass(cfg.class.noviewer)
-- Original version credit: http://richard.warburton.it.
:addClass(cfg.class.navbox_image)
function h.explode(div, str)
:addClass(args[cfg.arg.imageclass])
if (div=='') then return nil end
:css('width', '1px')              -- Minimize width
local pos,arr = 0,{}
:css('padding', '0 2px 0 0')
-- for each divider found
:cssText(args[cfg.arg.imageleftstyle])
for st,sp in function() return string.find(str,div,pos,true) end do
:attr('rowspan', listnums_size)
arr[#arr+1] = string.sub(str,pos,st-1) -- Attach chars left of current divider
:tag('div')
pos = sp+1 -- Jump past current divider
:wikitext(processItem(args[cfg.arg.imageleft]))
end
end
arr[#arr+1] = string.sub(str,pos) -- Attach chars right of last divider
return arr
end


local group_and_num = format(cfg.arg.group_and_num, listnum)
function h.parseArgs(inputArgs)
local groupstyle_and_num = format(cfg.arg.groupstyle_and_num, listnum)
h.runHook('onSanitizeArgsStart', inputArgs)
if args[group_and_num] then
local groupCell = row:tag('th')
local args = {}
 
-- id for aria-labelledby attribute, if lone group with no title or above
for k, v in pairs(inputArgs) do
if listnum == 1 and not (args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group2]) then
if type(k) == 'string' then
groupCell
-- all named args have already been trimmed
:attr('id', mw.uri.anchorEncode(args[cfg.arg.group1]))
local key = h.normalizeKey(k)
args[key] = h.normalizeValue(key, v)
else
args[k] = mw.text.trim(v) -- keep number-index arguments (for {{navbox|child|...}})
end
end
end
h.runHook('onSanitizeArgsEnd', args, inputArgs)
return args
end


groupCell
-- Normalize the name string of arguments.
:attr('scope', 'row')
-- the normalized form is (index:)?name, in which:
:addClass(cfg.class.navbox_group)
-- index is number index such as 1, 1.3, 1.2.45,
:addClass(args[cfg.arg.groupclass])
-- name is in lowercase underscore-case, such as group, group_style
:cssText(args[cfg.arg.basestyle])
-- e.g: header_state, 1.3:list_style
-- the input argument name can be:
-- * camel-case: listStyle, ListStyle
-- * space separated: list style
-- * prefix+index+postfix?, and can be in camel-case or space/hyphen separated or mixed: list 1 style, list1, list1Style, list1_style
-- * index.name: 1.3.list
-- * 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


groupCell
-- standardize *_css to *_style
:cssText(args[cfg.arg.groupstyle])
s = s:gsub('_css$', '_style')
:cssText(args[groupstyle_and_num])
-- standardize *collapsible to *state
:wikitext(args[group_and_num])
s = s:gsub('collapsible$', 'state')
end


local listCell = row:tag('td')
-- standardize all aliases to the canonical name
return CANONICAL_NAMES[s] or s
end


if args[group_and_num] then
function h.normalizeValue(k, v)
listCell
k = tostring(k)
:addClass(cfg.class.navbox_list_with_group)
if k:find('_style$') then
else
v = (v .. ';'):gsub(';;', ';')
listCell:attr('colspan', 2)
return v
elseif k:find('state$') then
return STATES[v]
elseif k == 'striped' then
return STRIPED[v]
elseif k == 'meta' then
return not BOOL_FALSE[v]
elseif v:sub(1, 2) == '{|' or v:match('^[*:;#]') 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' .. v ..'\n'
end
end
return v
end
-- parse arguments, convert them to structured data tree
function h.buildDataTree(args)
-- parse args to a tree
local tree = h.buildTree(args)


local rowstyle  -- usually nil so cssText(rowstyle) usually adds nothing
-- build root navbox data
if index % 2 == 1 then
local data = h.buildNavboxData(tree.info)
rowstyle = args[cfg.arg.oddstyle]
else
-- Recursively build section tree
rowstyle = args[cfg.arg.evenstyle]
if tree.children then
data.sections = h.buildSections(tree.children, {
listClass = h.mergeAttrs(args.list_class, config.default_list_class),
listStyle = args.list_style,
groupClass = h.mergeAttrs(args.group_class, config.default_group_class),
groupStyle = args.group_style,
sectionClass = h.mergeAttrs(args.section_class, config.default_section_class),
sectionStyle = args.section_style,
headerClass = h.mergeAttrs(args.header_class, config.default_header_class),
headerStyle = args.header_style,
headerState = args.header_state or config.default_header_state,
})
end
end
 
if args[1] == 'child' then
local list_and_num = format(cfg.arg.list_and_num, listnum)
data.CHILD_MODE = true
local listText = args[list_and_num]
local oddEven = cfg.marker.oddeven
if listText:sub(1, 12) == '</div><table' then
-- Assume list text is for a subgroup navbox so no automatic striping for this row.
oddEven = listText:find(cfg.pattern.navbox_title) and cfg.marker.restart or cfg.class.navbox_odd_part
end
end


local liststyle_and_num = format(cfg.arg.liststyle_and_num, listnum)
return data
local listclass_and_num = format(cfg.arg.listclass_and_num, listnum)
end
listCell
:css('padding', '0')
:cssText(args[cfg.arg.liststyle])
:cssText(rowstyle)
:cssText(args[liststyle_and_num])
:addClass(cfg.class.navbox_list)
:addClass(cfg.class.navbox_part .. oddEven)
:addClass(args[cfg.arg.listclass])
:addClass(args[listclass_and_num])
:tag('div')
:css('padding',
(index == 1 and args[cfg.arg.list1padding]) or args[cfg.arg.listpadding] or '0 0.25em'
)
:wikitext(processItem(listText, args[cfg.arg.nowrapitems]))


if index == 1 and args[cfg.arg.image] then
function h.buildSections(list, defaults)
row
local sections = {}
:tag('td')
local section = nil
:addClass(cfg.class.noviewer)
for k, node in h.orderedPairs(list) do
:addClass(cfg.class.navbox_image)
local info = node.info or {}
:addClass(args[cfg.arg.imageclass])
--start a new section if needed
:css('width', '1px')              -- Minimize width
if info.header or not section then
:css('padding', '0 0 0 2px')
section = {
:cssText(args[cfg.arg.imagestyle])
class = h.mergeAttrs(info.section_class, defaults.sectionClass),
:attr('rowspan', listnums_size)
style = h.mergeAttrs(info.section_style, defaults.sectionStyle),
:tag('div')
body = {}
:wikitext(processItem(args[cfg.arg.image]))
}
-- Section header if needed.
-- If the value of a `|header_n=` is two or more consecutive "-" characters (e.g. --, -----),
-- it means start a new section without header, and the new section will be not collapsable.
if info.header and not string.match(info.header, '^%-%-+$') then
section.header = {
content = info.header,
class = h.mergeAttrs(info.header_class, defaults.headerClass),
style = h.mergeAttrs(info.header_style, defaults.headerStyle),
}
section.state = info.state or defaults.headerState or 'mw-collapsible'
end
sections[#sections+1] = section
end
-- above/below for this section
if info.above then
section.above = {
content = info.above,
class= h.mergeAttrs(info.above_class, config.default_above_class),
style = info.above_style,
}
end
if info.below then
section.below = {
content = info.below,
class= h.mergeAttrs(info.below_class, config.default_below_class),
style = info.below_style,
}
end
-- this group+list row
if info.group or info.list or node.children then
local row = {}
section.body[#section.body+1] = row
if info.group then
row.group = {
content = info.group,
class = h.mergeAttrs(info.group_class, defaults.groupClass),
style = h.mergeAttrs(info.group_style, defaults.groupStyle),
}
end
if info.list then
if string.sub(info.list, 1, NAVBOX_CHILD_INDICATOR_LENGTH) == NAVBOX_CHILD_INDICATOR then
-- it is from {{navbox|child| ... }}
row.sections = mw.text.jsonDecode(string.sub(info.list, NAVBOX_CHILD_INDICATOR_LENGTH+1))
else
row.list = {
content = info.list,
class = h.mergeAttrs(info.list_class, defaults.listClass),
style = h.mergeAttrs(info.list_style, defaults.listStyle),
}
end
end
-- sub-nodes, will override {{navbox|child| ... }}
if node.children then
row.sections = h.buildSections(node.children, defaults)
end
end
end
end
-- flatten if needed:
-- If a section has only one list with no content and no corresponding group but has sublists, these sublists will be moved to upper level.
if config.auto_flatten_top_level then
for _, sect in ipairs(sections) do
if #sect.body == 1 then
local node = sect.body[1]
if not node.group and not node.list and node.sections and #node.sections == 1 and not node.sections[1].header then
sect.body = node.sections[1].body
end
end
end
end
return sections
end
end
 
-- uses this now to make the needHlistCategory correct
function h.buildNavboxData(info)
-- to use later for when we add list styles via navbox
local data = {
local function has_list_class(htmlclass)
state = info.state or 'mw-collapsible', -- here we need a default value for empty input
local class_args = { -- rough order of probability of use
striped = info.striped,
cfg.arg.bodyclass, cfg.arg.listclass, cfg.arg.aboveclass,
class = h.mergeAttrs(info.navbox_class, config.default_navbox_class),
cfg.arg.belowclass, cfg.arg.titleclass, cfg.arg.navboxclass,
style = info.navbox_style,
cfg.arg.groupclass, cfg.arg.imageclass
}
local patterns = {
'^' .. htmlclass .. '$',
'%s' .. htmlclass .. '$',
'^' .. htmlclass .. '%s',
'%s' .. htmlclass .. '%s'
}
}
for _, arg in ipairs(class_args) do
-- data for titlebar
for _, pattern in ipairs(patterns) do
if info.title or info.meta or data.state ~= '' then
if mw.ustring.find(args[arg] or '', pattern) then
data.title = {
return true
content = info.title,
class = h.mergeAttrs(info.title_class, config.default_title_class),
style = info.title_style,
}
if info.meta then
data.metaLinks = {
link = info.meta_link, -- will be used as [[$link|$text]]
url = info.meta_url,  -- will be used as [$url $text], only if there is no data.metaLinks.link
text = info.meta_text, --hovertext
}
if not info.meta_link and not info.meta_url then
-- default link target
local title = mw.title.new(info.template or mw.getCurrentFrame():getParent():getTitle(), 'Template')
if not title then
error('Invalid title ' .. info.template)
end
data.metaLinks.link = title.fullText
end
if not info.meta_text then
local msg = mw.message.new(config.editlink_hover_message_key)
data.metaLinks.text = msg:exists() and msg:plain() or 'View or edit this template'
end
end
end
end
end
end
return false
end
-- above/below
if info.above then
data.above = {
content = info.above,
class= h.mergeAttrs(info.above_class, config.default_above_class),
style = info.above_style,
}
end


local function needsHorizontalLists(border)
if info.below then
if border == cfg.keyword.border_subgroup then
data.below = {
return false
content = info.below,
class= h.mergeAttrs(info.below_class, config.default_below_class),
style = info.below_style,
}
end
end
return not has_list_class(cfg.pattern.hlist) and not has_list_class(cfg.pattern.plainlist)
return data
end
end


local function hasBackgroundColors()
-- parse arguments, convert them into a tree based on their index
for _, key in ipairs({cfg.arg.titlestyle, cfg.arg.groupstyle,
-- each node on tree is { info = { #data for this node# }, children = {#children nodes#}  }
cfg.arg.basestyle, cfg.arg.abovestyle, cfg.arg.belowstyle}) do
function h.buildTree(args, defaults)
if tostring(args[key]):find('background', 1, true) then
local tree = { info = {} }
return true
local check = function(key, value)
local index, name = string.match(key, '^([%d%.]+):(.+)$')
 
-- no number index found, for root node
if not index then
tree.info[key] = value
return
end
end
end
return false
end


local function hasBorders()
-- filter invalid number index
for _, key in ipairs({cfg.arg.groupstyle, cfg.arg.basestyle,
if string.match(index, '^%.') or string.match(index, '%.$') or string.match(index, '%.%.') then
cfg.arg.abovestyle, cfg.arg.belowstyle}) do
return
if tostring(args[key]):find('border', 1, true) then
return true
end
end
-- find the node that matches the index in the tree
local arr = h.explode('.', index)
local node = tree
for _, v in ipairs(arr) do
v = tonumber(v)
if not node.children then
node.children = {}
end
if not node.children[v] then
node.children[v] = { info = {} }
end
node = node.children[v]
end
node.info[name] = value
end
for k,v in pairs(args) do
check(k, v)
end
end
return false
return tree
end
end


local function renderMainTable(border, listnums)
function h.render(data)
local tbl = mw.html.create('table')
-- handle {{navbox|child|...}} syntax
:addClass(cfg.class.nowraplinks)
if data.CHILD_MODE then
:addClass(args[cfg.arg.bodyclass])
return NAVBOX_CHILD_INDICATOR..mw.text.jsonEncode(data.sections)
end


local state = args[cfg.arg.state]
-----  normal case -----
if args[cfg.arg.title] and state ~= cfg.keyword.state_plain and state ~= cfg.keyword.state_off then
if state == cfg.keyword.state_collapsed then
local out = mw.html.create()
state = cfg.class.collapsed
-- build navbox container
local navbox = out:tag('div')
:attr('role', 'navigation'):attr('aria-label', 'Navbox')
:addClass(CLASS_PREFIX..'navbox')
:addClass(data.class)
:addClass(data.striped)
:addClass(data.state)
:cssText(data.style)
 
--title bar
if data.title then
local titlebar = navbox:tag('div'):addClass(CLASS_PREFIX..'title')
titlebar:tag('div'):addClass('mw-collapsible-toggle-placeholder')
if data.metaLinks then
titlebar:node(h.renderMetaLinks(data.metaLinks))
end
if data.title then
titlebar:addClass(data.title.class):tag('div')
:addClass(CLASS_PREFIX..'title-text')
:addClass(data.title.class)
:cssText(data.title.style)
:wikitext(data.title.content)
end
end
tbl
:addClass(cfg.class.collapsible)
:addClass(state or cfg.class.autocollapse)
end
end


if border == cfg.keyword.border_subgroup or border == cfg.keyword.border_none then
--above
tbl
if data.above then
:addClass(cfg.class.navbox_subgroup)
navbox:tag('div')
:cssText(args[cfg.arg.bodystyle])
:addClass(CLASS_PREFIX..'above mw-collapsible-content')
:cssText(args[cfg.arg.style])
:addClass(data.above.class)
else -- regular navbox - bodystyle and style will be applied to the wrapper table
:cssText(data.above.style)
tbl
:wikitext(data.above.content)
:addClass(cfg.class.navbox_inner)
:attr('id', (not data.title) and mw.uri.anchorEncode(data.above.content) or nil) -- id for aria-labelledby attribute, if no title
end
-- sections
if data.sections then
h.renderSections(data.sections, navbox, 0, true)
else
-- Insert a blank section for completely empty navbox to ensure it behaves correctly when collapsed.
if not data.above and not data.below then
navbox:tag('div'):addClass(CLASS_PREFIX..'section mw-collapsible-content')
end
end
end
tbl:cssText(args[cfg.arg.innerstyle])


renderTitleRow(tbl)
--below
renderAboveRow(tbl)
if data.below then
local listnums_size = #listnums
navbox:tag('div')
for i, listnum in ipairs(listnums) do
:addClass(CLASS_PREFIX..'below mw-collapsible-content')
renderListRow(tbl, i, listnum, listnums_size)
:addClass(data.below.class)
:cssText(data.below.style)
:wikitext(data.below.content)
end
end
renderBelowRow(tbl)
return tostring(out)..'[[Category:Pages with navboxes]]' -- suggest to use HIDDENCAT here; will be used for maintenance & gadget imports
end


return tbl
function h.renderSections(data, container, level, even)
for i,sect in ipairs(data) do
--section box
local section = container:tag('div')
:addClass(CLASS_PREFIX..'section mw-collapsible-content')
:addClass(sect.class)
:addClass(sect.state)
:cssText(sect.style)
-- section header
if sect.header then
section:tag('div')
:addClass(CLASS_PREFIX..'header')
:addClass(sect.header.class)
:cssText(sect.header.style)
:tag('div'):addClass('mw-collapsible-toggle-placeholder'):done()
:tag('div'):addClass(CLASS_PREFIX..'header-text'):wikitext(sect.header.content)
end
-- above:
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')
even = h.renderBody(sect.body, box, level, (level==0) and true or even) -- 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
return even
end
end


function p._navbox(navboxArgs)
args = navboxArgs
local listnums = {}


for k, _ in pairs(args) do
function h.renderMetaLinks(info)
if type(k) == 'string' then
local box = mw.html.create('span'):addClass(CLASS_PREFIX..'meta')
local listnum = k:match(cfg.pattern.listnum)
local meta = box:tag('span'):addClass('nv nv-view')
if listnum then table.insert(listnums, tonumber(listnum)) end
if info.link then
meta:wikitext('[['..info.link..'|')
:tag('span'):wikitext(info.text):attr('title', info.text):done()
:wikitext(']]')
elseif info.url then
meta:wikitext('['..info.url..' ')
:tag('span'):wikitext(info.text):attr('title', info.text):done()
:wikitext(']')
end
return box
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.sections then
count = count + 1
-- row container
local row = box:tag('div'):addClass(CLASS_PREFIX..'row')
-- group cell
if v.group or (v.sections 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
else
row:addClass('empty-group')
end
-- list cell
local listCell = row:tag('div'):addClass(CLASS_PREFIX..'listbox')
if not v.list and not v.sections then
listCell:addClass('empty')
row:addClass('empty-list')
end
if v.list or (v.group and not v.sections) 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
if v.sections then
local sublistBox = listCell:tag('div'):addClass(CLASS_PREFIX..'sublist level-'..level)
even = h.renderSections(v.sections, sublistBox, level+1, even)
end
end
end
end
end
table.sort(listnums)
if count > 0 then
box:css('--count', count) -- for flex-grow
end
return even
end


local border = mw.text.trim(args[cfg.arg.border] or args[1] or '')
-- pairs, but sort the keys alphabetically
if border == cfg.keyword.border_child then
function h.orderedPairs(t, f)
border = cfg.keyword.border_subgroup
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
return iter
end


-- render the main body of the navbox
-- For cascading parameters, such as style or class, they are merged in exact order (from general to specific).  
local tbl = renderMainTable(border, listnums)
-- Any parameter starting with multiple hyphens(minus signs) will terminate the cascade.
 
-- An example:
local res = mw.html.create()
-- For group_1.1, its style is affected by parameters |group_1.1_style=... , |subgroup_level_1_style=... , and |subgroup_style=... .
-- render the appropriate wrapper for the navbox, based on the border param
-- 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 border == cfg.keyword.border_none then
-- if we have |group_1.1_style= -- color:red; |subgroup_level_1_style= font-weight: bold; and |subgroup_style= color: green; ,
local nav = res:tag('div')
-- 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.
:attr('role', 'navigation')
function h.mergeAttrs(...)
:node(tbl)
local trim = mw.text.trim
-- aria-labelledby title, otherwise above, otherwise lone group
local s = ''
if args[cfg.arg.title] or args[cfg.arg.above] or (args[cfg.arg.group1]
for i=1, select('#', ...) do
and not args[cfg.arg.group2]) then
local v = trim(select(i, ...) or '')
nav:attr(
local str = string.match(v, '^%-%-+(.*)$')
'aria-labelledby',
if str then
mw.uri.anchorEncode(
s = trim(str..' '..s)
args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1]
break
)
)
else
nav:attr('aria-label', cfg.aria_label)
end
elseif border == cfg.keyword.border_subgroup then
-- We assume that this navbox is being rendered in a list cell of a
-- parent navbox, and is therefore inside a div with padding:0em 0.25em.
-- We start with a </div> to avoid the padding being applied, and at the
-- end add a <div> to balance out the parent's </div>
res
:wikitext('</div>')
:node(tbl)
:wikitext('<div>')
else
local nav = res:tag('div')
:attr('role', 'navigation')
:addClass(cfg.class.navbox)
:addClass(args[cfg.arg.navboxclass])
:cssText(args[cfg.arg.bodystyle])
:cssText(args[cfg.arg.style])
:node(tbl)
-- aria-labelledby title, otherwise above, otherwise lone group
if args[cfg.arg.title] or args[cfg.arg.above]
or (args[cfg.arg.group1] and not args[cfg.arg.group2]) then
nav:attr(
'aria-labelledby',
mw.uri.anchorEncode(args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1])
)
else
else
nav:attr('aria-label', cfg.aria_label)
s = trim(v..' '..s)
end
end
end
end
 
if s == '' then s = nil end
return striped(tostring(res), border)
return s
end
end


function p.navbox(frame)
function h.runHook(key, ...)
if not getArgs then
if hooks[key] then
getArgs = require('dev:Arguments').getArgs
hooks[key](...)
end
args = getArgs(frame, {wrappers = {cfg.pattern.navbox}})
 
-- Read the arguments in the order they'll be output in, to make references
-- number in the right order.
local _
_ = args[cfg.arg.title]
_ = args[cfg.arg.above]
-- Limit this to 20 as covering 'most' cases (that's a SWAG) and because
-- iterator approach won't work here
for i = 1, 20 do
_ = args[format(cfg.arg.group_and_num, i)]
_ = args[format(cfg.arg.list_and_num, i)]
end
end
_ = args[cfg.arg.below]
return p._navbox(args)
end
end


-----------------------------------------------
return p
return p

Latest revision as of 23:46, 21 October 2025

This module is used by Template:Navbox.


-- version 1.2.5.1

-- config table for RANGER.
-- 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 noexcerpt",   -- 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
	-- ...
	--   |header1 = Items
	--   | group1.1 = Weapons
	--   |  list1.1 = Swords · Guns · Wands
	--   | group1.2 = Armors
	--   |  list1.2 = Head pieces · Capes
	--   |header2 = NPCs
	--   | group2.1 = Town NPCs
	--   |  list2.1 = Guide · Witch
	-- ...
	-- }}
	-- will be equal to:
	-- {{navbox
	-- ...
	--   |header1 = Items
	--   | group2 = Weapons
	--   |  list2 = Swords · Guns · Wands
	--   | group3 = Armors
	--   |  list3 = Head pieces · Capes
	--   |header5 = NPCs
	--   | group6 = Town NPCs
	--   |  list6 = 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',
	['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',
	['name'] = 'template',
	['evenodd'] = 'striped',
	['class'] = 'navbox_class',
	['css'] = 'navbox_style',
	['style'] = 'navbox_style',
	['group'] = '1:group',
	['list'] = '1:list',
}

local DEFAULT_ARGS = {
	['meta'] = true,
}

local STATES = {
	['no'] = '',
	['off'] = '',
	['plain'] = '',
	['collapsed'] = 'mw-collapsible mw-collapsed',
	['expanded'] = 'mw-collapsible',
}

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

local STRIPED = {
	['odd'] = 'striped-odd',
	['swap'] = 'striped-odd',
	['y'] = 'striped-even',
	['yes'] = 'striped-even',
	['on'] = 'striped-even',
	['even'] = 'striped-even',
	['striped'] = 'striped-even',
}

local NAVBOX_CHILD_INDICATOR = '!!C$H$I$L$D!!'
local NAVBOX_CHILD_INDICATOR_LENGTH = string.len( NAVBOX_CHILD_INDICATOR )

local CLASS_PREFIX = 'ranger-'

---------------------------------------------------------------------

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

---------------------------------------------------------------------

-- For templates: {{#invoke:navbox|main|...}}
function p.main(frame)
	local args = p.mergeArgs(frame)
	return p.build(args, true)
end

-- For modules: return require('module:navbox').build(args)
-- By default this method will skip the arguments sanitizing phase 
-- (and onSanitizeArgsStart/onSanitizeArgsEnd hooks).
-- Set `doParseArgs` to true to do arguments sanitizing.
-- If `customConfig` table is provided, it will be merged into default config table (after onLoadConfig()).
-- 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
	
	if doParseArgs then 
		args = h.parseArgs(args)
	end

	h.runHook('onLoadConfig', config, args)

	if customConfig then
		for k,v in pairs(customConfig) do
			config[k] = v
		end
	end
	
	--merge default args
	for k,v in pairs(DEFAULT_ARGS) do
		if args[k] == nil then
			args[k] = DEFAULT_ARGS[k]
		end
	end

	h.runHook('onBuildTreeStart', args)
	local dataTree = h.buildDataTree(args)
	h.runHook('onBuildTreeEnd', dataTree, args)
	
	if type(config.custom_render_handle) == 'function' then
		return config.custom_render_handle(dataTree, args)
	else
		return h.render(dataTree)
	end
end

-- merge args from frame and frame:getParent()
-- It may be used when creating custom wrapping navbox module.
--
-- For example, Module:PillNavbox
--
-- local RANGER = require('Module:Navbox')
-- local p = {}
-- function p.main(frame)
--     return RANGER.build(RANGER.mergeArgs(frame), true, {
--         default_navbox_class = 'pill', -- use "pill" style by default.
--     })
-- end
-- return p
--
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
	
	for k, v in pairs(frame:getParent().args) do
		v = mw.text.trim(v)
		if v ~= '' then
			inputArgs[k] = v
		end
	end
	
	return inputArgs
end

------------------------------------------------------------------------

-- equivalent to mw.text.split(str, div, true) for non-empty separator, but can be over 60x faster, since the latter is Unicode-aware.
-- Original version credit: http://richard.warburton.it.
function h.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

function h.parseArgs(inputArgs)
	h.runHook('onSanitizeArgsStart', inputArgs)
	
	local args = {}
	
	for k, v in pairs(inputArgs) do
		if type(k) == 'string' then
			-- all named args have already been trimmed
			local key = h.normalizeKey(k)
			args[key] = h.normalizeValue(key, v)
		else
			args[k] = mw.text.trim(v) -- keep number-index arguments (for {{navbox|child|...}})
		end
	end
	
	h.runHook('onSanitizeArgsEnd', args, inputArgs)
	
	return args
end

-- Normalize the name string of arguments.
-- the normalized form is (index:)?name, in which:
-- index is number index such as 1, 1.3, 1.2.45,
-- name is in lowercase underscore-case, such as group, group_style
-- e.g: header_state, 1.3:list_style
-- the input argument name can be:
-- * camel-case: listStyle, ListStyle
-- * space separated: list style
-- * prefix+index+postfix?, and can be in camel-case or space/hyphen separated or mixed: list 1 style, list1, list1Style, list1_style
-- * index.name: 1.3.list
-- * 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 *collapsible to *state
	s = s:gsub('collapsible$', 'state')

	-- standardize all aliases to the canonical name
	return CANONICAL_NAMES[s] or s
end

function h.normalizeValue(k, v)
	k = tostring(k)
	if k:find('_style$') then
		v = (v .. ';'):gsub(';;', ';')
		return v
	elseif k:find('state$') then
		return STATES[v]
	elseif k == 'striped' then
		return STRIPED[v]
	elseif k == 'meta' then
		return not BOOL_FALSE[v]
	elseif v:sub(1, 2) == '{|' or v:match('^[*:;#]') 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' .. v ..'\n'
	end
	return v
end

-- parse arguments, convert them to structured data tree
function h.buildDataTree(args)
	-- parse args to a tree
	local tree = h.buildTree(args)

	-- build root navbox data
	local data = h.buildNavboxData(tree.info)
	
	-- Recursively build section tree
	if tree.children then
		data.sections = h.buildSections(tree.children, {
			listClass = h.mergeAttrs(args.list_class, config.default_list_class),
			listStyle =  args.list_style,
			groupClass = h.mergeAttrs(args.group_class, config.default_group_class),
			groupStyle = args.group_style,
			sectionClass = h.mergeAttrs(args.section_class, config.default_section_class),
			sectionStyle = args.section_style,
			headerClass = h.mergeAttrs(args.header_class, config.default_header_class),
			headerStyle = args.header_style,
			headerState = args.header_state or config.default_header_state,
		})
	end
	if args[1] == 'child' then
		data.CHILD_MODE = true
	end

	return data
end

function h.buildSections(list, defaults)
	local sections = {}
	local section = nil
	for k, node in h.orderedPairs(list) do
		local info = node.info or {}
		--start a new section if needed
		if info.header or not section then
			section = { 
				class = h.mergeAttrs(info.section_class, defaults.sectionClass),
				style = h.mergeAttrs(info.section_style, defaults.sectionStyle),
				body = {}
			}
			-- Section header if needed.
			-- If the value of a `|header_n=` is two or more consecutive "-" characters (e.g. --, -----), 
			-- it means start a new section without header, and the new section will be not collapsable.
			if info.header and not string.match(info.header, '^%-%-+$') then
				section.header = {
					content = info.header,
					class = h.mergeAttrs(info.header_class, defaults.headerClass),
					style = h.mergeAttrs(info.header_style, defaults.headerStyle),
				}
				section.state = info.state or defaults.headerState or 'mw-collapsible'
			end
			sections[#sections+1] = section
		end
		-- above/below for this section
		if info.above then
			section.above = {
				content = info.above,
				class= h.mergeAttrs(info.above_class, config.default_above_class),
				style = info.above_style,
			}
		end
		if info.below then
			section.below = {
				content = info.below,
				class= h.mergeAttrs(info.below_class, config.default_below_class),
				style = info.below_style,
			}
		end
		-- this group+list row
		if info.group or info.list or node.children then
			local row = {}
			section.body[#section.body+1] = row
			if info.group then
				row.group = {
					content = info.group,
					class = h.mergeAttrs(info.group_class, defaults.groupClass),
					style = h.mergeAttrs(info.group_style, defaults.groupStyle),
				}
			end
			if info.list then
				if string.sub(info.list, 1, NAVBOX_CHILD_INDICATOR_LENGTH) == NAVBOX_CHILD_INDICATOR then
					-- it is from {{navbox|child| ... }}
					row.sections = mw.text.jsonDecode(string.sub(info.list, NAVBOX_CHILD_INDICATOR_LENGTH+1))
				else
					row.list = {
						content = info.list,
						class = h.mergeAttrs(info.list_class, defaults.listClass),
						style = h.mergeAttrs(info.list_style, defaults.listStyle),
					}
				end
			end
			-- sub-nodes, will override {{navbox|child| ... }}
			if node.children then
				row.sections = h.buildSections(node.children, defaults)
			end
		end
	end
	-- flatten if needed:
	-- If a section has only one list with no content and no corresponding group but has sublists, these sublists will be moved to upper level.
	if config.auto_flatten_top_level then
		for _, sect in ipairs(sections) do
			if #sect.body == 1 then
				local node = sect.body[1]
				if not node.group and not node.list and node.sections and #node.sections == 1 and not node.sections[1].header then
					sect.body = node.sections[1].body
				end
			end
		end
	end
	return sections
end
	
function h.buildNavboxData(info)
	local data = {
		state = info.state or 'mw-collapsible', -- here we need a default value for empty input
		striped = info.striped,
		class = h.mergeAttrs(info.navbox_class, config.default_navbox_class),
		style = info.navbox_style,
	}
	
	-- data for titlebar
	if info.title or info.meta or data.state ~= '' then
		data.title = {
			content = info.title,
			class = h.mergeAttrs(info.title_class, config.default_title_class),
			style = info.title_style,
		}
		if info.meta then
			data.metaLinks = {
				link = info.meta_link, -- will be used as [[$link|$text]]
				url = info.meta_url,  -- will be used as [$url $text], only if there is no data.metaLinks.link
				text = info.meta_text, --hovertext
			}
			if not info.meta_link and not info.meta_url then
				-- default link target
				local title = mw.title.new(info.template or mw.getCurrentFrame():getParent():getTitle(), 'Template')
				if not title then
					error('Invalid title ' .. info.template)
				end
				data.metaLinks.link = title.fullText
			end
			if not info.meta_text then
				local msg = mw.message.new(config.editlink_hover_message_key)
				data.metaLinks.text = msg:exists() and msg:plain() or 'View or edit this template'
			end
		end
	end
	
	-- above/below
	if info.above then
		data.above = {
			content = info.above,
			class= h.mergeAttrs(info.above_class, config.default_above_class),
			style = info.above_style,
		}
	end

	if info.below then
		data.below = {
			content = info.below,
			class= h.mergeAttrs(info.below_class, config.default_below_class),
			style = info.below_style,
		}
	end
	
	return data
end

-- parse arguments, convert them into a tree based on their index
-- each node on tree is { info = { #data for this node# }, children = {#children nodes#}  }
function h.buildTree(args, defaults)
	local tree = { info = {} }
	local check = function(key, value)
		local index, name = string.match(key, '^([%d%.]+):(.+)$')

		-- no number index found, for root node
		if not index then 
			tree.info[key] = value
			return
		end

		-- filter invalid number index
		if string.match(index, '^%.') or string.match(index, '%.$') or string.match(index, '%.%.') then
			return
		end
		
		-- find the node that matches the index in the tree
		local arr = h.explode('.', index)
		local node = tree
		for _, v in ipairs(arr) do
			v = tonumber(v)
			if not node.children then
				node.children = {}
			end
			if not node.children[v] then 
				node.children[v] = { info = {} }
			end
			node = node.children[v]
		end
		
		node.info[name] = value
	end
	for k,v in pairs(args) do
		check(k, v)
	end
	return tree
end

function h.render(data)
	-- handle {{navbox|child|...}} syntax
	if data.CHILD_MODE then
		return NAVBOX_CHILD_INDICATOR..mw.text.jsonEncode(data.sections)
	end

	-----  normal case -----
	
	local out = mw.html.create()
	
	-- build navbox container
	local navbox = out:tag('div')
		:attr('role', 'navigation'):attr('aria-label', 'Navbox')
		:addClass(CLASS_PREFIX..'navbox')
		:addClass(data.class)
		:addClass(data.striped)
		:addClass(data.state)
		:cssText(data.style)

	--title bar
	if data.title then
		local titlebar = navbox:tag('div'):addClass(CLASS_PREFIX..'title')
		titlebar:tag('div'):addClass('mw-collapsible-toggle-placeholder')
		if data.metaLinks then
			titlebar:node(h.renderMetaLinks(data.metaLinks))
		end
		if data.title then
			titlebar:addClass(data.title.class):tag('div')
			:addClass(CLASS_PREFIX..'title-text')
			:addClass(data.title.class)
			:cssText(data.title.style)
			:wikitext(data.title.content)
		end
	end

	--above
	if data.above then
		navbox:tag('div')
		: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
	
	-- sections
	if data.sections then
		h.renderSections(data.sections, navbox, 0, true)
	else
		-- Insert a blank section for completely empty navbox to ensure it behaves correctly when collapsed.
		if not data.above and not data.below then 
			navbox:tag('div'):addClass(CLASS_PREFIX..'section mw-collapsible-content')
		end
	end

	--below
	if data.below then
		navbox:tag('div')
		:addClass(CLASS_PREFIX..'below mw-collapsible-content')
		:addClass(data.below.class)
		:cssText(data.below.style)
		:wikitext(data.below.content)
	end
	return tostring(out)..'[[Category:Pages with navboxes]]' -- suggest to use HIDDENCAT here; will be used for maintenance & gadget imports
end

function h.renderSections(data, container, level, even)
	for i,sect in ipairs(data) do
		--section box
		local section = container:tag('div')
			:addClass(CLASS_PREFIX..'section mw-collapsible-content')
			:addClass(sect.class)
			:addClass(sect.state)
			:cssText(sect.style)
		-- section header
		if sect.header then
			section:tag('div')
			:addClass(CLASS_PREFIX..'header')
			:addClass(sect.header.class)
			:cssText(sect.header.style)
			:tag('div'):addClass('mw-collapsible-toggle-placeholder'):done()
			:tag('div'):addClass(CLASS_PREFIX..'header-text'):wikitext(sect.header.content)
		end
		-- above:
		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')
		even = h.renderBody(sect.body, box, level, (level==0) and true or even) -- 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
	return even
end


function h.renderMetaLinks(info)
	local box = mw.html.create('span'):addClass(CLASS_PREFIX..'meta')
	local meta = box:tag('span'):addClass('nv nv-view')
	
	if info.link then 
		meta:wikitext('[['..info.link..'|')
			:tag('span'):wikitext(info.text):attr('title', info.text):done()
			:wikitext(']]')
	elseif info.url then
		meta:wikitext('['..info.url..' ')
			:tag('span'):wikitext(info.text):attr('title', info.text):done()
			:wikitext(']')
	end
	
	return box
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.sections then
			count = count + 1
			-- row container
			local row = box:tag('div'):addClass(CLASS_PREFIX..'row')
			-- group cell
			if v.group or (v.sections 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
			else
				row:addClass('empty-group')
			end
			-- list cell
			local listCell = row:tag('div'):addClass(CLASS_PREFIX..'listbox')
			if not v.list and not v.sections then
				listCell:addClass('empty')
				row:addClass('empty-list')
			end
			if v.list or (v.group and not v.sections) 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
			if v.sections then
				local sublistBox = listCell:tag('div'):addClass(CLASS_PREFIX..'sublist level-'..level)
				even = h.renderSections(v.sections, 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
	return iter
end

-- 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
			s = trim(v..' '..s)
		end
	end
	if s == '' then s = nil end
	return s
end

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

-----------------------------------------------
return p