Module:Navbox
本模块用于执行模板{{导航框}},用法详见模板说明。本模块是根据维基百科的Module:Navbox改写的,和维基百科的具有
运作原理[编辑源代码]
对于模板,我们会将模板的参数(args)转换成可用于直接读取的数据(data),然后对数据进行迭代处理,以渲染整个表。
每个框表都是一个对象,其HTML对象存储在self.root中。每一个子框表又是一个新的对象,该对象会与其根对象(rootself)关联,因此子框表与根框表即有一定的独立性,又有一定的关联(子框表的listclass、liststyle取决于根框表而与子框表本身的设置无关,且子框表的列表和根框表的列表的奇偶性是一同处理的)。
index函数与newindex函数以及tnewindex函数[编辑源代码]
index函数和newindex函数可以使得数据的处理变得更加方便。可以快速地求得一个表的嵌套域(即域中的域),同时避免无法索引空值(nil)而抛出的错误。
getkeys函数[编辑源代码]
这个函数以连字符为分隔拆分匹配出的数字组合,然后将每一个拆分出的字符串tostring。任何一个无法拆分都将返回nil。拆分后的数字将会夹着list和content被放入表中返回。
参数键的识别[编辑源代码]
本模块采用类似于正则表达式的“匹配模式”(patterns)来对参数(args)的键(key)进行解析(p.checkItem函数):
- 第一遍迭代识别以data开头,以数字结尾的参数键,并将其值解码为表作为子导航框数据。
- 第二遍迭代:
- 对于name这一类单一的键,直接将其搬到data中。
- 识别以list开头,其余部分为一个完整的键的键(其实这并没有用)。
- 识别以title、above、below等开头(前缀)、其余部分(后缀)均为小写字母的键,这里的后缀通常是style、class、content或者为空,空和content是等价的。
- 识别“小写字母(前缀)+数字组合+小写字母(后缀)”这样的键,前缀通常是group、list、title、above、below等,后缀同上一条。
- 如果不识别,或后缀不为style、class、content及为空,并且前缀为list,后缀依然作为一个完整的参数键传入子导航框的数据(但函数还是使用根导航框的checkItem函数。
- 对于单个数字的数字组合(比如1、2、3),如果前缀是group或list,相当于对该框表的特定的组或列进行定义,否则视为对特定位置的子框表的title、above、below等进行定义。
- 对于多个数字的数字组合(用“-”隔开,比如1-4、2-3、5-1、2-3-2),则是对子框表进行定义。比如title2-3相当于第二列的子框表中的第三列的子框表的标题内容,list4-2表示第四列的子框表的第二列。
- 如果定义了一个列,又定义了其列的子框表,则子框表内容优先。比如同时定义了list2-3和list2,则第二列为一个子框表,list2-3定义了这个子框表的第三列,list2则直接被忽略。
参数与数据的转换[编辑源代码]
本模块在渲染导航框前,需要先通过p.processArgs函数将对象内的参数(args)转换成数据(data)以供使用。
数据的转换过程中,需要用到上面提到过的index和newindex函数。例如,参数:
self.args = {
above = "Above",
below = "Below",
["below4-3"] = "Below4-3",
bodyclass = "mobileplainbox",
group1 = "Group1",
group10 = "Group10",
group2 = "Group2",
["group2-1"] = "Group2-1",
["group2-2"] = "Group2-2",
["group2-2style"] = "color:green;",
group3 = "Group3",
group4 = "Group4",
["group4-1"] = "Group4-1",
["group4-2"] = "Group4-2",
["group4-3"] = "Group4-3",
["group4-3-1"] = "Group4-3-1",
["group4-3-2"] = "Group4-3-2",
group8 = "Group8",
group9 = "Group9",
list1 = "List1",
list10 = "List10",
["list2-1"] = "List2-1",
["list2-2"] = "List2-2",
["list2-2style"] = "color:red;",
list3 = "List3",
["list4-1"] = "List4-1",
["list4-2"] = "List4-2",
["list4-3"] = "List4-3(这个属性应该被忽略)",
["list4-3-1"] = "List4-3-1",
["list4-3-2"] = "List4-3-2",
list8 = "List8",
list9 = "List9",
["list9.5"] = "List9.5(小数是有效的,这在追加插入时尤其有用。)",
name = "模块:Navbox",
oddstyle = "color:navy;",
title = "Title",
title4 = "Title-4"
}
它应该被转换成:
self.data = {
[1] = {
group = { content = "Group1" },
list = { content = "List1" },
},
[2] = {
group = { content = "Group2" },
list = {
content = {
[1] = {
group = { content = "Group2-1" },
list = { content = "List2-1" },
},
[2] = {
group = { content = "Group2-2", style = "color:green;" },
list = { content = "List2-2", style = "color:red;" },
},
},
},
},
[3] = {
group = { content = "Group3" },
list = { content = "List3" },
},
[4] = {
group = { content = "Group4" },
list = {
content = {
[1] = {
group = { content = "Group4-1" },
list = { content = "List4-1" },
},
[2] = {
group = { content = "Group4-2" },
list = { content = "List4-2" },
},
[3] {
group = { content = "Group4-3" },
list = {
content = {
[1] = {
group = { content = "Group4-3-1" },
list = { content = "List4-3-1" },
},
[2] = {
group = { content = "Group4-3-2" },
list = { content = "List4-3-2" },
},
below = { content = "Below4-3" },
},
},
},
title = { content = "Title-4" },
},
},
},
[8] = {
group = { content = "Group8" },
list = { content = "List8" },
},
[9] = {
group = { content = "Group9" },
list = { content = "List9" },
},
[9.5] = {
list = "List9.5(小数是有效的,这在追加插入时尤其有用。)"
},
[10] = {
group = { content = "Group10" },
list = { content = "List10" },
},
above = { content = "Above" },
below = { content = "Below" },
body = {
class = "mobileplainbox",
},
group = { content = "Group8" },
list = { content = "List4-3-2" },
name = "模块:Navbox",
odd = {
style = "color:navy;",
},
title = { content = "Title" },
}
转换后,当前效果如下:
渲染[编辑源代码]
每一个框表对象都含有一个mw.html对象(该对象在代码中称为root),子框表也是如此。与维基百科版本不同的是,子框表的mw.html对象也是根框表的mw.html的一个子对象,最终一同被渲染成字符串,而不是先将子框表渲染成字符串再作为根框表的内容。
此版本的导航框是一个使用display: grid
的div
元素实现的,每一个单元格都是它的一个子元素。此外,导航框的标题单元格.navbox-title
也是一个grid,以实现标题内容与导航栏、折叠按钮的排版。
此版本并没有对各元素硬编码什么样式,绝大多数样式都是通过CSS层叠样式表(参考MediaWiki:Gadget-navbox.css)实现的。由于本站在移动版视图使用和桌面版视图一致的Timeless皮肤,因此这些导航框在手机版可以正常显示。此外还默认设置了hlist类(暂时无法取消),这样,在需要使用到列表时不再需要设置listclass=hlist。
注意:渲染各列时,模块会先将所有含有list的列的键整合到一个序列表中,进行排序再依次渲染。这样可以避免由空缺时的渲染次序问题,比如如果指定了第1、2、4、5列而没有指定第3列,就一定会按照第1、2、4、5列的顺序渲染,而非1、2、5、4。
--
-- This module will implement {{Navbox}}
-- 这是该模板的更新测试版本。
--
local p = {}
local navbar = require 'Module:Navbar'._navbar
local util = require 'Module:Util'
local trim = mw.text.trim
local index = util.multiIndex
local newindex = util.multiNewIndex
local addNewline = util.addNewline
local function getkeys(key, ...)
-- 识别单个数字或拆分逗号隔开的多个数字并转化为number,
-- 非法的key会返回nil。
-- 拆分出的每个结果会夹着list和content并传入newindex。
-- 可选参数会被加在表的最后。
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
-- 为便于维护,采用面向对象。
function p.new(paras)
-- 创建一个新的navbox对象。
-- 创建时,其对应的html对象会被自动创建,也可以在参数中手动创建。
-- 创建一个子框表时,会给它设置好根框表。
local obj = {}
local paras = paras or {}
obj.rootself = paras.rootself or obj
obj.data = paras.data or {}
obj.frame = paras.frame -- or nil
obj.args = paras.args or {}
obj.level = paras.level or 0 -- 对象的级别,根框表为0,子框表为1
obj.root = paras.root -- 通常为nil,如有需要会在渲染时准备好
return setmetatable(obj,{
__index=p
})
end
function p:renderRow(groupArgs, listArgs)
-- 给定一个列的标题和文本,渲染这个列。
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 rootdata = self.rootself.data
-- 对于子框表,其list的奇偶性存储在根框表中,而非子框表本身。
local isEven = self.rootself.isEven
local root = self.root
local row = root
if groupContent then
local group = row:tag 'div'
:addClass 'navbox-group navbox-cell'
:tag 'span'
:addClass 'navbox-group-flex-inner'
:wikitext(addNewline(groupContent))
-- groupclass、groupstyle等不影响其子框表的group。
:addClass(index(data,'group','class')) -- 共同类,即args中的groupclass
:addClass(groupClass) -- 单独类,比如group1class
:cssText(index(data,'group','style')) -- 共同样式,比如groupstyle
:cssText(groupStyle) -- 单独样式,比如group1style
end
local list = row:tag 'div'
:addClass 'navbox-list navbox-cell'
if not groupContent then
list
:addClass 'navbox-sole-row'
end
if type(listContent) == 'string' then
-- addNewline函数在前文中已定义
list:wikitext(addNewline(listContent))
-- listclass、liststyle等会影响其子框表的list。
-- 且如果一个list的内容是一个子框表,
-- 则这个list不受listclass、liststyle的影响。
:addClass(index(rootdata,'list','class'))
:cssText(index(rootdata,'list','style'))
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'
local subObj = self.new{
rootself = self,
root = subElement,
data = listContent,
level = (self.level or 0) + 1
}
subObj:render()
end
list
:addClass(listClass) -- 单独类,如list1class
:cssText(listStyle) -- 单独样式,如list1style
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)
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)
:addClass('stikitable-cell')
end
row:tag 'span'
:addClass 'navbox-title-content'
:wikitext(content)
else
row:wikitext(content)
end
return row
end
local suffixes = {
content = true,
style = true,
class = true,
[''] = true
}
function p:checkItem(k, v, data)
-- 对于参数的一个键值对,对其键进行检查。
-- 符合特定条件的键会被以特定的方式“转录”到self:data中。
-- “转录”过程中需要用到前面定义的newindex。
data = data or self.data
-- 首先检查一些固定的参数,检查成功即跳出函数。
if k=='name' or k=='state' then
data[k]=v
return
end
-- 以下均只检查字符串键。
if type(k)~='string' then return end
-- var
local suffix = k:match("^var([%-%l]+)")
if suffix then
newindex(data,v,"var",suffix)
return
end
-- 对于list前缀,如果其suffix不合法,后缀会作为一个完整的参数键传入list表。
-- 其实并没有用,因为“遗传”功能还没有开发。
local suffix = k:match "^list(%l+[%l%d%.%-]*)$"
if suffix and not suffixes[suffix] then
data.list = data.list or {}
self:checkItem(suffix,v,data.list)
return
end
local suffix, prefix
-- 对下列这些前缀,检查其后缀(可以为空)。
-- 比如title、abovestyle、belowclass之类的。
for _, localprefix in
ipairs{'title','above','below','group','list','body','even','odd'}
do
-- 检查通用属性
suffix = k:match('^' .. localprefix .. '(%l*)$') -- 2021.6.14: +$
-- 这里的suffix可以是空值,或者style、class,
-- 其他的值也有效,但是不起作用。
if suffix then prefix = localprefix break end
-- 如果没有suffix进入下一轮循环
end
if suffix=='' then
newindex(data,v,prefix,'content')
return
elseif suffix and suffixes[suffix] then
-- 这里的suffix通常是style、class之类
newindex(data,v,prefix,suffix)
return
end
-- 检查带有数字的变量。
local prefix, key, suffix = k:match '^([a-z]+)([0-9%.%-]+)(%l*)$'
--2021.6.14:floatable
-- 对于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
local keys1, keys2, keys3 = getkeys(key,'list','content'),
getkeys(key,prefix,suffix),
getkeys(key,'list','content',prefix,suffix)
if not prefix then
self:checkItem(suffix,v,newindex(data,{},unpack(keys1)))
elseif isCell then
newindex(data,v,unpack(keys2))
else
newindex(data,v,unpack(keys3))
end
end
function p:processArgs()
-- 这里的args是frame中未经重写处理的args。
-- 转录参数时,参数遍历两遍。
-- 这是为了防止普通参数占了data参数的位置导致不能newindex。
for k,v in pairs(self.args) do
v = trim(v)
if v and v~='' then self:checkItem(k,v) end
end
end
function p:render()
-- 如果是通过args而非data创建的对象,
-- 渲染之前务必记得processArgs,以将args“转录”到data中。
-- 因为渲染过程只认data不认args。
self.root = self.root
or mw.html.create 'div'
-- :addClass 'content-table-wrapper navbox-wrapper' -- 手动替代Timeless将table包裹住,以便于选择器选择。
-- :tag 'span' -- 使用span而非div是为了避免Timeless重新将表格再包裹一次。这种情况需要再搭配display:block才能起到效果。
-- :addClass 'content-table'
-- :tag 'div' -- root
:addClass 'navbox'
local root = self.root
local navbarObj
local data = self.data or {}
self.rootdata = self.rootself.data or {}
local level = self.level or 0
local state = data.state
root
:addClass('navbox-level-'..(level or 'unknown'))
:addClass(index(data,'body','class'))
:addClass(state=='collapsed' and 'collapsible collapsed' or
state=='plain' and '' or
state=='collapsible' and 'collapsible' or
level==0 and 'collapsible' or nil) -- 若state未指定,则只有在level=0时才是可折叠的。
:cssText(index(data,'body','style'))
if data.var then
-- root:tag "span":addClass("css-variables"):css("display","none"):wikitext(mw.text.jsonEncode(data.var))
for k, v in pairs(data.var) do
root:css("--navbox-" .. k, v)
end
end
if level==0 then
root:addClass 'stikitable hlist'
local name = data.name
if name then
navbarObj = navbar{
mini = 1,
title = 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
}
-- 渲染列表。这是重头戏。
for k,v in util.sortedNumberIpairs(data) do
-- 上述9行语句其实就相当于
-- for k,v in pairs(data) do
-- 但是考虑到断层参数的排序问题
-- 所以先进行人工排序再迭代
local groupContent = index(v,'group','content')
local listContent = index(v,'list','content')
local id = v.id or k -- 这个k其实没什么用
local groupClass = index(v,'group','class')
local groupStyle = index(v,'group','style')
local listClass = index(v,'list','class')
local listStyle = index(v,'list','style')
self:renderRow({
content=groupContent,
class=groupClass,
style=groupStyle,
level=level
},{
content=listContent,
class=listClass,
style=listStyle,
level=level,
})
end
-- 渲染下方框。
self:renderSingleRow{
content = index(data,'below','content'),
class = index(data,'below','class'),
style = index(data,'below','style'),
rowtype = 'below'
}
-- root
-- :wikitext((self.frame or mw.getCurrentFrame())
-- :extensionTag('templatestyles', nil, {src = 'Template:导航框/style.css'}))
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.direct(frame)
-- 通过#invoke直接使用
return p._navbox(frame.args, frame)
end
function p.navbox(frame)
-- 通过模板使用
return p.direct(frame:getParent())
end
return p