本模块用于实现模板链接:{{navbox}}系列模板。
实现方式
本模块会先将参数表args转化为data,并根据data进行渲染。可以使用p.new
创建对象,并使用obj.render()
将其转化为可渲染的mw.html对象。
在控制台中调试时,可以使用p._navbox
测试参数并返回导航框,其他模块在使用本模块时也应当使用该方法。p.test
和p.test2
还能用来测试将参数转化为数据的过程,后者更加简便。
local p = {}
local navbar = require "Module:Navbar"._navbar
local trim = mw.text.trim
local tools = require 'Module:TableTools'
local boolean = require 'Module:Yesno'
--[[
用于多次访问表的字段。例如,
index(t, 'a', 'b', 'c')
类似于
t.a.b.c 但如果a或b字段不存在,直接返回nil,而不是抛出错误。
就相当于
((t.a or {}).b).c
]]
local function index(t, ...)
local result = t
for i = 1, select("#", ...) do
local k = select(i, ...)
if type(result) == "table" then
result = result[k]
else
return nil
end
end
return result
end
--[[
为表创建指定位置的字段。例如,
newindex(t, value, 'a', 'b', 'c')
就类似于
t.a.b.c = value
但是当a、b字段不存在时,以空表创建字段,再写入这个空表。大致相当于:
t.a = t.a or {}
t.a.b = t.a.b or {}
t.a.b.c = value
]]
local function newindex(t, value, ...)
local current = t
local k
if type(current) ~= 'table' then
return current
end
for i = 1, select("#", ...) - 1 do
-- 此时的current必然是个表
k = select(i, ...)
if not current[k] then
current[k] = {}
elseif type(current[k]) ~= 'table' then
local warn = '警告:尝试将字段值' .. tostring(value)
.. '写入非空非表的值:' .. tostring(current[k])
mw.addWarning(warn)
mw.log(warn)
mw.log('\tt=', t)
mw.log('\t...=', ...)
mw.log(debug.traceback())
return
end
current = current[k]
end
if type(current[k]) ~= "table" then
k = select(-1, ...)
current[k] = value
end
return type(value) == "table" and current[k] or t
end
local function addNewline(s)
if s:match("^[*:;#]") or s:match("^{|") then
return "\n" .. s .. "\n"
else
return s
end
end
--[[
识别单个数字或拆分逗号隔开的多个数字(@param key)并转化为多个数字。例如:
getKeys '1' → {1}
getKeys '1-4' → {1, 'list', 'content', 4}
getKeys '1-4-5' → {1, 'list', 'content', 4, 'list', 'content', 5}
不符合格式的键会返回nil。
可选参数(@param ...)会被加在表的最后。例如:
getKeys '1-4style' → {1, 'list', 'content', 4, 'style'}
]]
local function getkeys(key, ...)
local keys = {}
for eachnum in key:gmatch "[^%- ]+" do
-- 以横杠(减号)为分隔拆分数字
eachnum = tonumber(eachnum)
if not eachnum then
return nil
end
-- 循环体里面使用原始写法,以增加效率。
keys[#keys + 1] = eachnum
keys[#keys + 1] = "list"
keys[#keys + 1] = "content"
end
keys[#keys], keys[#keys - 1] = nil, nil
-- 最后的两个list和content不一定需要
-- 需要时会作为可选参数传入
if #keys == 0 then
return nil
end
for i = 1, select("#", ...) do
keys[#keys + 1] = select(i, ...)
end
return keys
end
-- 将上述函数导出为public以便于外部访问
p.getkeys = getkeys
-- 为便于维护,采用面向对象。
--[[
创建一个新的navbox对象。
创建时,其对应的html对象会被自动创建,也可以在参数中手动创建。
创建一个子框表时,会给它设置好根框表。
@param paras 可选的命名参数,包括:
rootself 如果正在创建一个子框表,则该参数为根框表对象
frame 框架对象,可能为null
args 该对象的参数
level 该对象的等级,根框表为0,子框表为1
root 该对象的根元素对象,如果为nil则会在渲染时准备
]]
function p.new(paras)
local obj = {}
local paras = paras or {}
obj.rootself = paras.rootself or obj
obj.data = paras.data or {}
local frame = paras.frame
obj.frame = paras.frame -- or nil
obj.args = paras.args or {}
obj.level = paras.level or 0
obj.root = paras.root
return setmetatable(
obj,
{
__index = p
}
)
end
function p:renderRow(groupArgs, listArgs, k)
-- 给定一个列的标题和文本,渲染这个列。
local listContent = listArgs.content
if not listContent then
return
end
local listStyle = listArgs.style
local listClass = listArgs.class
local groupContent = groupArgs.content
local groupStyle = groupArgs.style
local groupClass = groupArgs.class
local data = self.data
local isGroups = data.isGroups
-- 根框表的数据。
local rootdata = self.rootself.data
-- 对于子框表,其list的奇偶性存储在根框表中,而非子框表本身。
local isEven = self.rootself.isEven
local root = self.root
local row = root
if isGroups then
row = root:tag "div"
:addClass "navbox-list navbox-sole-row navbox"
:addClass "collapsible"
local selected = data.selected
local isSelected
if not selected or next(selected)==nil or selected.all or selected['*'] then
isSelected = true
elseif selected.none then
isSelected = false
elseif selected[listArgs.id] or selected[k] then
isSelected = true
end
if not isSelected then
row:addClass 'collapsed'
end
end
if groupContent then
local group =
row:tag "div" -- groupclass、groupstyle等不影响其子框表的group。
:addClass "navbox-cell"
:addClass(isGroups and "navbox-title navbox-sole-row" or "navbox-group")
:addClass(index(data, "group", "class")) -- 单独类,比如group1class
:addClass(groupClass) -- 共同类,即args中的groupclass
:cssText(index(data, "group", "style")) -- 单独样式,比如group1style
:cssText(groupStyle) -- 共同样式,比如groupstyle
:tag "span"
:wikitext(addNewline(groupContent))
:addClass(isGroups and "navbox-title-content" or nil)
:done()
end
local list = row:tag "div"
:addClass "navbox-list navbox-cell"
:addClass(listClass) -- 共同类,即args中的listclass
:cssText(listStyle) -- 共同样式,即args中的liststyle
-- 该列表所在直接框表中定义的样式
:addClass(index(data, "list", "class"))
:cssText(index(data, "list", "style"))
if rootdata ~= data then list
:addClass(index(rootdata, "list", "class"))
:cssText(index(rootdata, "list", "style"))
end
if isGroups or not groupContent then
list:addClass "navbox-sole-row"
end
if type(listContent) == "string" then
-- addNewline函数在前文中已定义
-- 一层span是为了适应grid,二层span是为了应付维基百科遗留下来的先</span>再<span>的烂代码
-- 未来肯定是不会有两层span了
list
:tag 'span':tag 'span'
:wikitext(addNewline(listContent))
-- 则这个list不受listclass、liststyle的影响。
self.rootself.isEven = not isEven -- 交换奇偶性
if isEven then
list
:addClass "navbox-even"
:addClass(index(rootdata, "even", "class"))
:cssText(index(rootdata, "even", "style"))
else
list
:addClass "navbox-odd"
:addClass(index(rootdata, "odd", "class"))
:cssText(index(rootdata, "odd", "style"))
end
elseif type(listContent) == "table" then
local subElement = list:addClass "navbox"
-- listngroupwidth
local groupwidth = listArgs.groupwidth
if groupwidth then
list:css('grid-template-columns', tostring(groupwidth) .. ' auto')
end
local subObj = self.new {
rootself = self,
root = subElement,
data = listContent,
level = (self.level or 0) + 1
}
subObj:render()
end
root:newline()
return row
end
function p:renderSingleRow(rowArgs)
-- 渲染标题、上方栏或下方栏
-- type可以是'title' 'above' 'below'
local content = rowArgs.content
if content then
content = addNewline(content)
else
return
end
local class = rowArgs.class
local style = rowArgs.style
local rowtype = rowArgs.rowtype or "unknown-rowtype"
local root = self.root
local level = rowArgs.level
local row =
root
:tag "div"
:addClass "navbox-cell navbox-sole-row"
:addClass("navbox-" .. rowtype):addClass(
class
):cssText(style):attr("colspan", "2")
if rowtype == "title" then
if level == 0 then
local navbarObj = rowArgs.navbarObj -- or nil
row:node(navbarObj):addClass(navbarObj and "navbox-title-with-navbar" or nil)
end
row:tag "span":addClass "navbox-title-content":wikitext(content)
else
row:wikitext(content)
end
row:newline()
return row
end
function p:hasBackgroundColors()
local data = self.data or '无法运行hasBackgroundColors函数,因为data字段不存在。'
return mw.ustring.match(data.title and data.title.style or '', 'background')
or mw.ustring.match(data.group and data.group.style or '', 'background')
end
function p:argNameAndRealTitleAreDifferent()
local args = self.args
return args.name and args.name ~= mw.title.getCurrentTitle().text
end
local prefixes = {
group = true,
list = true,
title = true,
above = true,
below = true,
body = true,
even = true,
odd = true,
id = true, abbr = true
}
local suffixes = {
content = true,
style = true,
class = true,
groupwidth = true,
[""] = true
}
--[[
检查这个键值对,并进行处理。
符合特定条件的键,将被“转录”到data参数(或self.data中)。
]]
function p:processArg(k, v, data)
data = data or self.data
-- 以下均只检查字符串键。
if type(k) ~= "string" then
return
end
-- 优先处理list开头的。这部分似乎不起作用,因为没有必要。
--[[local suffix = k:match "^list(%l+[%l%d%.%-]*)$"
if suffix and not suffixes[suffix] then
data.list = data.list or {}
self:processArg(suffix, v, data.list)
return
end]]--
local suffix, prefix
--[[
检查固定的前缀是否存在直接搭配了固定的后缀的情况。例如:
bodyclass(prefix = body, suffix = class)
titlestyle(prefix = title, suffix = title)
odd(prefix = odd, suffix = '')
若为:oddnothing(prefix = odd, suffix = nil),将在后面略过
]]
for localprefix, _ in pairs(prefixes) do
-- 检查通用属性
suffix = k:match("^" .. localprefix .. "(%l*)$")
-- 其他的值也有效,但是不起作用。
if suffix then
prefix = localprefix
break
end
-- 如果没有suffix进入下一轮循环
end
if prefix == 'body' then
-- 此时 suffix 不应该是空字符串或者'content',但是这里忽略了。
data[suffix] = v
elseif suffix == "" then
newindex(data, v, prefix, "content")
return
elseif suffix and suffixes[suffix] then
-- 这里的suffix通常是style、class之类
newindex(data, v, prefix, suffix)
return
end
--[[
检查带有数字的变量。在这个过程中,任何prefix和suffix都有可能被匹配到。
]]
local prefix, key, suffix = k:match "^(%l+)([0-9%.%-]+)(%l*)$"
if not prefixes[prefix] then
return -- 忽略不被识别的前缀
end
-- 对于group和list,以及其他的类型,采取不同的数据加工方式
local isCell = (prefix == "group" or prefix == "list")
-- 这里的prefix可以是group,list
-- 这里的suffix可以是空白、class或style
-- 这里的key可以是1、2、3或1-2、1-4这样的数字或多重数字。
if not suffix or not suffixes[suffix] then
key, suffix = k:match "^list([%d%.%-]+)(%l+[%l%d%-%.]*)$"
end
if not key then return end
if suffix == "" then suffix = "content" end
local keys = getkeys(key)
if not keys then return end
if not prefix then
self:processArg(suffix, v, newindex(data, {}, unpack(getkeys(key, "list", "content"))))
elseif (prefix == 'id' or prefix == 'abbr') and suffix == 'content' then
newindex(data, v, unpack(getkeys(key, 'list', 'id')))
elseif isCell then
newindex(data, v, unpack(getkeys(key, prefix, suffix)))
else
newindex(data, v, unpack(getkeys(key, "list", "content", prefix, suffix)))
end
end
function p:processArgs()
-- 这里的args是frame中未经重写处理的args。
-- 转录参数时,参数遍历两遍。
-- 这是为了防止普通参数占了data参数的位置导致不能newindex。
-- 先处理一些已知名称的参数,这些参数键的哈希值会提前编译,这样可以更快地
-- 被访问,无需走p:processArg的流程。
local args = self.args
local data = self.data
data.name = args.name or data.name
data.state = args.state or data.state
data.isChild = data.isChild
or (args[1] or args.border) == 'subgroup'
or (args[1] or args.border) == 'child'
data.nocat = boolean(args.nocat) or data.nocat
data.isGroups = boolean(args.isGroups) or data.isGroups
data.selected = data.selected or {}
local selected = data.selected
if type(args.selected) == 'string' then
for a_selected in mw.text.gsplit(args.selected, '[,;]%s*') do
selected[tonumber(a_selected) or a_selected] = true
end
end
data.style = args.style or data.style
data.class = args.class or data.class
data.groupwidth = args.groupwidth or data.groupwidth
for k, v in pairs(args) do
v = trim(v)
if v and v ~= "" then
self:processArg(k, v, data)
end
end
return self
end
--[[
用于在控制台测试参数的转换过程。
注意这是个静态方法。
]]
function p.test(args, params)
params = params or {}
assert(type(args)=='table', 'args should be table')
assert(type(params)=='table', 'params should be nil or table')
params.args = args
local obj = p.new(params)
obj:processArgs()
mw.logObject(obj.data, 'data')
return obj:render()
end
--[[
类似于p.test的一种更快的测试方式。
例如,
p.test2('list1-2', 'list1-3')
等价于
p.test{['list1-2'] = 'list1-2', ['list1-3'] = 'list1-3'}
]]
function p.test2(...)
local args = {}
local list = {...}
for _, v in ipairs(list) do
args[v] = v
end
return p.test(args)
end
function p:render()
local data = self.data or {}
-- 如果是通过args而非data创建的对象,
-- 渲染之前务必记得processArgs,以将args“转录”到data中。
-- 因为渲染过程只认data不认args。
self.root = self.root
or mw.html.create "div":addClass "navbox"
local root = self.root
local navbarObj
local level = self.level or 0
local state = data.state
root
:addClass("navbox-level")
:addClass("navbox-level-" .. (level or "unknown"))
-- navbox主体的类,可能是args.bodyclass或args.class
:addClass(data.class)
-- navbox主体的样式,可能是args.bodystyle或args.style
:cssText(data.style)
do
local groupwidth = data.groupwidth
if groupwidth then
root:css('grid-template-columns', tostring(groupwidth)..' auto')
end
end
root:addClass(
state == "collapsed" and "collapsible collapsed"
or state == "plain" and ""
or state == "collapsible" and "collapsible"
-- 若state未指定或为其他值,且该表不是子框表,则默认为collapsible
or (level == 0 and not data.isChild) and "collapsible"
or nil
)
-- 为表格添加模板样式。如果frame不存在,则不添加表格样式。
if level == 0 and self.frame and self.frame.getParent then
root:addClass "hlist"
local name = data.name
if name then
navbarObj =
navbar {
mini = 1,
name
}
end
end
-- 渲染标题。
self:renderSingleRow {
navbarObj = navbarObj,
content = index(data, "title", "content"),
class = index(data, "title", "class"),
style = index(data, "title", "style"),
rowtype = "title",
level = level
}
-- 渲染上方框。
self:renderSingleRow {
content = index(data, "above", "content"),
class = index(data, "above", "class"),
style = index(data, "above", "style"),
rowtype = "above",
level = level
}
-- 渲染列表。这是重头戏。
local isGroups = self.data.groups
for k, v in tools.extendedSparseIpairs(data) do
local groupContent = index(v, "group", "content")
local listContent = index(v, "list", "content")
local groupClass = index(v, "group", "class")
local groupStyle = index(v, "group", "style")
local listClass = index(v, "list", "class")
local listStyle = index(v, "list", "style")
local listGroupWidth = index(v, 'list', 'groupwidth')
self:renderRow(
index(v, 'group') or {},
index(v, 'list') or {}, k
)
end
-- 渲染下方框。
self:renderSingleRow {
content = index(data, "below", "content"),
class = index(data, "below", "class"),
style = index(data, "below", "style"),
rowtype = "below"
}
if self.data.nocat then
if self:hasBackgroundColors() then root:wikitext('[[Category:使用背景颜色的导航框]]') end
if self:argNameAndRealTitleAreDifferent() then root:wikitext('[[Category:name參數和實際不同的導航框]]') end
end
return root:allDone()
end
function p._navbox(args, frame)
-- 通常第三方模块可以不必这样使用
-- 可以设置data然后再render
local obj =
p.new {
args = args,
frame = frame
}
obj:processArgs()
return obj:render()
end
function p.navbox(frame)
-- 通过#invoke直接使用
local getArgs = require 'Module:Arguments'.getArgs
return p._navbox(getArgs(frame), frame)
end
function p.subgroup(frame)
local getArgs = require 'Module:Arguments'.getArgs
local args = getArgs(frame)
local obj = p.new {
args = args,
frame = frame,
data = {isChild = true}
}
return obj:processArgs():render()
end
function p.groups(frame)
local getArgs = require 'Module:Arguments'.getArgs
local args = getArgs(frame)
local obj = p.new {
args = args,
frame = frame,
data = {isGroups = true, state = 'plain'}
}
return obj:processArgs():render()
end
p.main = p.navbox
return p