模块:Navbox

求闻百科,共笔求闻

本模块用于实现模板链接:{{navbox}}系列模板。

实现方式

本模块会先将参数表args转化为data,并根据data进行渲染。可以使用p.new创建对象,并使用obj.render()将其转化为可渲染的mw.html对象。

在控制台中调试时,可以使用p._navbox测试参数并返回导航框,其他模块在使用本模块时也应当使用该方法。p.testp.test2还能用来测试将参数转化为数据的过程,后者更加简便。

上述文档内容嵌入自Module:Navbox/doc编辑 | 历史
编者可以在本模块的沙盒编辑和测试样例创建页面进行实验。
请将模块自身所属的分类添加在文档中。本模块的子页面
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