模块:Navbox

来自滚动的天空Wiki
跳到导航 跳到搜索
文档图示 模块文档[查看] [编辑] [历史] [清除缓存]

本模块用于执行模板{{导航框}},用法详见模板说明。本模块是根据维基百科的Module:Navbox改写的。

运作原理[编辑]

对于模板,我们会将模板的参数(args)转换成可用于直接读取的数据(data),然后对数据进行迭代处理,以渲染整个表。

每个框表都是一个对象,其HTML对象存储在self.root中。每一个子框表又是一个新的对象,该对象会与其根对象(rootself)关联,因此子框表与根框表即有一定的独立性,又有一定的关联(子框表的listclass、liststyle取决于根框表而与子框表本身的设置无关,且子框表的列表和根框表的列表的奇偶性是一同处理的)。

index函数与newindex函数[编辑]

index函数和newindex函数可以使得数据的处理变得更加方便。可以快速地求得一个表的嵌套域(即域中的域),同时避免无法索引空值(nil)而抛出的错误。

参数键的识别[编辑]

本模块采用类似于正则表达式的“匹配模式”(patterns)来对参数(args)的键(key)进行解析(p.checkItem函数):

  • 对于name这一类单一的键,直接将其搬到data中。
  • 识别以title、above、below等开头(前缀)、其余部分(后缀)均为小写字母的键,这里的后缀通常是style、class、content或者为空,空和content是等价的。
  • 识别“小写字母(前缀)+数字组合+小写字母(后缀)”这样的键,前缀通常是group、list、title、above、below等,后缀同上一条。
    • 对于单个数字的数字组合(比如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 = "mw-collapsible 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",
    name = "模块:Navbox",
    oddstyle = "color:navy;",
    title = "Title",
    title4 = "Title-4"
}

它应该被转换成:

self.data = {
  {
    group = {
      content = "Group1",
    },
    list = {
      content = "List1",
    },
  },
  {
    group = {
      content = "Group2",
    },
    list = {
      content = {
        {
          group = {
            content = "Group2-1",
          },
          list = {
            content = "List2-1",
          },
        },
        {
          group = {
            content = "Group2-2",
            style = "color:green;",
          },
          list = {
            content = "List2-2",
            style = "color:red;",
          },
        },
      },
    },
  },
  {
    group = {
      content = "Group3",
    },
    list = {
      content = "List3",
    },
  },
  {
    group = {
      content = "Group4",
    },
    list = {
      content = {
        {
          group = {
            content = "Group4-1",
          },
          list = {
            content = "List4-1",
          },
        },
        {
          group = {
            content = "Group4-2",
          },
          list = {
            content = "List4-2",
          },
        },
        {
          group = {
            content = "Group4-3",
          },
          list = {
            content = {
              {
                group = {
                  content = "Group4-3-1",
                },
                list = {
                  content = "List4-3-1",
                },
              },
              {
                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",
    },
  },
  [10] = {
    group = {
      content = "Group10",
    },
    list = {
      content = "List10",
    },
  },
  above = {
    content = "Above",
  },
  below = {
    content = "Below",
  },
  body = {
    class = "mw-collapsible mobileplainbox",
  },
  group = {
    content = "Group8",
  },
  list = {
    content = "List4-3-2",
  },
  name = "模块:Navbox",
  odd = {
    style = "color:navy;",
  },
  title = {
    content = "Title",
  },
}

转换后,当前效果如下:

Title
Above
Group1List1
Group2
Group2-1List2-1
Group2-2List2-2
Group3List3
Group4
Title-4
Group4-1List4-1
Group4-2List4-2
Group4-3
Group4-3-1List4-3-1
Group4-3-2List4-3-2
Below4-3
Group8List8
Group9List9
Group10List10
Below

渲染[编辑]

每一个框表对象都含有一个mw.html对象(该对象在代码中称为root),子框表也是如此。与维基百科版本不同的是,子框表的mw.html对象也是根框表的mw.html的一个子对象,最终一同被渲染成字符串,而不是先将子框表渲染成字符串再作为根框表的内容。

此外,此版本的导航框中,一个单元格就是一个单元格,一个表格就是一个表格,不存在专门的表格来实现间隙之类的,也不区分外框表(body)和内框表(inner)。为了确保美观,此版本的单元格间隙是通过透明的border(边框)实现的,同时设置了{background-clip: padding-box !important},从而防止单元格背景延伸到间隙内。

此版本并没有对各元素硬编码什么样式,绝大多数样式都是通过CSS层叠样式表(参考MediaWiki:Gadget-navbox.css)实现的。由于规避了navbox类(准确地说,应该是指符合正则表达式\bnavbox\b的类),因此它可以在手机版视图中显示(此版本还提供了专门适合手机版显示的类mobileplainbox,详见模板文档)。此外还默认设置了hlist类(暂时无法取消),这样,在需要使用到列表时不再需要设置listclass=hlist。

注意:渲染各列时,模块会先将所有含有list的列的键整合到一个序列表中,进行排序再依次渲染。这样可以避免由空缺时的渲染次序问题,比如如果指定了第1、2、4、5列而没有指定第3列,就一定会按照第1、2、4、5列的顺序渲染,而非1、2、5、4。

上述文档内容嵌入自模块:Navbox/doc编辑 | 历史
编者可以在本模块的沙盒创建 | 镜像和测试样例创建页面进行实验。
请将模块自身所属的分类添加在文档中。本模块的子页面
--
-- This module will implement {{Navbox}}
-- 这是该模板的更新测试版本。
--
 
local p = {}
 
local navbar = require('Module:Navbar')._navbar
 
local trim = mw.text.trim

local function addNewline(s)
	-- 参数内容以这些字符开头时,添加一个换行符,
	-- 这是为了成功形成其格式。
    if s:match('^[*:;#]') or s:match('^{|') then
        return '\n' .. s ..'\n'
    else
        return s
    end
end

local function index(t, ...)
	-- 本函数用于求取层叠表的一个域。
	-- 例如index(a,'b','c','d')
	-- 就相当于获得a.b.c.d,
	-- 并且,如果a、a.b、a.b.c中的任何一个如果不是表,
	-- 则直接返回nil,避免出错。
	local paras = {...}
	local result = t
	for k,v in ipairs(paras) do
		if type(result)=='table' then
			result = result[v]
		else
			return nil
		end
	end
	return result
end

local function newindex(t, value, ...)
	-- 类似于index。
	-- 例如,newindex(t,3,'a','b','c')就相当于设置t.a.b.c=3。
	-- 并且如果t.a、t.a.b中的任何一个不是表,则将其修改为表。
	-- 注意:如果是其他数据类型,也会被强制改成表。
	local paras = {...}
	if #paras <1 then return end
	local current = t
	local num = #paras
	for k,v in ipairs(paras) do
		if k==num then
			if type(current[v])=='table' then return end
			current[v] = value
		else
			if type(current[v])~='table' then
				current[v] = {}
			end
			current = current[v]
		end
	end
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.args = paras.args or {}
	obj.level = paras.level or 0 -- 对象的级别,根框表为0,子框表为1
	obj.isChild = paras.isChild -- 默认为nil
	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:tag 'tr'
		:addClass 'newstyle_navbox-row'
	
	if groupContent then
		local group = row:tag 'th'
			:addClass 'newstyle_navbox-group'
			: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 'td'
		:addClass 'newstyle_navbox-list'
	
	if not groupContent then
		list
			:attr('colspan','2')
			:addClass 'newstyle_navbox-list-without-group'
	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','class'))
		self.rootself.isEven = not isEven -- 交换奇偶性
		if isEven then
			list
				:addClass 'newstyle_navbox-even'
				:addClass(index(rootdata,'even','class'))
				:cssText(index(rootdata,'even','style'))
		else
			list
				:addClass 'newstyle_navbox-odd'
				:addClass(index(rootdata,'odd','class'))
				:cssText(index(rootdata,'odd','style'))
		end
	elseif type(listContent) == 'table' then
		local subElement = list
			:addClass 'newstyle_navbox-list-with-subgroup'
			:tag 'table'
			:addClass 'newstyle_navbox newstyle_navbox-subgroup'
		local subObj = self.new{
			rootself = self,
			root = subElement,
			data = listContent,
			isChild = true,
			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
	
	-- 适配title的navbar功能
	if rowtype == 'title' and level == 0 then
		local navbarObj = rowArgs.navbarObj or ''
		content = navbarObj .. content
		root:addClass 'newstyle_navbox-with-navbar'
	end
	
	local row = root:tag 'tr'
		:addClass 'newstyle_navbox-row'
		:tag (rowtype=='title' and 'th' or 'td')
		:attr('colspan','2')
		:addClass('newstyle_navbox-' .. rowtype)
		:addClass(class)
		:cssText(style)
		:wikitext(content)
	if rowtype=='above' or rowtype=='below' then
		row:addClass 'newstyle_navbox-abovebelow'
	end
	return row
end

function p:checkItem(k,v)
	-- 对于参数的一个键值对,对其键进行检查。
	-- 符合特定条件的键会被以特定的方式“转录”到self:data中。
	-- “转录”过程中需要用到前面定义的newindex。
	local data = self.data
	
	-- 首先检查一些固定的参数,检查成功即跳出函数。
	if k=='name' then
		data[k]=v
		return
	end
	if k==1 and v=='child' then
		data.isChild = true
	end
	
	-- 以下均只检查字符串键。
	if type(k)~='string' then return end
	
	-- 对下列这些前缀,检查其后缀(可以为空)。
	-- 比如title、abovestyle、belowclass之类的。
	for _, prefix in ipairs{'title','above','below',
				'group','list','body','even','odd'} do
		-- 检查通用属性
		local suffix=k:match('^' .. prefix .. '([a-z]*)')
		-- 这里的suffix可以是空值,或者style、class,
		-- 其他的值也有效,但是不起作用。
		if suffix=='' then
			newindex(data,v,prefix,'content')
			break
		elseif suffix then
			-- 这里的suffix通常是style、class之类
			newindex(data,v,prefix,suffix)
			break
		end -- 如果没有suffix进入下一轮循环
	end
	
	-- 检查带有数字的变量。
	local prefix, key, suffix = k:match '^([a-z]+)([0-9%-]+)([a-z]*)$'
	-- 对于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 key then return end
	if suffix=='' then suffix='content' end
	
	-- 如果是匹配单个数字,比如list2style、group4class。
	local id=tonumber(key)
	if id then
		if isCell then
			newindex(data,v,id,prefix,suffix)
		else
			newindex(data,v,id,'list','content',prefix,suffix)
		end
		return
	end
	
	-- 如果是匹配双个数字(数字用连字符隔开,下同),
	-- 比如list2-3style、group2-3class。
	-- 应该可以直接匹配数值的,后续会继续优化。
	local id2
	id,id2 = key:match '^([0-9]+)%-([0-9]+)$'
	id,id2 = tonumber(id),tonumber(id2)
	if id and id2 then
		if isCell then
			newindex(data,v,id,'list','content',id2,prefix,suffix)
		else
			newindex(data,v,id,'list','content',id2,'list','content',prefix,suffix)
		end
		return
	end
	
	-- 如果是匹配三个,比如list2-3-5style、group1-5-4class。
	local id3
	id,id2,id3 = key:match '^([0-9]+)%-([0-9]+)%-([0-9]+)$'
	id,id2,id3 = tonumber(id),tonumber(id2),tonumber(id3)
	if id and id2 and id3 and isCell then
		newindex(data,v,id,'list','content',id2,'list','content',
			id3,prefix,suffix)
	end
end

function p:processArgs()
	-- 这里的args是frame中未经重写处理的args。
	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 'table'
			:addClass 'newstyle_navbox'
	local root = self.root
	local navbarObj
	local data = self.data or {}
	self.rootdata = self.rootself.data or {}
	self.isChild = self.isChild or data.isChild
	local isChild = self.isChild
	local level = self.level or 0
	if isChild then
		root:addClass 'newstyle_navbox-subgroup'
	end
	root
		:addClass('newstyle_navbox-level-'..(level or 'unknown'))
		:addClass(index(data,'body','class'))
		:cssText(index(data,'body','style'))
	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
	}
	
	-- 渲染列表。这是重头戏。
	-- 考虑到空缺参数问题,所以要先将键做成序列进行排序,再进行迭代。
	local keys = {}
	for k,v in pairs(data) do
		-- keys是由data中所有整数键组成的序列
		if type(k)=='number' then
			table.insert(keys,k)
		end
	end
	table.sort(keys)
	for i,k in pairs(keys) do
		local v = data[k]
		-- 上述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'
	}
	return root
end

function p._navbox(args)
	-- 通常第三方模块可以不必这样使用
	-- 可以设置data然后再render
	local obj = p.new{args=args}
	obj:processArgs()
	local root = obj:render()
	return root
end

function p.direct(frame)
	-- 通过#invoke直接使用
    return p._navbox(frame.args)
end

function p.navbox(frame)
	-- 通过模板使用
	return p._navbox(frame:getParent().args)
end
 
return p