模組解[]
-- vim: set sw=4 ts=4 noet ai sm :
---- 呢個係用嚟喺維基架構之內處理多倫多公車局相關文章嘅大量重覆或者非常難排版嘅資訊
---- 2023年粵維原創

require ('strict');
local p = {};

local styles = 'Template:多倫多公車局/styles.css';

if true then
	local z = require('模組:書名');
	p.cvs = z.cvs;
	p.cjk_p = z.cjk_p;
	p.array_p = z.array_p;
end

--- 基本資料

local lines = {
	['1'] = {
		aliases = {'YUS'};
		link = '多倫多地下鐵1號綫';
		name = '一號綫';
		icon = 'File:TTC - Line 1 - Yonge-University-Spadina line.svg';
		livery = 'f8c300';
	};
	['2'] = {
		aliases = {'BD'};
		link = '多倫多地下鐵2號綫';
		name = '二號綫';
		icon = 'File:TTC - Line 2 - Bloor-Danforth line.svg';
		livery = '00923f';
	};
	['3a'] = {
		aliases = {'SRT'};
		link = '士嘉堡 RT';
		name = '士嘉堡 RT';
		icon = 'File:TTC - Line 3 - Scarborough RT line.svg';
		livery = '0082c9';
	};
	['3b'] = {
		aliases = {'Ontario'};
		link = '安大略綫';
		name = '安大略綫';
		icon = 'File:TTC - Line 3 - Scarborough RT line.svg';
		livery = '0082c9';
	};
	['4'] = {
		aliases = {'Sheppard'};
		link = '多倫多地下鐵4號綫';
		name = '雪柏綫';
		icon = 'File:TTC - Line 4 - Sheppard line.svg';
		livery = 'a21a68';
	};
	['5'] = {
		aliases = {'ECLRT'};
		link = '艾靈頓跨城輕鐵綫';
		name = '艾靈頓跨城輕鐵綫';
		icon = 'File:TTC - Line 5.svg';
		livery = 'f87005';	-- 假定圖標嘅色冇錯
	};
	['6'] = {
		aliases = {'FWLRT'};
		link = '芬治西輕鐵綫';
		name = '芬治西輕鐵綫';
		icon = 'File:TTC - Line 6.svg';
		livery = '888888';	-- 假定圖標嘅色冇錯
	};
	['7'] = {
		aliases = {'EELRT'};
		link = '艾靈頓東輕鐵綫';
		name = '艾靈頓東輕鐵綫';
		icon = 'File:TTC - Line 7.svg';
	};
	['WELRT'] = {
		link = '湖濱東輕鐵綫';
		name = '湖濱東輕鐵綫';
	};
	['501'] = {
		-- 301 is roughly the same route but not identical (different in the middle)
		aliases = {'301'};
		link = '多倫多街車501號綫';
		name = '501/301號綫';
		icon = 'File:TTC - 501.svg';
	};
	['503'] = {
		link = '多倫多街車503號綫';
		name = '503號綫';
		icon = 'File:TTC - 503.svg';
	};
	['504'] = {
		-- 304 is roughly the same route but not identical (no Dundas West; quite different near Broadview)
		aliases = {'304'};
		link = '多倫多街車504號綫';
		name = '504/304號綫';
		icon = 'File:TTC - 504.svg';
	};
	['505'] = {
		link = '多倫多街車505號綫';
		name = '505號綫';
		icon = 'File:TTC - 505.svg';
	};
	['506'] = {
		-- 306 is very similiar but not identical; east of Sorauren is completely different
		aliases = {'306'};
		link = '多倫多街車506號綫';
		name = '506/306號綫';
		icon = 'File:TTC - 506.svg';
	};
	['507'] = {
		link = '多倫多街車507號綫';
		name = '507號綫';
		icon = 'File:TTC - 507.svg';
	};
	['508'] = {
		link = '多倫多街車508號綫';
		name = '508號綫';
		icon = 'File:TTC - 508.svg';
	};
	['509'] = {
		link = '多倫多街車509號綫';
		name = '509號綫';
		icon = 'File:TTC - 509.svg';
	};
	['510'] = {
		-- 310 is similiar but actually quite different in the middle
		aliases = {'310'};
		link = '多倫多街車510號綫';
		name = '510/310號綫';
		icon = 'File:TTC - 510.svg';
	};
	['511'] = {
		link = '多倫多街車511號綫';
		name = '511號綫';
		icon = 'File:TTC - 511.svg';
	};
	['512'] = {
		link = '多倫多街車512號綫';
		name = '512號綫';
		icon = 'File:TTC - 512.svg';
	};
}
if true then	-- 重組 lines 定義,將 aliases 全部加入索引
	local index = {};
	local annotation = '';
	local prefix = '';
	for k, v in pairs(lines) do
		index[k] = v;
		if v.aliases then
			annotation = annotation .. prefix;
			local prefix = annotation .. k .. ' 定義咗';
			for _, alias in pairs(v.aliases) do
				if lines[alias] or index[alias] then
					error('lines 定義出錯,' .. k .. ' 嘗試定義 alias = ' .. alias .. ',但係已經有呢個名(' .. annotation .. ')');
				end
				index[alias] = v;
				annotation = prefix .. alias;
				prefix = '、';
			end
		end
		prefix = ',';
	end
	lines = index;
end

--- Auxiliaries

local function interpret_boolean( s )
	local it;
	if type(s) == 'boolean' then
		it = s;
	else
		it = s:match('^%s*true%s*$')
			or s:match('^%s*yes%s*$')
			or s:match('^%s*0*1%s*$');
	end
	return it;
end

local function new_Entrance (s)
	-- not doing anything for now. Just check the keys to make sure no unknown keys (ie typos) are present
	local self = {};
	if not p.array_p(s) then
		error('new_Entrance: 實際參數唔係陣列(數值:' .. p.cvs(s) .. ')');
	end
	for i, a in pairs(s) do
		local node = {};
		for k, v in pairs(a) do
			if k == 'website' then
				node.website = v;
			elseif k == 'active' then
				node.active_p = interpret_boolean(v);
			elseif k == 'directional' or k == 'directional sign' then
				node.directional = v;
			elseif k == 'identification' or k == 'identification sign' then
				node.identification = v;
			elseif k == 'outside' then
				node.outside = v;
			elseif k == 'inside' then
				node.inside = v;
			elseif k == 'up' or k == 'dir' then
				node.dir = v;
			elseif k == 'at' or k == 'building' then
				node.building = v;
			elseif k == 'intersection' then
				node.intersection = v;
			elseif k == 'corner' then
				node.corner = v;
			elseif k == 'side' then
				node.side = v;
			elseif k == 'link' then
				node.link = v;
			elseif k == 'note' then
				node.note = v;
			elseif k == 'def' then
				node.def = new_Entrance(v);
			elseif k:sub(1, 2) ~= 'x-' then
				error('new_Entrance: 實際參數遇到不明欄位「' .. k .. '」(數值:' .. p.cvs(v) .. ')');
			end
		end
		table.insert(self, node);
	end
	return self;
end

local function icon_size( icon )
	local it;
	if icon:match('^File:TTC +- +Line ') then
		it = 16;
	elseif icon:match('^File:TTC +- +%d+%.') then
		it = 28;
	end
	return it;
end

local function remove_tags( s )
	local it;
	if s then
		it = s:gsub('<[^<>]*>', ''):gsub("'''", ''):gsub("''", '');
	end
	return it;
end

local function handle_breaks( s )
	local it = s;
	local sep = mw.ustring.char(0x200b); -- separator to match
	local sep2 = sep;					-- separator to add back
	if mw.ustring.match(s, sep) then
		local a = mw.text.split(s, sep, true);
		local n = #a;
		if a[n] == '' then
			n = n - 1;
		end
		it = '';
		for i = 1, n, 1 do
			if i > 1 then
				it = it .. sep2;
			end
			it = it .. '<span style="white-space:nowrap">' .. a[i] .. '</span>';
		end
	end
	return it;
end

local function join_segments( a, b )
	local it;
	if #a == 0 or #b == 0 or (#a > 0 and #b > 0 and p.cjk_p(mw.ustring.sub(a, -1)) and p.cjk_p(mw.ustring.sub(b, 1, 1))) then
		it = a .. b;
	else
		it = a .. ' ' .. b;
	end
	return it;
end

local function entrance_canonicalized( s )
	-- try to reduce labelling "website version" of entrance names when the name is practically the same as what's on the sign
	s = mw.ustring.gsub(mw.ustring.lower(s), '%s+street', ' st.');
	s = mw.ustring.gsub(s, '%s+entrance', '');
	s = mw.ustring.gsub(s, '%s%(([^%(%)]+)%)$', ' / %1');
	s = mw.ustring.gsub(s, '%s+$', '');
	return s;
end

local function show_icon( args )
	local it;
	if not args or not args.line then
		error('冇指定路綫(實際參數:' .. p.cvs(args) .. ')');
	elseif not lines[args.line] then
		error('指定咗路綫' .. p.cvs(args.line) .. ',但係模組冇呢條綫嘅資料');
	elseif not lines[args.line].icon then
		error('指定咗路綫' .. p.cvs(args.line) .. ',但係手頭上嘅資料入面冇圖標');
	end
	local size = icon_size(lines[args.line].icon);
	it = '<span class=ttc-icon>[[' .. lines[args.line].icon;
	if size then
		it = it .. '|' .. size .. 'px';
	end
	it = it .. ']]</span>';
	return it;
end

local function show_name( args )
	local it;
	if not args or not args.line then
		error('冇指定路綫(實際參數:' .. p.cvs(args) .. ')');
	elseif not lines[args.line] then
		error('指定咗路綫' .. p.cvs(args.line) .. ',但係模組冇呢條綫嘅資料');
	elseif not lines[args.line].name then
		error('指定咗路綫' .. p.cvs(args.line) .. ',但係手頭上嘅資料入面冇名');
	end
	local name = lines[args.line].name;
	local link = lines[args.line].link;
	if link and not args.no_link_p then
		it = '[[' .. link;
		if link ~= name then
			it = it .. '|' .. name;
		end
		it = it .. ']]';
	else
		it = name;
	end
	return it;
end

local function show_icon_and_name( args )
	local it;
	if not args or not args.line then
		error('冇指定路綫(實際參數:' .. p.cvs(args) .. ')');
	elseif not lines[args.line] then
		error('指定咗路綫' .. p.cvs(args.line) .. ',但係模組冇呢條綫嘅資料');
	elseif not lines[args.line].name then
		error('指定咗路綫' .. p.cvs(args.line) .. ',但係手頭上嘅資料入面冇名');
	end
	local name = lines[args.line].name;
	local link = lines[args.line].link;
	if lines[args.line].icon then
		it = show_icon(args) .. ' ';
	else
		it = '';
	end
	it = it .. show_name(args);
	return it;
end

local function show_livery( args )
	local it;
	if not args or not args.line then
		error('冇指定路綫(實際參數:' .. p.cvs(args) .. ')');
	elseif not lines[args.line] then
		error('指定咗路綫' .. p.cvs(args.line) .. ',但係模組冇呢條綫嘅資料');
	end
	local livery = lines[args.line].livery;
	if not livery then
		it = '';
	else
		it = '#' .. livery;
	end
	return it;
end

-- 地下鐵出入口註釋
-- display a disclaimer about Toronto's bad EGD

local function make_disclaimer( subhead, message )
	return '<div class=ttc-disclaimer>'
		.. "<span class=disclaimer-icon>[[File:Stop-hand-caution.png|32px|alt='''" .. subhead .. "''':]]</span>"
		.. '<span class=disclaimer-message>' .. message .. '</span></div>';
end

local function show_disclaimer( args )
	local it = '';

	local city = args.city;
	if not city then
		city = '多倫多';
	end

	local network = args.network;
	if not network then
		network = '地鐵網';
	end

	local source = args.source;
	if not source then
		source = '官方網站';
	end

	if source then
		it = it .. join_segments('下列出入口次序係跟', source) .. ',未必合理。';
	end

	it = it .. join_segments(join_segments(city, network), "嘅出入口冇編號,好多時亦唔寫地標(淨寫街名");
	if mw.ustring.match(network, '地') then	-- FIXME
		it = it .. "、東南西北、同(如果適用)[[多倫多街車|街車]]嘅行車方向)。";
	else
		it = it .. "同東南西北。";
	end
	it = it .. "同一個站入面,";
	if mw.ustring.match(source, '官') then	-- FIXME
		it = it .. "一個出口可能唔止一個名,名好多時唔係網站寫嘅版本,甚至";
	end
	it = it .. "唔同出口可能會撞名。";

	it = make_disclaimer('注意', it);
	return it;
end

-- 車站樓層
-- produce a station structure diagram, mimicking the manual formatting expected on yuewiki

local function draw_station_description( s )
	if not p.array_p(s) then
		error('車站樓層參數唔係陣列(' .. p.cvs(s) .. ')');
	elseif #s == 0 then
		error('車站樓層參數係空陣列');
	end
	local it = '{|table border=0 cellspacing=0 cellpadding=3 class="ttc-station-structure"\n';
	local blank_divider = '|style="padding:1px;" colspan=3|\n';
	local solid_divider = '|style="border-bottom:solid 1px gray;padding:1px;" colspan=3|\n';
	local dashed_divider = '|style="border-bottom:dashed 1px gray;padding:1px;" colspan=3|\n';
	it = it .. '|-\n';
	for i = 1, #s, 1 do
		local curr = s[i];
		local prev, next;
		if i > 1 then
			prev = s[i - 1];
		end
		if i < #s then
			next = s[i + 1];
		end
		if type(curr) ~= 'table' then
			error('車站樓層參數嘅第' .. i .. '個元素唔係物件');
		elseif not curr.name then
			error('車站樓層參數嘅第' .. i .. '個元素冇 name');
		elseif not curr.def then
			error('車站樓層參數嘅第' .. i .. '個元素冇 def');
		elseif not p.array_p(curr.def) then
			error('車站樓層參數嘅第' .. i .. '個元素嘅 def 唔係陣列');
		elseif not #curr.def then
			error('車站樓層參數嘅第' .. i .. '個元素嘅 def 係空陣列');
		end
		if prev and not curr.level then
			it = it .. dashed_divider;
		else
			it = it .. solid_divider;
		end
		it = it .. '|-\n';
		it = it .. blank_divider;
		it = it .. '|-\n';
		if p.array_p(curr.def[1]) then	-- 唔係月台
			local n = 0;	-- need to first figure out total number of rows needed
			for j = 1, #curr.def, 1 do
				local det = curr.def[j][2];
				n = n + #det;
			end
			for j = 1, #curr.def, 1 do
				if j == 1 then
					it = it .. '|width=50 rowspan=' .. n .. ' valign=top|';
					it = it .. "'''";
					if curr.level then
						it = it .. curr.level .. '<br />';
					end
					it = it .. handle_breaks(curr.name) .. "'''\n";
				end
				local det = curr.def[j][2];
				if j < #curr.def then
					it = it .. '|style="border-bottom:solid 1px gray;" rowspan=' .. #det .. ' valign=top|' .. curr.def[j][1] .. '\n';
				else
					it = it .. '|rowspan=' .. #det .. ' valign=top|' .. curr.def[j][1] .. '\n';
				end
				for k = 1, #det, 1 do
					if k == #det and j < #curr.def then
						it = it .. '|style="border-bottom:solid 1px gray;" valign=top|' .. det[k] .. '\n';
					else
						it = it .. '|valign=top|' .. det[k] .. '\n';
					end
					it = it .. '|-\n';
				end
			end
		else							-- 月台
			for j = 1, #curr.def, 1 do
				if j == 1 then
					it = it .. '|width=50 rowspan=' .. #curr.def .. ' valign=top|';
					it = it .. "'''";
					if curr.level then
						it = it .. curr.level .. '<br />';
					end
					it = it .. handle_breaks(curr.name) .. "'''\n";
				end
				local tb;
				if curr.def[j].type:match('^platform ') then
					tb = '';
					if j == 1 or not curr.def[j - 1].type:match('^platform ') then
						tb = tb .. 'border-top:solid 2px black;';
					end
					if j == #curr.def or not curr.def[j + 1].type:match('^platform ') then
						tb = tb .. 'border-bottom:solid 2px black;';
					end
					if curr.def[j].wall then
						if j > 1 and curr.def[j - 1].type:match('^platform ') then
							tb = tb .. 'border-top:dashed 1px gray;';
						end
						if j < #curr.def and curr.def[j + 1].type:match('^platform ') then
							tb = tb .. 'border-bottom:dashed 1px gray;';
						end
						tb = tb .. 'background-color:#eee;';
					end
				end
				if curr.def[j].type == 'track' then
					if j < #curr.def and curr.def[j + 1].type == 'track' then
						it = it .. '|style="border-bottom:solid 1px gray" width=100 valign=top|\n';
						it = it .. '|style="border-bottom:solid 1px gray" width=285 valign=top|路軌\n';
					else
						it = it .. '|width=100|\n';
						it = it .. '|width=285|路軌\n';
					end
				elseif curr.def[j].type == 'platform edge' then
					it = it .. '|style="' .. tb .. 'border-left:solid 2px black;"|';
					if curr.def[j].id then
						it = it .. curr.def[j].id;
					end
					it = it .. '\n';
					it = it .. '|style="' .. tb .. 'border-right:solid 2px black;"|'
					if curr.def[j].dir then
						it = it .. curr.def[j].dir;
					end
					if curr.def[j].note then
						it = it .. ' <small>' .. curr.def[j].note .. '</small>\n';
					end
					it = it .. '\n';
				elseif curr.def[j].type == 'platform type' then
					it = it .. '|style="border-right:solid 2px black;border-left:solid 2px black;'
						.. tb .. '" colspan=2|<center><small>' .. curr.def[j].desc .. '</small></center>\n';
				end
				it = it .. '|-\n';
			end
		end
	end
	it = it .. solid_divider;
	it = it .. '|-\n';
	it = it .. '|}';
	return it;
end

-- 出入口
-- produce a list of entrances, somewhat mimicking the manual formatting expected on yuewiki
-- except Toronto EGD is garbage so the output will look somewhat like not identical to other lists

local function render_floor_sequence( s )
	local it;
	if type(s) == 'table' then
		it = '';
		for i = 1, #s, 1 do
			if i > 1 then
				if s[i] < s[i - 1] then
					it = it .. '↗';
				elseif s[i] == s[i - 1] then
					it = it .. '→';
				else
					it = it .. '↘';
				end
			end
			it = it .. s[i];
		end
	else
		it = s;
	end
	return it;
end

local function show_entrance( a, fake_id, aux, parent )
	local it;
	local official, actual;
	local comments = {};
	local position;
	official = a.website;
	if a.identification then
		actual = a.identification;
	elseif a.directional then
		actual = a.directional;
	end
	if official and not actual then
		it = official;
		if a.side then
			it = it .. '(' .. a.side .. '冇牌出口' .. ')';
		end
	elseif official and entrance_canonicalized(official) ~= entrance_canonicalized(actual) then
		it = "'''" .. actual .. "'''";
		table.insert(comments, '網上名稱:' .. official);
	elseif actual then
		it = "'''" .. actual .. "'''";
	elseif a.side then
		it = a.side .. '冇牌出口';	-- 其實冇記認,淨係有嘜低轉左轉右
	else	-- you can't make this stuff up
		it = "冇記認亦冇紀錄";
	end
	if actual then
		if aux[actual] then
			table.insert(aux[actual], fake_id);
		else
			aux[actual] = { fake_id };
		end
	end
	if a.active_p ~= nil and not a.active_p then
		it = '🚫 ' .. it;	-- U+1F6ab
	end
	if not a.def then
		position = {};
		for _, x in pairs({parent, a}) do
			if x then
				if x.inside then
					position.inside = x.inside;
				end
				if x.outside then
					position.outside = x.outside;
				end
				if x.intersection then
					position.intersection = x.intersection;
				end
				if x.corner then
					position.corner = x.corner;
				end
				if x.building then
					position.building = x.building;
				end
				if x.dir then
					position.dir = x.dir;
				end
			end
		end
	end
	if position then
		if position.intersection and #position.intersection > 3 then
			error(cvs.p('intersection ' .. position.intersection) .. ' 多過3個元素');
		elseif position.intersection and #position.intersection == 3 then
			local comment = position.intersection[1] .. ',' .. position.intersection[2];
			comment = join_segments(join_segments(comment, join_segments('同', position.intersection[3])), '之間');
			if position.corner then
				comment = comment .. ',' .. position.corner;
			end
			table.insert(comments, '位置:' .. comment);
		elseif position.intersection and #position.intersection == 2 then
			local comment = join_segments(join_segments(position.intersection[1], '夾'), position.intersection[2]);
			if position.corner then
				comment = join_segments(comment, position.corner);
			end
			table.insert(comments, '位置:' .. comment);
		elseif position.intersection and position.corner then
			table.insert(comments, '位置:' .. join_segments(position.intersection[1], position.corner));
		end
	end
	if #comments > 0 then
		it = it .. '(' .. table.concat(comments, ';') .. ')';
	end
	if position then
		if position.inside then
			if position.outside then
				it = it .. ',' .. render_floor_sequence(position.inside);
			else
				it = it .. ',站入面係' .. render_floor_sequence(position.inside);
				if type(position.inside) == 'number' or position.inside:match('^%d+$') then
					it = it .. '字樓';
				end
			end
		end
		if position.outside then
			if position.dir then
				if position.dir > 0 then
					it = it .. '↗';
				elseif position.dir < 0 then
					it = it .. '↘';
				else
					it = it .. '→';
				end
			else
				it = it .. ',';
			end
			it = it .. join_segments('出', position.outside);
		end
		if position.building then
			it = it .. '(' .. position.building .. ')';
		end
		if a.note then
			it = it .. ',' .. a.note;
		end
	end
	it = it .. '\n';
	return it;
end

local function find_dup_entrances( aux )
	local it;
	local next_number = 1;
	for actual, fake_ids in pairs(aux) do
		if #fake_ids > 1 then
			if not it then
				it = {};
				it.number_for_name = {};
				it.notes = {};
			end
			for _, fake_id in pairs(fake_ids) do
				if not it.number_for_name[actual] then
					it.number_for_name[actual] = next_number;
					it.notes[next_number] = fake_ids;
					next_number = next_number + 1;
				end
				it[fake_id] = actual;
			end
		end
	end
	return it;
end

local function conjoin( L )
	local it;
	if #L == 1 then
		it = L[1];
	elseif #L == 2 then
		it = join_segments(join_segments(L[1], '同'), L[2]);
	elseif L > 2 then
		it = join_segments(table.concat(L, '、', 1, #L - 1) '同', L[#L]);
	end
	return it;
end

local function chart_entrances( s )
	if not p.array_p(s) then
		error('出入口參數唔係陣列(' .. p.cvs(s) .. ')');
	elseif #s == 0 then
		error('出入口參數係空陣列');
	end
	local it;
	local aux = {};
	local items = {};
	for i, a in pairs(s) do
		local A = mw.ustring.char(64 + i);
		table.insert(items, { indent = '*', fake_id = A, value = show_entrance(a, A, aux) });
		if a.def then
			for j, b in pairs(a.def) do
				local B = A .. j;
				table.insert(items, {indent = '**', fake_id = B, value = show_entrance(b, B, aux, a)});
			end
		end
	end
	if items then
		local dups = find_dup_entrances(aux);
		it = '<div class=ttc-entrances>\n';
		for _, node in pairs(items) do
			it = it .. node.indent;
			if dups and dups[node.fake_id] then
				it = it .. '<sup>(' .. node.fake_id .. ')</sup> ';
			end
			it = it .. node.value;
		end
		it = it .. '</div>';
		if dups then
			local comment = '(唔係真嘅號碼,只係為方便討論)';
			local message = '呢個站有已知嘅撞名出口:\n';
			for number, fake_ids in pairs(dups.notes) do
				message = message .. '*出口 ' .. conjoin(fake_ids) .. comment .. join_segments('都係叫 ', dups[fake_ids[1]]) .. '\n';
				comment = ' ';
			end
			it = it .. '\n\n' .. make_disclaimer('小心', message);
		end
	end
	return it;
end

-- Entry point
p.main = function (frame, ruby_mode_p)
	local parent = frame:getParent();
	local s;
	local args = {};
	local f;
	local need_styles_p;
	for _, a in pairs({frame, parent}) do
		if a then
			for k, v in pairs(a.args) do
				v = v:gsub('^%s+', ''):gsub('%s+$', '');
				if type(k) == 'number' then
					if v == 'icon' or v == '圖標' then
						f = show_icon;
						need_styles_p = true;
					elseif v == 'icon and name' or v == '圖標同名' then
						f = show_icon_and_name;
						need_styles_p = true;
					elseif v == 'name' or v == '名' then
						f = show_name;
					elseif v == 'livery' or v == 'linecolor' or v == 'linecolour' or v == '顏色' then
						f = show_livery;
					elseif v == 'no link' or v == 'disable link' or v == '唔連' or v == '唔連文章' then
						args.no_link_p = true;
					elseif v == '出入口註釋' then
						f = show_disclaimer;
					need_styles_p = true;
					elseif lines[v] then
						args.line = v;
					else
						error('不明參數 「' .. v .. '」');
					end
				elseif k == 'line' or k == '路綫' then
					args.line = v;
				elseif k == 'link' or k == '連' or k == '連結' or k == '拎' then
					args.no_link_p = not interpret_boolean(v);
				elseif k == '出入口' then
					local a, b = pcall(mw.text.jsonDecode, v);
					if a then
						f = function () return chart_entrances(new_Entrance(b)); end;
						need_styles_p = true;
					else
						error(k .. '參數出錯,可能係 JSON 語法錯誤(' .. p.cvs(v) .. ')');
					end
				elseif k == '車站樓層' then
					local a, b = pcall(mw.text.jsonDecode, v);
					if a then
						f = function () return draw_station_description(b); end;
						need_styles_p = true;
					else
						error(k .. '參數出錯,可能係 JSON 語法錯誤(' .. p.cvs(v) .. ')');
					end
				-- 出入口註譯
				elseif k == 'locality' or k == '地區' then
					args.locality = v;
				elseif k == 'network' or k == '網絡' then
					args.network = v;
				elseif k == 'source' or k == '出處' then
					args.source = v;
				else
					error('不明參數 「' .. k .. '」');
				end
			end
		end
	end
	local it;
	if f then
		it = f(args);
	else
		error('冇指定顯示乜嘢');
	end
	-- request our style sheet
	if styles and need_styles_p then
		if it:match('^{|') then
			it = '\n' .. it;
		end
		it = table.concat ({ frame:extensionTag('templatestyles', '', {src=styles}), it });
	end
	it = it:gsub('\n$', '');
	return it;
end

p.test = function ()
	assert(icon_size('File:TTC - Line 4 - Sheppard line.svg') == 16);
	assert(icon_size('File:TTC - 509.svg') == 28);
	local a, b;

	a, b = pcall(p.main, {
			['getParent'] = function ()
					return {
							extensionTag = function () return '' end;
							args= { 'icon', line = '1' };
					};
			end;
			extensionTag = function () return '' end;
			args = {
			};
	});
	assert(a and b:match('Line 1.*16px'));

	a, b = pcall(p.main, {
			['getParent'] = function ()
					return {
							extensionTag = function () return '' end;
							args= { 'icon', line = 'YUS' };
					};
			end;
			extensionTag = function () return '' end;
			args = {
			};
	});
	assert(a and b:match('Line 1.*16px'));

	a, b = pcall(p.main, {
			['getParent'] = function ()
					return {
							extensionTag = function () return '' end;
							args= { 'livery', line = 'YUS' };
					};
			end;
			extensionTag = function () return '' end;
			args = {
			};
	});
	assert(a and b == '#f8c300');

	a, b = pcall(p.main, {
			['getParent'] = function ()
					return {
							extensionTag = function () return '' end;
							args= { 'livery', line = '509' };
					};
			end;
			extensionTag = function () return '' end;
			args = {
			};
	});
	assert(a and b == '');

	a, b = pcall(new_Entrance, mw.text.jsonDecode('[{ "website":"Yonge Street (Mel Lastman Square)", "def":[ { "outside":"地面", "xyzzy":"賴士民廣場" } ] } ]'));
	assert(not a and mw.ustring.match(b, '不明欄位'));

	a, b = pcall(new_Entrance, mw.text.jsonDecode('[{ "website":"Yonge Street (Mel Lastman Square)", "def":[ { "outside":"地面", "x-xyzzy":"賴士民廣場" } ] } ]'));
	assert(a);

end

p.chart_entrances = chart_entrances;
p.new_Entrance = new_Entrance;
return p;