文档图示 模块文档[查看] [编辑] [历史] [清除缓存]

本模块是实现一系列交通相关模板功能的基础模块,支持管理各城市公共汽车系统的数据资料,目前主要用于生成一个显示公交线路资料的表格。

子模块一览

目前,各公交系统均使用独立的模板,并依赖本模块子页面的相关资料。下表为本模块已建立的子模块。您也可以仿照后文所述的格式新建子模块(并不限于中国大陆城市),并将其添加至下表。欢迎各位对已有资料进行定期维护更新。

相互引用情况

珠三角各系统
位置
Module:CNBUS/
被引用系统
肇庆 珠海 江门 中山 佛山 广州 东莞 惠州 深圳
系统 肇庆 不適用 ZQ/foshan
珠海 不適用 ZH/jiangmen ZH/data
ZH/zhongsha
江门 JM/zhuhai 不適用 JM/zhongshan JM/foshan
中山 ZS/zhuhai ZS/data 不適用 ZS/foshan
佛山 FS/zhaoqing FS/data
FS/jiangmen
FS/data
FS/zhongshan
不適用 FS/guangzhou
广州 GZ/foshan 不適用 GZ/dongguan
东莞 DG/guangzhou 不適用 DG/huizhou DG/shenzhen
惠州 HZ/dongguan 不適用 HZ/shenzhen
深圳 SZ/dongguan SZ/huizhou 不適用
潮汕地区各系统
位置
Module:CNBUS/
被引用系统
汕头 潮州 揭阳
系统 汕头 不適用 ST/jieyang
潮州 CZ/shantou 不適用 CZ/jieyang
揭阳 JY/shantou 不適用

接口一览

直接调用接口的模板的参数表(传入)和其调用接口时显式指定的参数表(传出)都会被识别,两者优先级参见Module:Arguments。由本模块导出的模板可能还提供了其他别名。

接口参数列表
参数 说明 列表 折叠列表 运营商颜色 线路名称 别名与注释
{{{1}}} {{{2}}} 线路代码列表 需要 需要 不適用 单个
{{{city}}} 城市代码 需要 需要 需要 需要 各城市模板默认填写
{{{area}}} 区域代码 需要 需要 不適用 需要 部分模板提供{{{loc}}}别名
{{{operator}}} 运营商代码 不適用 不適用 需要 不適用 {{{company}}}
区别于{{{operators}}}
{{{start}}} 是否开始表格
输出<table>开标签及表头
可选 可选 不適用 不適用 默认为真
{{{end}}} 是否结束表格
输出</table>
可选 可选 不適用 不適用 默认为真
{{{header}}} 表格标题 可选 可选 不適用 不適用 {{{info}}} {{{station}}}
依赖于{{{start}}}
{{{type}}} 列表样式 可选 不適用 不適用 不適用 BRT:覆盖{{{fare}}}{{{operators}}}{{{vehicles}}}
{{{time}}} 是否显示时间 可选 不適用 不適用 不適用
{{{fare}}} 是否显示票价 可选 不適用 不適用 不適用 默认为真
{{{operators}}} 是否显示运营商 可选 不適用 不適用 不適用 默认为真
区别于{{{operator}}}
{{{vehicles}}} 是否显示车型 可选 不適用 不適用 不適用
{{{image}}} 是否显示图片 可选 不適用 不適用 不適用

list

通过输入一个或多个线路编号以生成包含这些线路资料的表格。目前支持起讫点、线路方向、营运公司(分公司)、票价、运营时间、车辆图片、线路配车、BRT站台信息、备注等信息。

{{#invoke:CNBUS |list |city=#包含系统 }}

编号线路及运营时间收费运营商备注
1芳村花园南门
6:00–22:30
东山署前路
6:00–22:30
2元一汽一分芳村客运站

或者,线路也可以使用单个模式匹配表达式(必须以^开头)指定,用例参见重庆公交线路列表 (中心城区)。常用的代码匹配方式如:

  • ^T:所有T开头的线路;
  • ^%d%d%D*$:所有两位数的线路(允许非数字后缀,不允许更多数字);
  • ^1%d%d%D*$:1开头三位数的线路(允许非数字后缀,不允许更多数字)。

注意,输出的线路将按照线路代码以字符串排序,这意味着2会排在10的后面;此情形下建议配合{{{start}}}{{{end}}}将不同位数代码分拆多个表格显示。

collapsibleList

类似list,但只会生成一个的简化版的表格。目前仅支持起讫点和线路方向。

{{#invoke:CNBUS |collapsibleList |city=#包含系统 }}

color

属于辅助功能,可输出代表线路运营商的颜色代号。list 已集成该功能。

lineName

展示简短行内链接。如{{惠州巴士路线极简列表|1}}:公交路线:1

数据格式

本模块约定将数据存储在子模块中。

城市总表模块

在将本模块引入公交系统前,首先需要建立一个子模块作为该系统的数据模块。请将该子模块命名为Module:CNBUS/<城市代码>,其基本框架为:

local xx = {
    areas = { },
    operators = { }
}

xx.areas['xx'] = {
    name = "<区域名>",
    page = "<线路列表条目名>",
    source = "Module:CNBUS/XX/data", -- 对应模块
    aliases = { "XX", "理塘", "default" } -- 
}

xx.operators['bus'] = {
    color = "red",
    aliases = { "Bus", "公交集团" }
}
xx.operators['transport'] = {
    color = "silver",
    aliases = { "交运集团" }
}

return xx

区域表

每个子模块可包含一个或多个子区域,可分别存放城区、郊区、外市路线的资料。在本模块设置各区域资料的例子如下:

gz.areas['guangzhou'] = {
	name = "广州",
	page = "广州巴士路线列表",
	source = "Module:CNBUS/GZ/data",
	aliases = { "Guangzhou", "GZ", "gz", "广州", "廣州", "default" }
}
gz.areas['nansha'] = {
	name = "南沙",
	page = "南沙巴士路线列表",
	source = "Module:CNBUS/GZ/nansha",
	aliases = { "Nansha", "NS", "ns", "南沙" }
}
gz.areas['foshan'] = {
	name = "佛山",
	page = "佛山巴士路线列表",
	source = "Module:CNBUS/GZ/foshan",
	aliases = { "Foshan", "FS", "fs", "佛山" }
}

其中,source的值为子区域各线路的详细资料;pagename用于设置list和collapsibleList的标题中指向列表条目的内部链接([[page|name]],如[[广州巴士路线列表|广州]]);aliases 则包含了该子区域的别名,由area参数调用。

本例中共有3个区域,分别为“guangzhou”、“nansha”和“foshan”。在未提供area参数的值,或area参数的值为“guangzhou”、“Guangzhou”、“GZ”、“gz”、“广州”、“廣州”时,则选择区域“guangzhou”。“nansha”和“foshan”同理。

运营商表

在本模块设置各运营商颜色的例子如下:

xx.operators['bus1'] = {
    color = "orange", -- 颜色
    aliases = { "一汽一分", "一汽二分" } -- 别名。键值本身(此处为bus1)不需要包含其中
}
xx.operators['bus3'] = {
    color = "#fff600",
    aliases = { "三汽一分", "三汽二分" }
}
xx.operators['other'] = {
    color = "white",
}
xx.operators['multi'] = {
    color = "black",
}

在本例中,一汽和三汽公司的代码分别为bus1bus3,则运营商为“一汽一分”和“一汽二分”的代表色为“orange”(橙色),“三汽一分”和“三汽二分”的代表色为“#fff600”(近似于黄色)。此外,还需要设置“other”和“multi”,分别代表模块中未列出的运营商(显示为白色)和多于一个运营商(显示为黑色)。

线路表模块

此后,便是为各个区域添加具体的线路(line)资料。请在该子模块下再新建一个二级子模块,并命名为“Module:CNBUS/<城市代码>/<区域代码>”,其基本框架如下:

local p = {
-- 常规线路
['1'] = { name = "线路名", mark = "线路名标注", fare = "票价", operators = "运营商", vehicles = { "配车1", "..." }, note = "备注", image = "[[File:示例.jpg|128px]]",
	{ { "左起讫点", time = "发车时间" }, { "方向箭头", mark = "方向标注" }, { "右起讫点", time = "发车时间" } }, --[[区间1]]
	{ { "左起讫点", time = "发车时间" }, { "方向箭头", mark = "方向标注" }, { "右起讫点", time = "发车时间" } }, --[[区间2]]
	{ --[[…]] }, --[[任意数量区间]] },
-- BRT线路
['B1'] = { name = "线路名", mark = "线路名标注", fare = "票价" --[[BRT样式下不可见]], operator = "运营商", note = "备注", image = "[[File:图片.jpg|x128px]]", brt = { { "驶入BRT通道车站", "经停BRT车站数", "驶出BRT通道车站" --[[右向]] }, { "驶出BRT通道车站", "经停BRT车站数", "驶入BRT通道车站" --[[左向车站定义位置相反]] } }
	{ { "左起讫点", time = "发车时间" }, { "→" --[[完整列表BRT样式下由brt字段自动确定;单向线路需要为其他情况填写]], mark = "方向标注" }, { "右起讫点", time = "发车时间" } }, },

-- 停办线路
['114'] = { name = "线路名", mark = "线路名标注", status = { -1, date = "日期" } },
-- 暂时停运线路
['514'] = { name = "线路名", mark = "线路名标注", status = { 0, date = "日期" } },
}

-- 导入其他模块的线路资料。请注意,引用的线路代码不能是重定向
p._external = {
	['Module:CNBUS/XX/data'] = {
    	['1'] = '1',
        ['114'] = { '514', override = { name = '114' } }, -- 支持覆写部分属性
    },
	['Module:CNBUS/YY/data'] = {
    	['2'] = '2',
    },
}

-- 定义线路编号重定向
p._map = {
	['01'] = '1',
	['BRT1'] = 'B1',
}

return p
  • 线路代码['line']区分简体/繁体和英文大写/小写的,因此在使用时不可混用,建议统一同城市下各子系统的简繁和大小写规则。其他参数内容不受限制。
  • 如需在名词中使用连字符,请使用“-”而非“-”或“-”等。为使用方便,连字暨减号-U+002D)在线路名称、时间、票价和备注中,与数字字母相邻时将被替换为半宽连接号U+2013),其余情形下将被替换为全宽连接号U+2014)。
  • name必填,<区间>[1][1]<区间>[2][1]<区间>[3][1]fareoperatornote参数建议填写(<区间>相关补全规则见#区间子表)。
  • operatorsvehicles既可以是字符串,也可以是数组(显示时分行)。数组表示的operators会直接被视为多运营商。
  • 若需要显示BRT信息,则右向<BRT>[1][1]<BRT>[1][2]<BRT>[1][3]和左向<BRT>[2][1]<BRT>[2][2]<BRT>[2][3]参数需要至少填入一组。右向为空时需要显式置nil方能填写左向数据;若实际存在但缺少相关资料,则应将对应方向置为空表{ }
  • 各参数值可以加入内部链接及换行符<br />,但来源引用<ref>和各类模板是无法使用的。

区间子表

为使用简便,一部分置空的值会使用邻近的值进行取代。补全的顺序遵循区间定义的顺序,区间内部则依次为左、右、方向。首个区间(route)和后继区间的回落规则有所不同。

对于每条线路的首个区间

  • 左起讫点保持原样(as-is),除表不存在(nil或未定义)时转化为空表;
  • 右起讫点若无效(表不存在;表首位的字符串不存在或长度为0),复制左起讫点的名称,附属属性保持原样;
  • 方向若无效(表不存在;表首位的字符串不存在或长度为0),左右起讫点(回落补全后)若相等则视作逆时针循环线,否则视为往返线,附属属性保持原样。
{ },
-- 等价于
{ { }, { "↺" }, { } },

{ { "火车站" }, { mark = "直" }, { "机场" } },
-- 等价于
{ { "火车站" }, { "⇆", mark = "直" }, { "机场" } },

{ { "火车站", time = "10:00" }, nil, { time = "20:00" } },
-- 等价于
{ { "火车站", time = "10:00" }, { "↺" }, { "火车站", time = "20:00" } },

对于后继区间

  • 左右起讫点若表不存在,则复制上一区间对应起讫点的所有属性;若表首位的字符串(名称)不存在或长度为0,则只复制上一区间对应起讫点的名称,附属属性保持原样;
  • 方向的行为与首个区间相同。
-- 假定补全后的上一区间
{ { "火车站", time = "10:00" }, { "⇆", mark = "快" }, { "机场", time = "20:00" } },

{ },
-- 等价于
{ { "火车站", time = "10:00" }, { "⇆" }, { "机场", time = "20:00" } },

{ nil, { mark = "直" }, { "" } },
-- 等价于
{ { "火车站", time = "10:00" }, { "⇆", mark = "直" }, { "机场" } },

用例

常见错误

在条目中使用引用了本模块的模板后,可能会提示以下错误:

错误提示 错误原因 解决方法
错误 Module:CNBUS不存在“XXX”的公交系统数据 Module:CNBUS/XXX不存在 检查模板中 city=<城市代码> 的<城市代码>是否填写错误
错误 “city”参数为空,请输入城市代码 模板未填入 city 的值 填写模板中 city=<城市代码> 的<城市代码>
错误 Module:CNBUS/XXX中未包含“yyy”的资料模块 Module:CNBUS/XXX/yyy不存在 检查模板中 loc=<子系统代码> 的<子系统代码>是否填写错误
错误 资料模块Module:CNBUS/XXX/yyy出现错误,请前往检查 Module:CNBUS/XXX/yyy出现错误 大多为资料模块导入其他模块时输入错误(本说明文档示例中的“导入其他模块的线路资料”部分),请仔细检查资料模块中该部分代码是否有误。如无法定位错误,可借助编辑框下方的“调试控制台”寻找出错行数。不知道怎么用?最简单的方法是输入 print(p) 后回车,如提示“Lua错误”,即可找到出错的变量及其位置。
无输入 请输入线路编号 使用模板时填入线路编号,或去除多余的“|”
1234 本线已于0202年1月1日停办,请移除 使用模板时删除该线路
5678 本线自0202年1月1日起暂停服务 使用模板时视情况删除或保留该线路
9012 本线并非BRT线路 线路缺少 brt_b 参数 如该线路并非BRT线路,使用模板时请勿选择 style=BRT 样式;如该线路的确为BRT线路,请补充完整该线路在BRT通道的行驶信息,否则使用模板时请勿选择 style=BRT 样式。

备注

本模块的相关功能以先前广州巴士路线列表的样式排版设计。模块前身为{{廣州巴士路線}};为优化页面加载速度和方便在不同系统间调用资料,模板于2019年中进行模块化改版(Module:GZBUS);为方便各模块的维护管理,2020年5月再将原先各模块合并于此。

如您在使用本模块时遇到问题或有任何建议,欢迎在模块讨论页中提出。

local p = {}

local _err_category = '[[Category:含有CNBUS错误的页面]]'

---@overload fun(frame: frame, options: table?): { [any]: string }
local getArgs = require('Module:Arguments').getArgs

---@overload fun(s: string): boolean?
---@overload fun(s: string?, default: boolean): boolean
local _yesno = require('Module:yesno')
local yesno = function(val, default)
	if default then
		-- 适应解析参数需要,覆写nil行为
		return val == nil or _yesno(val, true)
	else
		return _yesno(val, false)
	end
end

local tableTools = require('Module:TableTools')
---@overload fun(orig: table, noMetatable: boolean?, already_seen: table?): table
local deepCopy = tableTools.deepCopy
---@overload fun(array: table, sep: string?, i: integer?, j: integer?)
local sparseConcat = tableTools.sparseConcat

---@param var any
---@return boolean
local function _isEmpty(var)
	return not var or var == ''
end

---将空字符串转换为`nil`
---@param var string?
---@return string?
local function _nilEmpty(var)
	if var == '' then return nil
	else return var
	end
end

--#region 线路定义

---@alias terminus { [1]: string?, time: string? }
---@alias direction { [1]: string?, mark: string? }
---@alias route { [1]: terminus?, [2]: direction?, [3]: terminus? }
---@alias pTerminus { [1]: string?, time: string?, rowspan: integer? }
---@alias pRoute { [1]: pTerminus, [2]: direction, [3]: pTerminus }

---@alias brt { [1]: string, [2]: string, [3]: string }

---@class line: { [number]: route }
---@field name string
---@field mark string?
---@field fare string
---@field operators string|string[]
---@field image string?
---@field vehicles (string|string[])?
---@field brt { [1]: brt?, [2]: brt? }?
---@field note string?
---@field status { [1]: integer, date: string }?
local L = {}

---获取解析区间表
---@param nameOnly boolean 行合并时只判定站名(而不包括时间)
---@return pRoute[]
function L:getParsedRoutes(nameOnly)
	---@type pRoute[]
	local routes = {}
	---@type route
	local last_route = { {}, nil, {} } -- 引用上一区间
	for r, route in ipairs(self) do
		---@type terminus
		local left, right

		-- 当非首行终点站表整体置空时,克隆整个表
		if not route[1] then
			left = deepCopy(last_route[1], true)
			-- 否则,只复制站名
		else
			left = deepCopy(route[1], true)
			left[1] = _nilEmpty(left[1]) or last_route[1][1]
		end

		-- 同上,但首行右终点站名将回落左终点
		if not route[3] then
			if r == 1 then
				right = { left[1] }
			else
				right = deepCopy(last_route[3], true)
			end
		else
			right = deepCopy(route[3], true)
			if r == 1 then
				right[1] = _nilEmpty(right[1]) or left[1]
			else
				right[1] = _nilEmpty(right[1]) or last_route[3][1]
			end
		end

		-- 隐式方向
		local direction = deepCopy(route[2] or {}, true)
		direction[1] = _nilEmpty(direction[1]) or ((left[1] == right[1]) and '↺' or '⇆')

		last_route = { left, direction, right }
		table.insert(routes, last_route)
	end

	-- 行合并判定
	if #routes > 1 then
		for r = #routes, 2, -1 do
			if _nilEmpty(routes[r][1][1]) == _nilEmpty(routes[r - 1][1][1]) then
				if nameOnly or _nilEmpty(routes[r][1].time) == _nilEmpty(routes[r - 1][1].time) then
					routes[r - 1][1].rowspan = (routes[r][1].rowspan or 1) + 1
					routes[r][1].rowspan = 0
				elseif _nilEmpty(routes[r][1].time) then
					routes[r][1][1] = nil -- 仅有站名一致时(且显示时间时)置空站名
				end
			end
			if _nilEmpty(routes[r][3][1]) == _nilEmpty(routes[r - 1][3][1]) then
				if nameOnly or _nilEmpty(routes[r][3].time) == _nilEmpty(routes[r - 1][3].time) then
					routes[r - 1][3].rowspan = (routes[r][3].rowspan or 1) + 1
					routes[r][3].rowspan = 0
				elseif _nilEmpty(routes[r][3].time) then
					routes[r][3][1] = nil
				end
			end
		end
	end

	return routes
end

--#endregion

--#region 区域定义

---@alias mArea { name: string, page: string, source: string, aliases: string[] }

---@class area
---@field name string
---@field page string
---@field source string
---@field aliases string
---@field lines { [string]: line }
local A = {}

---获取线路 w/ err
---@param l string
---@param inline boolean?
---@return line?
---@return string?
function A:getLine(l, inline)
	---@type line?
	local line = self.lines[l] or (self.lines._map and self.lines[self.lines._map[l]])

	local err = nil
	local page = self.page or (self.name .. '巴士路线列表')
	if _isEmpty(l) then
		err = string.format('未输入线路[[%s|编号]]', page)
	elseif not line then
		err = string.format('[[%s]]中无此[[%s|%s]]线路', self.source, page, self.name)
	else
		local name = line.name

		---@diagnostic disable-next-line: undefined-field
		if line.code then -- 旧版线路
			err = '数据格式不受支持'
			line = nil

		elseif line.status then
			if line.status[1] == -1 then
				if _isEmpty(line.status.date) then
					err = '已停办'
				else
					err = string.format('已于%s停办', line.status.date)
				end
			elseif line.status[1] == 0 then
				if _isEmpty(line.status.date) then
					err = '暂停服务'
				else
					err = string.format('自%s起暂停服务', line.status.date)
				end
			end
		end

		if inline then
			err = name .. err
		end
	end

	return line, err
end

---获取匹配指定模式的线路
---@param pattern string
---@return string[]
function A:getLines(pattern)
	local codes = {}
	for l, line in pairs(self.lines) do
		if l ~= '_map' and mw.ustring.match(l, pattern) and not (line.status and line.status[1] == -1) then
			table.insert(codes, l)
		end
	end
	table.sort(codes)
	return codes
end

--#endregion

--#region 城市定义

---@alias operator { color: string, aliases: string[] }

---@class city
---@field areas { [string]: area }
---@field area_map { [string]: string }
---@field lines { [string]: { [string]: line } }
---@field operators { [string]: operator }
---@field operator_map { [string]: string }
local data = {}

---@param a string
---@return area
function data:getArea(a)
	return self.areas[a] or self.areas[self.area_map[a]] or self.areas['default']
end

---@param o string
---@return operator
function data:getOperator(o)
	return self.operators[o] or self.operators[self.operator_map[o]]
end

--#endregion

--#region 数据模块

---导入城市数据
---@param c string
local function _loadCityData(c)
	if not data.areas then
		if _isEmpty(c) then
			error(string.format('“city”参数为空,请输入城市代码'))
		end

		local success, ro_data = pcall(mw.loadData, 'Module:CNBUS/' .. c)
		if not success then
			error(string.format('[[Module:CNBUS]]不存在“%s”的公交系统数据', c))
		end

		-- 每个area下需要读写权限挂载线路表
		data.areas = {}
		data.area_map = {}
		for a, ro_area in pairs(ro_data.areas) do
			data.areas[a] = setmetatable({}, { __index = ro_area })
			if ro_area.aliases then
				for _, alias in ipairs(ro_area.aliases) do
					data.area_map[alias] = a
				end
			end
		end

		-- operators只需要只读访问
		data.operator_map = {}
		data.operators = setmetatable({}, { __index = ro_data.operators })
		for o, ro_operator in pairs(ro_data.operators) do
			if ro_operator.aliases then
				for _, alias in ipairs(ro_operator.aliases) do
					data.operator_map[alias] = o
				end
			end
		end
	end
end

---导入区域线路数据
---@param c string
---@param a string
local function _loadAreaData(c, a)
	_loadCityData(c)

	if _isEmpty(a) then
		error(string.format('“area”参数为空,请输入区域代码'))
	end

	local area = data:getArea(a)

	if not area then
		error(string.format('[[Module:CNBUS/%s]]中未包含“%s”的资料模块', c, a))
	end

	if not area.lines then
		local success, ro_data = pcall(mw.loadData, area.source)
		if not success then
			error(string.format('数据模块[[%s]]出现错误', area.source))
		end

		area.lines = {}
		for l, line in pairs(ro_data) do
			area.lines[l] = line
		end

		if ro_data._external then
			for source, map in pairs(ro_data._external) do
				local source_data
				success, source_data = pcall(mw.loadData, source)
				if not success then
					error(string.format('模块[[%s]]引用的数据模块[[%s]]出现错误', area.source, source))
				end

				for l, hint in pairs(map) do
					if type(hint) == "table" then
						area.lines[l] = deepCopy(source_data[hint[1]], true)
						if hint.override then
							for _p, prop in pairs(hint.override) do
								area.lines[l][_p] = prop
							end
						end
					else
						area.lines[l] = source_data[hint]
					end
				end
			end
		end
	end
end

--#endregion

--#region 颜色模板

---@param c string
---@param operator (string|string[])?
---@return string
function p._color(c, operator)
	local success, err = pcall(_loadCityData, c)

	if not success then
		return err .. _err_category
	end

	if type(operator) == 'table' then
		return data:getOperator('multi').color
	end

	local info = data:getOperator(operator or 'other')
	if info then
		return info.color
		-- 运营商名超过6字(UTF-8下18字节)视为联营
	elseif (operator and string.len(operator) > 18) or operator == 'multi' then
		return 'black' -- 原索引multi
	else
		return 'white' -- 原索引other
	end
end

---运营商颜色
---@param frame frame
---@return string
function p.color(frame)
	local args = frame.args
	return mw.text.nowiki(p._color(args.city, args.operator or args.company))
end

--#endregion

--#region 列表辅助模板

local enDash = mw.ustring.char(0x2013)
local enDashReplace = '%1' .. enDash .. '%2'
local emDash = mw.ustring.char(0x2014)
local emDashReplace = '%1' .. emDash .. '%2'

---@param s string?
---@return string?
local function _fixDash(s)
	if not s then
		return s
	end
	-- 两端皆为数字字母的将替换为 en dash
	s, _ = mw.ustring.gsub(s, '([a-zA-Z0-9])-([a-zA-Z0-9])', enDashReplace)
	-- 否则替换为 em dash
	s, _ = mw.ustring.gsub(s, '(%w)-(%w)', emDashReplace)
	return s
end

---@param color string
---@param numRows integer?
---@return html?
local function _createBarCell(color, numRows)
	local td = mw.html.create('td')
		:addClass('bar')
		:css('background-color', color)

	if numRows and numRows > 1 then
		td:attr('rowspan', numRows)
	end

	return td:allDone()
end

---@param line line
---@param numRows integer?
---@return html?
local function _createNameCell(line, numRows)
	local td = mw.html.create('td')
		:addClass('name')
		:wikitext(_fixDash(line.name))

	if numRows and numRows > 1 then
		td:attr('rowspan', numRows)
	end

	if _nilEmpty(line.mark) then
		td:tag('small'):wikitext(line.mark):done()
	end

	return td:allDone()
end

---@param route pRoute
---@param isLeft boolean
---@param showTime boolean
---@param numRows integer? Override rowspan
---@return html?
local function _createTerminusCell(route, isLeft, showTime, numRows)
	local terminus = isLeft and route[1] or route[3]
	local n_rows = numRows or terminus.rowspan

	if n_rows == 0 then
		return nil
	end

	local td = mw.html.create('td')
	td:addClass('terminus-' .. (isLeft and 'left' or 'right'))
		:wikitext(terminus[1])

	if n_rows and n_rows > 1 then
		td:attr('rowspan', n_rows)
	end

	if showTime and _nilEmpty(terminus.time) then
		if _nilEmpty(terminus[1]) then
			td:tag('br', { selfClosing = true }):done()
		end
		td:tag('small'):wikitext(_fixDash(terminus.time)):done()
	end

	return td:allDone()
end

---@param route pRoute
---@param direction string? Override
---@param mark string? Override
---@param biRows boolean?
---@return html?
local function _createDirectionCell(route, direction, mark, biRows)
	local td = mw.html.create('td')
		:addClass('direction')

	mark = _nilEmpty(mark) or _nilEmpty(route[2].mark)
	if mark then
		td:tag('small'):wikitext(mark):done():tag('br', { selfClosing = true }):done()
	end

	if biRows then
		td:attr('rowspan', 2)
	end

	return td:wikitext(_nilEmpty(direction) or route[2][1]):allDone()
end

---@param info brt
---@param isLeft boolean
---@param biRows boolean?
---@return html?
local function _createBrtCell(info, isLeft, biRows)
	local station = isLeft and info[1] or info[3]

	local td = mw.html.create('td')
	td:addClass('brt-' .. (isLeft and 'left' or 'right'))
		:wikitext(station)

	if biRows then
		td:attr('rowspan', 2)
	end

	return td:allDone()
end

---@param prop (string|string[])?
---@param numRows integer?
---@param fixDash boolean?
local function _createPropCell(prop, numRows, fixDash)
	local td = mw.html.create('td')

	if type(prop) == 'table' then
		prop = table.concat(deepCopy(prop, true), '<br/>') -- deepCopy for readonly tables
	else
		prop = prop or ''
	end

	if fixDash then
		td:wikitext(_fixDash(prop))
	else
		td:wikitext(prop)
	end

	if numRows and numRows > 1 then
		td:attr('rowspan', numRows)
	end

	return td:allDone()
end

---@param line line
---@param showImage boolean
---@param numRows integer?
---@return html?
local function _createNoteCell(line, showImage, numRows)
	local td = mw.html.create('td')
	local text

	if showImage then
		text = sparseConcat({ _fixDash(_nilEmpty(line.note)), _nilEmpty(line.image) }, '<br/>')
	else
		text = _fixDash(line.note)
	end
	td:addClass('note'):wikitext(text)

	if numRows and numRows > 1 then
		td:attr('rowspan', numRows)
	end

	return td:allDone()
end

--#endregion

--#region 列表模板

---@class listFlags
---@field bar boolean
---@field brt boolean
---@field time boolean
---@field fare boolean
---@field operators boolean
---@field vehicles boolean
---@field image boolean
local list_flags = {
	---@param typ string?
	---@param fTime string?
	---@param fFare string?
	---@param fOperators string?
	---@param fVehicles string?
	---@param fImage string?
	---@return listFlags flags
	parse = function(typ, fTime, fFare, fOperators, fVehicles, fImage)
		typ = mw.ustring.lower(typ or '')
		if mw.ustring.find(typ, 'brt') then
			return {
				bar       = true,
				brt       = true,
				time      = yesno(fTime or '', false),
				fare      = true,
				operators = true,
				vehicles  = false,
				image     = yesno(fImage or '', false),
			}
		else
			return {
				bar       = true,
				brt       = false,
				time      = yesno(fTime or '', false),
				fare      = yesno(fFare or '', true),
				operators = yesno(fOperators or '', true),
				vehicles  = yesno(fVehicles or '', false),
				image     = yesno(fImage or '', false),
			}
		end
	end
}

---获取表格CSS类
---@param f listFlags
---@return string
local function _getListClass(f)
	if f.brt then
		return 'cnbus-brt'
	else
		return 'cnbus-l' .. ((f.fare and 1 or 0) + (f.operators and 1 or 0) + (f.vehicles and 2 or 0))
	end
end

---@param c string
---@param a string
---@param f listFlags
---@return html head
function p._generateHead(c, a, f)
	local success, err = pcall(_loadAreaData, c, a)

	if not success then
		if f.brt then
			return mw.html.create('tr')
				:tag('th'):attr('colspan', 10):wikitext(err .. _err_category)
				:allDone()
		else
			local n_cols = 5 + (f.bar and 1 or 0) + (f.fare and 1 or 0) + (f.operators and 1 or 0) + (f.vehicles and 1 or 0)
			return mw.html.create('tr')
				:tag('th'):attr('colspan', n_cols):wikitext(err .. _err_category)
				:allDone()
		end
	end

	local area = data:getArea(a)
	local header_lines = (f.time and '线路及运营时间') or '线路'
	local header_note = (f.image and '备注及图片') or '备注'
	local link_page = area.page or string.format('%s巴士路线列表', area.name)

	if f.brt then
		return mw.html.create('tr')
			:tag('th'):attr('colspan', 2):wikitext(string.format('[[%s|编号]]', link_page)):done()
			:tag('th'):addClass('unsortable'):attr('colspan', 3):wikitext(header_lines):done()
			:tag('th'):addClass('unsortable'):attr('colspan', 3):wikitext('BRT通道内停站'):done()
			:tag('th'):addClass('operator'):wikitext('运营商'):done()
			:tag('th'):addClass('note'):wikitext(header_note):done()
			:allDone()
	else
		local tr = mw.html.create('tr')

		tr:tag('th'):attr('colspan', 2):wikitext(string.format('[[%s|编号]]', link_page)):done()
			:tag('th'):addClass('unsortable'):attr('colspan', 3):wikitext(header_lines):done()
		if f.fare then
			tr:tag('th'):addClass('fare'):wikitext('收费'):done()
		end
		if f.operators then
			tr:tag('th'):addClass('operator'):wikitext('运营商'):done()
		end
		if f.vehicles then
			tr:tag('th'):addClass('vehicle'):wikitext('运力'):done()
		end
		tr:tag('th'):addClass('note'):wikitext(header_note):done()

		return tr:allDone()
	end
end

---@param line line
---@param f listFlags
---@param msg string
---@param isWarning boolean?
---@return html row
local function _generateErrorRow(line, f, msg, isWarning)
	local n_cols
	if f.brt then
		n_cols = 8
	else
		n_cols = 4 + (f.fare and 1 or 0) + (f.operators and 1 or 0) + (f.vehicles and 1 or 0)
	end

	if not isWarning then
		msg = msg .. _err_category
	end

	local tr = mw.html.create('tr'):addClass('msg')
	if f.bar then
		tr:tag('td'):addClass('bar'):done()
	end
	tr:node(_createNameCell(line))
		:tag('td'):addClass('msg'):attr('colspan', n_cols)
		:wikitext(msg)
		:done()
	return tr
end

---生成单行内容
---@param c string
---@param a string
---@param l string
---@param f listFlags
---@return html row
function p._generateRow(c, a, l, f)
	local success
	local err
	success, err = pcall(_loadAreaData, c, a)

	local output = mw.html.create()

	if not success then
		l = '错误'
	else
		local area = data:getArea(a)

		local line
		line, err = A.getLine(area, l)
		local isWarning = line ~= nil -- 不追踪暂停/撤销线路

		if not err then
			local routes = L.getParsedRoutes(line, not f.time)
			local color = f.bar and p._color(c, line.operators) -- 懒调用运营商颜色接口

			-- BRT线路(广州、中山)
			if f.brt then
				if not line.brt then
					err = '本线并非[[快速公交系统|BRT线路]]'
				else
					local route = routes[1]

					if (line.brt[1] and line.brt[2]) then
						local tr1 = mw.html.create('tr'):addClass('line')
						local tr2 = mw.html.create('tr'):addClass('route')

						tr1:node(_createBarCell(color, 2))
							:node(_createNameCell(line, 2))
							:node(_createTerminusCell(route, true, f.time, 2))
							:node(_createDirectionCell(route, '→'))
							:node(_createTerminusCell(route, false, f.time, 2))
						tr2:node(_createDirectionCell(route, '←'))

						-- BRT通道左
						if line.brt[1][1] == line.brt[2][1] then
							tr1:node(_createBrtCell(line.brt[1], true, true))
						else
							tr1:node(_createBrtCell(line.brt[1], true))
							tr2:node(_createBrtCell(line.brt[2], true))
						end

						-- BRT通道方向
						if _nilEmpty(line.brt[1][2]) == _nilEmpty(line.brt[2][2]) then
							local s = _nilEmpty(line.brt[1][2]) and (line.brt[1][2] .. '站')
							tr1:node(_createDirectionCell(route, '⇄', s, true))
						else
							local s1 = _nilEmpty(line.brt[1][2]) and (line.brt[1][2] .. '站')
							local s2 = _nilEmpty(line.brt[2][2]) and (line.brt[2][2] .. '站')
							tr1:node(_createDirectionCell(route, '→', s1))
							tr2:node(_createDirectionCell(route, '←', s2))
						end

						-- BRT通道右
						if line.brt[1][3] == line.brt[2][3] then
							tr1:node(_createBrtCell(line.brt[1], false, true))
						else
							tr1:node(_createBrtCell(line.brt[1], false))
							tr2:node(_createBrtCell(line.brt[2], false))
						end

						tr1:node(_createPropCell(line.operators, 2))
							:node(_createNoteCell(line, f.time, 2))

						output:node(tr1):node(tr2)
					elseif line.brt[1] or line.brt[2] then
						local info = line.brt[1] or line.brt[2] --[[@as brt]]

						output
							:tag('tr')
							:addClass('line')
							:node(_createBarCell(color, 1))
							:node(_createNameCell(line, 1))
							:node(_createTerminusCell(route, true, f.time, 1))
							:node(_createDirectionCell(route))
							:node(_createTerminusCell(route, false, f.time, 1))
							:node(_createBrtCell(info, true))
							:node(_createDirectionCell(route, nil, _nilEmpty(info[2]) and (info[2] .. '站')))
							:node(_createBrtCell(info, false))
							:node(_createPropCell(line.operators))
							:node(_createNoteCell(line, f.image))
							:done()
					else
						err = '线路[[快速公交系统|BRT]]数据无效'
					end
				end
				-- 常规线路
			else
				local tr

				for r, route in ipairs(routes) do
					tr = mw.html.create('tr')

					if r == 1 then
						tr:addClass('line')
						if f.bar then
							tr:node(_createBarCell(color, #routes))
						end
						tr:node(_createNameCell(line, #routes))
					else
						tr:addClass('route')
					end

					tr:node(_createTerminusCell(route, true, f.time))
						:node(_createDirectionCell(route))
						:node(_createTerminusCell(route, false, f.time))

					if r == 1 then
						if f.fare then
							tr:node(_createPropCell(line.fare, #routes, true))
						end

						if f.operators then
							tr:node(_createPropCell(line.operators, #routes))
						end

						if f.vehicles then
							tr:node(_createPropCell(line.vehicles, #routes))
						end

						tr:node(_createNoteCell(line, f.image, #routes))
					end

					output:node(tr:allDone())
				end
			end
		end

		if err then
			return _generateErrorRow(line or { name = l }, f, err, isWarning)
		end
	end

	return output:allDone()
end

---生成多行内容支持
---@param c string
---@param a string
---@param l string
---@param f listFlags
---@return html rows
function p._generateRows(c, a, l, f)
	local success, err = pcall(_loadAreaData, c, a)

	local output = mw.html.create()

	if success then
		local area = data:getArea(a)

		for _, _l in ipairs(A.getLines(area, l)) do
			if not mw.ustring.match(_l, '^^') then
				output:node(p._generateRow(c, a, _l, f))
			end
		end
	else
		---@diagnostic disable-next-line: param-type-mismatch
		return _generateErrorRow({ name = '错误' }, f, err)
	end

	return output:allDone()
end

---列表模板
---@param frame frame
---@return string
function p.list(frame)
	local args = getArgs(frame)

	local flags = list_flags.parse(args.type, args.time, args.fare, args.operators, args.vehicles, args.image)
	local class = _getListClass(flags)

	local outputs = {}

	if yesno(args.start, true) then
		-- 表格开始
		table.insert(outputs, '{| class="wikitable sortable cnbus-normal ' .. class .. '"\n')

		-- 标题
		local caption = _nilEmpty(args.header) or _nilEmpty(args.info) or _nilEmpty(args.station)
		if caption then
			table.insert(outputs, '|+ ' .. caption .. '\n')
		end

		-- 表头
		table.insert(outputs,
			tostring(p._generateHead(args.city, args.area, flags)))
	end

	if mw.ustring.match(args[1] or '', '^^') then
		table.insert(outputs,
			tostring(p._generateRows(args.city, args.area, args[1], flags)))
	else
		for _, l in ipairs(args) do
			table.insert(outputs,
				tostring(p._generateRow(args.city, args.area, l, flags)))
		end
	end

	if yesno(args['end'], true) then table.insert(outputs, '</table>') end

	return table.concat(outputs)
end

---折叠列表模板
---@param frame frame
---@return string
function p.collapsibleList(frame)
	local args = getArgs(frame)

	local outputs = {}

	if yesno(args.start, true) then
		table.insert(outputs,
			[[
{| class="collapsible collapsed cnbus-collapsible"
! colspan=5 class="title" | ]] ..
			((_nilEmpty(args.header) or _nilEmpty(args.info) or _nilEmpty(args.station) or '行经巴士路线一览') .. [[

|-
! 编号 !! colspan=3 | 路线 !! 备注
]]))
	end

	for _, l in ipairs(args) do
		table.insert(outputs, tostring(p._generateRow(args.city, args.area, l, {})))
	end

	if yesno(args['end'], true) then table.insert(outputs, '</table>') end

	return table.concat(outputs)
end

--#endregion

--#region 线路名称模板(惠州)

---@param c string
---@param a string
---@param l string
---@return string
function p._lineName(c, a, l)
	local success
	local err
	success, err = pcall(_loadAreaData, c, a)

	if not success then
		l = '错误'
	end

	if not err then
		local area = data:getArea(a)
		local line
		line, err = A.getLine(area, l)

		if not err then
			---@diagnostic disable-next-line: need-check-nil
			return line.name or l
		end
	end

	return string.format('(%s)', err .. _err_category)
end

---线路名称模板
---@param frame frame
---@return string
function p.lineName(frame)
	local args = frame.args
	return p._lineName(
		args.city,
		args.area or args.loc,
		args[1] or args.code)
end

--#endregion

return p