Module:BSicon

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules

Code

local p = {}

-- local test = {}

local getArgs = require('Module:Arguments').getArgs

local function makeInvokeFunction(funcName)
	-- makes a function that can be returned from #invoke, using
	-- [[Module:Arguments]].
	return function(frame)
		local args = getArgs(frame, {parentOnly = true})
		return p[funcName](args)
	end
end

p.category = makeInvokeFunction('_category')

-- permutations of category titles that do not have the '/set ..' titleparts
-- in lexicographical order ('u','g','f','mixed' different, but also specific)
-- are filled with a category redirect if created with {{BS-category}} content
function sort_set_titleparts(cat_title, oc_cat)
	local _ocols = { ins = table.insert, srt = table.sort, rm = table.remove }
	cat_title:gsub("/set ([^/]+)", function (c) _ocols:ins(c) end)
	_ocols:srt(function (c1, c2) return c2=="mixed"
			or c2=="f" and not ("f:mixed"):find(c1,1,true)
			or ("g:f"):find(c2,1,true) and not ("g:f:mixed"):find(c1,1,true)
			or ("u:g:f"):find(c2,1,true) and not ("u:g:f:mixed"):find(c1,1,true)
			or not ("u:g:f:mixed"):find(c2,1,true) and
				not ("u:g:f:mixed"):find(c1,1,true) and c1 < c2
		end
	)
	local um = not oc_cat and #_ocols == 2 and _ocols[1]=="u" and _ocols[2]=="mixed"
	return cat_title:gsub("(/set )[^/]+", function (s) local c = _ocols:rm(1)
		return um and #_ocols > 0 and '' or s..c
	end)
end
function redirect(title, oc_cat)
	local t = sort_set_titleparts(title, oc_cat)
	local n, msg1
	
	t, n = mw.ustring.gsub(t, '/railway(.*/level crossing)', '/road–rail%1')
	if n > 0 then
		msg1 = function() return '\n' .. mw.getCurrentFrame()
			:expandTemplate{ title="notice", args = {
				"BSicons depicting non-road, same-leveled railway crossings usually categorize into simply '../crossing', " ..
				"while the term 'level crossing' binds with the more specific, but also more frequent ''road–rail level crossings'' " ..
				"for the purpose of categorizing within [[:Category:BSicon/road–rail]] subtree." }
			}
		end
	end
	-- see 'level crossing' entry in roots table below, no need to have the term twice in a title
	t = t:find('/level crossing',1,true) and mw.ustring.gsub(t..'/', '/crossing/', '/'):gsub('/$', '') or t
	
	-- sometimes 'one quarter' cats are created as 'one quarters', catch and redirect..
	t = mw.ustring.gsub(t, '(/one quarter)s', '%1')
	
	if title ~= t then
		return mw.getCurrentFrame()
			:expandTemplate{ title="category redirect", args = { t } }
			.. (msg1 and msg1() or '')
	end
end

-- used to categorize BSicon graphics, it populates the category tree and
-- fills a category header with some basic information, categories by nature
-- can be monotone - to display icons from different cats on the same page
-- either BSicon/Catalogue or other _gallery_ pages can be used
function p._category(args)
	local title, categories, color, result, tmp = mw.title.getCurrentTitle(), '', {''}, ''
	if title.namespace ~= 14 then return error("This template should only be used on category pages") end
	title = title.text
	
	local oc_cat = title:match("/other[^/]*colors$")
	local redir = redirect(title, oc_cat)
	if redir then return redir end
	
	title = mw.ustring.gsub(title, '%+', '#')
	title = mw.ustring.gsub(title, '%-', '_')
	local title2 = mw.clone(title)
	local rgb, contrast = require('Module:Routemap')._RGBbyCode, require('Module:Color contrast')._ratio
	local function rgb_cell(x)
		x = tostring(x) or 'BE2D2C'
		return '<td class="rgb '..((contrast{x, 'FFF'} < 4.5) and 'light' or 'dark')..'" style="background-color: #'..x..'"><kbd>#'..x..'</kbd>'
	end
	local function hex(x, ex)
		x = rgb{mw.ustring.gsub((ex and 'ex_' or '')..(tostring(x) or ''), '^ex_([ufg]?)$', '%1ex', 1)}
		return rgb_cell(x)
	end
	local roots = {
		BHF = 'stations/',
		HST = 'stations/',
		DST = 'stations/',
		BST = 'stations/',
		INT = 'stations/',
		ACC = 'stations/',
		INTACC = 'stations/',
		HSTACC = 'stations/',
		suburban = 'stations/',
		SBHF = 'stations/',
		SHST = 'stations/',
		['S#BHF'] = 'stations/',
		['S#HST'] = 'stations/',
		['BHF#DST'] = 'stations/',
		['BHF#HST'] = 'stations/',
		['HST#BST'] = 'stations/',
		RD = 'generic road/',
		RP1 = 'generic road/',
		RP2 = 'generic road/',
		RP4 = 'generic road/',
		fork = 'junction/',
		wye = 'junction/',
		split = 'junction/',
		formations = 'legende/',
		['level crossing'] = 'crossing/',
		['straight#curve'] = 'curve/',
		['straight#shift'] = 'shift/',
		['straight#junction'] = 'junction/',
		['straight#corner'] = 'corner/',
		['crossing#wye'] = 'junction/',
		['crossing#split'] = 'junction/',
	}
	local compounds = {
		['road–rail'] = 'railway/road',
		INTACC = 'INT/ACC',
		HSTACC = 'HST/ACC',
		['S#BHF'] = 'suburban/BHF',
		['S#HST'] = 'suburban/HST',
		SBHF = 'suburban/BHF',
		SHST = 'suburban/HST',
		['BHF#DST'] = 'BHF/DST',
		['BHF#HST'] = 'BHF/HST',
		['HST#BST'] = 'HST/BST',
		['curve#corner'] = 'curve/corner',
		['crossing#corner'] = 'crossing/corner',
		['crossing#junction'] = 'crossing/junction',
		['crossing#wye'] = 'crossing/wye',
		['crossing#split'] = 'crossing/split',
		['junction#corner'] = 'junction/corner',
	}
	local used_roots = {}
	local matches = {}
	local used_compounds = {}
	title = title..'/'
	for k, v in pairs(roots) do
		if mw.ustring.match(title, '/'..k..'/') then
			title = mw.ustring.gsub(title, k, v..k)
			table.insert(used_roots, k)
			used_roots[k] = true
		end
	end
	title = mw.ustring.gsub(title, '/$', '')
	for k, v in pairs(compounds) do
		if mw.ustring.match(title, k) then
			title = mw.ustring.gsub(title, k, v)
			table.insert(matches, k)
			table.insert(used_compounds, v)
		end
	end
	local legende_color
	if mw.ustring.match(title, 'water') then
		result = result..'\n<tr><td>water'..rgb_cell('007CC3')
	end
	if mw.ustring.match(title, 'tunnel to') or mw.ustring.match(title, 'portal') or mw.ustring.match(title, 'elevated') or mw.ustring.match(title, 'bridge') or mw.ustring.match(title, 'crossing') or mw.ustring.match(title, '/tower') or mw.ustring.match(title, 'cutting') or mw.ustring.match(title, 'embankment') then
		result = result..'\n<tr><td>structure'..rgb_cell('80A080')
		legende_color = true
	end
	if mw.ustring.match(title, 'line endings') then
		result = result..'\n<tr><td>line ending (open)'..rgb_cell('000')..'<tr><td>line ending (closed)'..rgb_cell('AAA')
		legende_color = true
	end
	if mw.ustring.match(title, 'border') then
		result = result..'\n<tr><td>border (active)'..rgb_cell('000')..'<tr><td>border (inactive)'..rgb_cell('AAA')
		legende_color = true
	end
	if mw.ustring.match(title, 'platform') then
		result = result..'\n<tr><td>platform (open)'..rgb_cell('888')..'<tr><td>platform (closed)'..rgb_cell('CCC')
		legende_color = true
	end
	if mw.ustring.match(title, 'mask') then
		result = result..'\n<tr><td>mask'..rgb_cell('F9F9F9')
		legende_color = true
	end
	if mw.ustring.match(title, 'INT') then
		result = result..'\n<tr><td>INT (open)'..rgb_cell('000')..'<tr><td>INT (closed)'..rgb_cell('AAA')
		legende_color = true
	end
	if mw.ustring.match(title, 'ACC') then
		result = result..'\n<tr><td>ACC (open)'..rgb_cell('034EA2')..'<tr><td>ACC (closed)'..rgb_cell('6592C5')
		legende_color = true
	end
	if mw.ustring.match(title, 'CPIC') then
		result = result..'\n<tr><td rowspan="2">cross-platform<br/>interchange'..rgb_cell('000')..'<tr>'..rgb_cell('B3B3B3')
	end
	if mw.ustring.match(title, 'S#?BHF') or mw.ustring.match(title, 'S#?HST') or mw.ustring.match(title, 'suburban') then
		result = result..'\n<tr><td>S-Bahn (open)'..rgb_cell('006E34')..'<tr><td>S-Bahn (closed)'..rgb_cell('5ABF89')
		legende_color = true
	end
	if mw.ustring.match(title, 'DST') or mw.ustring.match(title, 'BST') or mw.ustring.match(title, 'ACC') or mw.ustring.match(title, 'INT') or mw.ustring.match(title, 'S#?BHF') or mw.ustring.match(title, 'S#?HST') or mw.ustring.match(title, 'suburban') then
		result = result..'\n<tr><td>fill'..rgb_cell('FFF')
	end
	legende_color = legende_color and mw.ustring.match(title, 'legende')
	
	local r = { ins = table.insert, rm = table.remove }
	r:ins(mw.getCurrentFrame():expandTemplate{ title = 'BS-set' })
	if oc_cat then
		r:ins("<div style=\"clear:right;float:right;padding-left:1.5em\">\n")
		r:ins(mw.getCurrentFrame():expandTemplate{ title = 'Collapse', args = {
				"\n" .. mw.getCurrentFrame():expandTemplate{ title = 'BS-colorlist', args = {} },
				title = 'BSicon color list&nbsp;&nbsp;'
			}
		})
		r:ins("\n</div>\n")
	end
	r:ins("These [[BSicon]]s are to be used with '''route diagram templates'''. ")
	r:ins("For an overview, see [[:Category:BSicon]].\n")
	r:ins('<table class="wikitable"><tr><th>Colour<th>RGB hex triplet')
	
	title_string = mw.clone(title)..'/'
	title = mw.text.split(title, '/')
	if title[1] ~= 'BSicon' then return '' end
	if title[2] == 'railway' and (not used_roots['formations']) and (not legende_color) then
		-- 'set mixed' assumes {'', 'u'} combination, while a 2nd color (cp. "/set mixed/set azure") may override 'u'
		-- if 'set mixed' follows, then '' (default color) is combined with previous one (cp. "/set azure/set mixed")
		-- {{BS-category}} produces color table for line(s) as long as at least one set spec is in category title
		-- caveat: the interpretation of 'set mixed' is closely oriented on its use in icon title codes, which by definition
		--         mixes default color with color u ('/set mixed' and '/set u/set mixed' are ident in that respect), while
		--         the term 'set mixed' within BSicon scope implies 'mixed colors', not all 'mixed colors' icons are in 'set mixed'
		local i = ((title[3] ~= 'road') and (title[3] ~= 'water')) and 3 or 4
		local ci = 1
		while i <= #title do
			local c = title[i] and mw.ustring.match(title[i], '^set (.+)$')
			if c then
				for _c in mw.text.gsplit(c, '–') do
					if _c == 'mixed'
					then color[ci] = ''    if ci == 1 and not oc_cat then color[2] = 'u' end
					else color[ci] = _c
					end
					ci = ci + 1
				end
			end
			i = i + 1
		end
		r:ins('\n<tr><td rowspan="') r:ins(tostring(#color))
		r:ins('">open line'..(#color>1 and 's' or ''))
		for _, c in ipairs(color) do r:ins(hex(c)) r:ins('<tr>') end r:rm()
		r:ins('\n<tr><td rowspan="') r:ins(tostring(#color))
		r:ins('">closed line'..(#color>1 and 's' or ''))
		for _, c in ipairs(color) do r:ins(hex(c, true)) r:ins('<tr>') end r:rm()
		if title[3] == 'road' then
			local generic = { "RD", "RP1", "RP2", "RP4" }
			local classed = { "RA", "RM", "RR", "RB", "RG", "RE", "RY" }
			local _g, _c = { }, { }
			for _, v in ipairs(title) do
				for _, vg in ipairs(generic) do if v == vg then _g[#_g+1] = v end end
				for _, vc in ipairs(classed) do if v == vc then _c[#_c+1] = v end end
				if v == "set f" then
					for i, vs in ipairs(r) do
						if tostring(vs):find(' line',1,true) then
							r[i] = r[i] .. ' or footpath'
						end
					end
				end
				if v:sub(1,7) == "generic" then
					classed, _c = { }, { }
				end
			end
			if #_g > 0 or #_c > 0 then generic, classed = _g, _c end
			if #generic > 0 then
				r:ins('\n<tr><td>generic road<td>')
				for _, v in ipairs(generic) do r:ins('[[File:BSicon '..v:gsub('RD','RD1')..'.svg|x30px| ]]&nbsp;') end
			end
			if #classed > 0 then
				r:ins('\n<tr><td>classed road<td>')
				for _, v in ipairs(classed) do r:ins('[[File:BSicon '..v..'.svg|x30px| ]]&nbsp;') end
			end
			for i, v in ipairs(r) do -- adjust header to accomodate road rows
				if v:find('<table',1,true) then
					r[i] = r[i]:gsub('<th>([^<]*)<th>([^<]*)', '<th>Object<th>%2 or prototype'
						..((#generic + #classed)>1 and 's' or ''))
				end
			end
		end
	end
	
	if mw.ustring.match(result, '<t[rd]>') then r:ins(result) end
	if r[#r]:find("<th>",1,true) -- if only html table skeleton, remove it
	then for i = 1, #r do if r:rm():find("<table>",1,true) then i = #r end end
	else r:ins("</table>")
	end
	
	if oc_cat then
		r:ins("\nThis category is on categories holding icons ")
		r:ins(title[#title]:find('mixed',1,true) -- if '/other mixed colors' ..
			and 'combining the set color(s) given above with an additional one'
			or 'replacing the set color given above with one'
		)
		r:ins(" from [[:Template:BS-colorlist|BSicon color list]].")
	end
	
	local set_subkey = '0'
	table.remove(title, 1)
	for k, v in ipairs(title) do
		if not ((mw.ustring.match((title[2] or ''), '^set ') or mw.ustring.match((title[3] or ''), '^set ')) and title[k] == 'railway')
		and not ((mw.ustring.match((title[2] or ''), 'R([A-Z0-9]+)$') or mw.ustring.match((title[3] or ''), 'R([A-Z0-9]+)$') or mw.ustring.match((title[4] or ''), 'R([A-Z0-9]+)$') or mw.ustring.match((title[5] or ''), 'R([A-Z0-9]+)$') or title[2] == 'generic road' or title[3] == 'generic road' or title[4] == 'generic road') and title[k] == 'road')
		and not ((title_string or ''):find('/level crossing',1,true) and title[k] == 'road' and title[k-1] and title[k-1] == 'railway')
		and (#title < 3 or k > 1 or (title[2] == 'road' and k == 1) or (title[1] ~= 'railway' and title[1] ~= 'road' and title[1] ~= 'canal'))
		and not (title[k] == 'shift' and mw.ustring.match((title[k+1] or ''), ' quarters?$'))
		and not (title[k] == 'stations' and (title[k+1] == 'interchange' or title[k+1] == 'terminus' or title[k+1] == 'limited'))
		and not (title[k] == 'interchange' and (title[k+1] == 'CPIC'))
		and not (title[k] == 'uw' and title[k+1] == 'double')
		and not (title[k] == 'parallel lines' and ((title_string or ''):find('/straight#shift',1,true) or (title_string or ''):find('/double/',1,true)))
		and not (title[k] == 'tunnel' and (title[k+1] == 'portal' or title[k+2] == 'portal'))
		and not (oc_cat and title[#title] ~= v) then
			tmp = mw.clone(title)
			tmp = 'BSicon/'..table.concat(tmp, '/')
			for _, x in ipairs(used_compounds) do
				if mw.ustring.match(x, v) then
					x = mw.ustring.gsub(x, v, '')
					x = mw.ustring.gsub(x, '/', '')
					local tmp_root = roots[x]
					if tmp_root then
						tmp = mw.ustring.gsub(tmp, tmp_root, '')
					end
				end
			end
			tmp = tmp..'/'
			tmp = mw.ustring.gsub(tmp, '/'..v..'/', '/')
			tmp = mw.ustring.gsub(tmp, '/+', '/')
			for k, v in ipairs(matches) do
				tmp = mw.ustring.gsub(tmp, compounds[v], v)
			end
			for k, v in ipairs(used_roots) do
				if mw.ustring.match(tmp, '/'..v..'/') then tmp = mw.ustring.gsub(tmp, '/'..roots[v], '/') end
			end
			tmp = mw.ustring.gsub(tmp, '/junction/crossing/', '/crossing+junction/')
			tmp = mw.ustring.gsub(tmp, '/$', '')
			if title2 ~= tmp then
				local tmpsub, omctitle = '', '/other mixed colors'
				local c, n = mw.ustring.gsub(title[k], '^set ([ufg])$', '!++++'..set_subkey..'%1')
				if n < 1 then c, n = mw.ustring.gsub(c, '^set mixed', '!+++'..set_subkey) end
				if n < 1 then c, n = mw.ustring.gsub(c, '^set ex', '!+'..set_subkey) end
				if n < 1 then c, n = mw.ustring.gsub(c, '^set ', '!++'..set_subkey) end
				if n > 0 then
					set_subkey = string.char(set_subkey:byte() + 1)
					if title[1] == 'railway' and title[k] ~= 'set u' and title[k] ~= 'set mixed' then
						tmpsub = tmp:find('/set ',1,true) and omctitle or '/other colors'
					end
				end
				if n < 1 then c, n = mw.ustring.gsub(c, '^other.*colors$',  '!,other') end
				if n < 1 then c, n = mw.ustring.gsub(c, '^railway$',        '!'..'%0') end
				if n < 1 then c, n = mw.ustring.gsub(c, '^R([A-Z0-9]+)$',   '$'..'%1') end
				if n < 1 then c, n = mw.ustring.gsub(c, '^(generic) road$', '$'..'%1') end
				if n < 1 then c, n = mw.ustring.gsub(c, '^road$',           '$'..'%0') end
				if n < 1 then c, n = mw.ustring.gsub(c, '^(.*width)$',      '*'..'%1') end
				if n < 1 then c, n = mw.ustring.gsub(c, '^parallel lines$', '*'..'%0') end
				if n < 1 then c, n = mw.ustring.gsub(c, '^([a-z]+) quarters?$',
					{ one=1, two=2, three=3, four=4, five=5, six=6, seven=7, eight=8 })
				end
				categories = categories..'\n[[Category:'..tmp..tmpsub..'|'..c..']]'
				if oc_cat then
					c, n = categories, 0
					for _, v in ipairs(color) do
						if v=='u' then n = n + 1 end
						if v=='' then n = n + 2 end
					end
					if n > 2 then
						categories = c:gsub('/set u([^|]*)', '%1'..omctitle)
							.. c:gsub('/set mixed([^|]*)', '%1'..omctitle)
					elseif n == 1 then
						categories = c .. c:gsub('/set u', '/set mixed')
					elseif n == 2 then
						if #color == 1 then
							r:ins("<br />It also contains the (indirect) categories on mixing non-default set colors with another one from this list.")
						end
					elseif n == 0 then
						if #color == 1 then
							categories = '[[Category:'..tmp:gsub("/set "..color[1], "/set mixed")..omctitle..'|'
								.. (#color[1] < 2 and '@' or (color[1]:sub(1,2) == 'ex' and '\\' or '')) .. color[1]:upper()
								..']]\n' .. c
						end
					end
				end
			end
		end
	end
	categories = mw.ustring.gsub(categories, '#', '+')
	categories = mw.ustring.gsub(categories, '_', '-')
	
	return table.concat(r)..categories
end

p.categorize = makeInvokeFunction('_categorize')

-- NOT COMPLETE YET. THIS WILL NOT WORK ON A LOT OF ICONS AND NEEDS A LOT OF CATEGORY TITLE PARTS TO BE ADDED.

function p._categorize(args)
	local title = (mw.ustring.match(mw.title.getCurrentTitle().text, '^BSicon (.*)%.svg$') or 'BHF')
	local category = {['Category:BSicon'] = true}
	local titleparts
	local tmp_set
	local tmp_match = ''
	local result = {}
	if not (mw.ustring.match(title, '[^~]R[ABDEGMPRY]') or mw.ustring.match(title, '^R[ABDEGMPRY]') or mw.ustring.match(title, 'WASSER') or mw.ustring.match(title, 'WABZ')) then
		category['railway'] = true
		titleparts = mw.text.split(title, ' ')
		if titleparts[2] then 
			for k = 2, #titleparts do
				if mw.ustring.match(titleparts[k], '^[a-z]+$') then
					category['set '..titleparts[k]] = true
					category['other colors'] = true
				else
					tmp_set = mw.ustring.match(titleparts[k], '^[a-z]+')
					if tmp_set then category['set '..tmp_set] = true end
					titleparts[1] = titleparts[1]..string.sub(titleparts[k], string.len(tmp_set or '')+1)
				end
			end
		end
		titleparts = titleparts[1]
		titleparts = '|'..mw.ustring.gsub(titleparts, '.', '%0|')
		titleparts = mw.ustring.gsub(titleparts, '|;|g', '|;|ABZ|g')
		titleparts = mw.ustring.gsub(titleparts, '([A-ZÜ])|([A-ZÜ])', '%1%2')
		titleparts = mw.ustring.gsub(titleparts, '([A-ZÜ])|([A-ZÜ])', '%1%2')
		titleparts = mw.ustring.gsub(titleparts, '|n|u|m|', '|NUM|')
		titleparts = mw.ustring.gsub(titleparts, '|([A-ZÜ][A-ZÜ]+)(SPL)|', '|%1|%2|')
		titleparts = mw.ustring.gsub(titleparts, '|([A-ZÜ][A-ZÜ]+)(SHI)|', '|%1|%2|')
		titleparts = mw.ustring.gsub(titleparts, '(SHI)|([1-8])', '%1%2')
		titleparts = mw.ustring.gsub(titleparts, '(BRÜCKE)|([1-3]|[^%+])', '%1%2')
		titleparts = mw.ustring.gsub(titleparts, '(BRÜCKE)|([1-3]|)$', '%1%2')
		titleparts = mw.ustring.gsub(titleparts, '(VIADUKT)|([1-3]|[^%+])', '%1%2')
		titleparts = mw.ustring.gsub(titleparts, '(VIADUKT)|([1-3]|)$', '%1%2')
		titleparts = mw.ustring.gsub(titleparts, '(TUNNEL)|([12]|[^%+])', '%1%2')
		titleparts = mw.ustring.gsub(titleparts, '(TUNNEL)|([12]|)$', '%1%2')
		-- Other roots
		titleparts = mw.ustring.gsub(titleparts, '|?(SW)([A-HJ-ZÜ][A-HJ-ZÜ])', '|!SW|%2')
		titleparts = mw.ustring.gsub(titleparts, '|?S|%+|([A-ZÜ][A-ZÜ])', '|S+%1')
		titleparts = mw.ustring.gsub(titleparts, '|LL?([A-KMNPQS-ZÜ])', '|L|%1')
		titleparts = mw.ustring.gsub(titleparts, '|M([A-ZÜ])', '|M|%1')
		titleparts = mw.ustring.gsub(titleparts, '|M|ASK', '|MASK')
		titleparts = mw.ustring.gsub(titleparts, '|P([A-RT-ZÜ][A-ZÜ][A-ZÜ])', '|P|%1')
		titleparts = mw.ustring.gsub(titleparts, '|PS([A-KM-ZÜ][A-ZÜ])', '|P|S%1')
		titleparts = mw.ustring.gsub(titleparts, '|T([A-DF-QSTV-ZÜ])', '|T|%1')
		titleparts = mw.ustring.gsub(titleparts, '|K([A-LN-QS-ZÜ])', '|K|%1')
		titleparts = mw.ustring.gsub(titleparts, '|X([A-ZÜ])', '|X|%1')
		titleparts = mw.ustring.gsub(titleparts, '|X|PLT', '|X')
		-- Other capital prefix and combining suffix searches
		-- Discard x, since it isn't used for categorization and only has one meaning
		titleparts = mw.ustring.gsub(titleparts, '|x|', '|')
		titleparts = mw.ustring.gsub(titleparts, '^([ufgelhatpnk|]*|)(c?)|?(d?)|?(b?)|', '%1%2%3%4|')
		titleparts = mw.ustring.gsub(titleparts, '|([%+%-])|([lhtnCDLM]?)|?([ck])|([1-4])|?([1-4]?)|?([1-4]?)|?([1-4]?)|', '|%1%3%4%5%6%7|%2|')
		titleparts = mw.ustring.gsub(titleparts, '|([lhtnCDLM]?)|?c|([1-4])|?([1-4]?)|?([1-4]?)|?([1-4]?)|', '|c%2%3%4%5|%1|')
		titleparts = mw.ustring.gsub(titleparts, '(|[%-~@%+])|([LRFGM]+|)', '%1%2')
		titleparts = mw.ustring.gsub(titleparts, '(|%-)([LRFGM]+|[A-ZÜ]+)', '%1|%2') -- split prefix L/F/M from parallel lines syntax
		titleparts = mw.ustring.gsub(titleparts, '|([~@])|([lrfgm])|?([lrfgm]*)|?([lrfgm]*)|?([lrfgm]*)|?([lrfgm]*)|?([lrfgm]*)|', '|%1%2%3%4%5%6|')
		titleparts = mw.ustring.gsub(titleparts, '|%(|([LRFGM]*)|?([LRFGM]*)|?([LRFGM]*)|?([LRFGM]*)|?([lrfgm]*)|?([lrfgm]*)|?([lrfgm]*)|?([lrfgm]*)|?([lrfgm]*)|%)|', '|(%1%2%3%4%5%6%7%8%9)|')
		titleparts = mw.ustring.gsub(titleparts, '^|([ufg])|', '|#%1|')
		titleparts = mw.ustring.gsub(titleparts, '(|[%-%+]|[^A-ZÜ%-%+]+|)([fg]|.*|[A-ZÜ][A-ZÜ])', '%1#%2')
		titleparts = mw.ustring.gsub(titleparts, '(|[%-%+]|[^A-ZÜ%-%+]+|)([fg]|.*|[A-ZÜ][A-ZÜ])', '%1#%2')
		titleparts = mw.ustring.gsub(titleparts, '(|[%-%+]|[^A-ZÜ%-%+]+|)([fg]|[A-ZÜ][A-ZÜ])', '%1#%2')
		titleparts = mw.ustring.gsub(titleparts, '(|[%-%+]|[^A-ZÜ%-%+]+|)([fg]|[A-ZÜ][A-ZÜ])', '%1#%2')
		titleparts = mw.ustring.gsub(titleparts, '^(|[^A-ZÜ%-%+]+|)([fg]|.*|[A-ZÜ][A-ZÜ])', '%1#%2')
		titleparts = mw.ustring.gsub(titleparts, '^(|[^A-ZÜ%-%+]+|)([fg]|.*|[A-ZÜ][A-ZÜ])', '%1#%2')
		titleparts = mw.ustring.gsub(titleparts, '^(|[^A-ZÜ%-%+]+|)([fg]|[A-ZÜ][A-ZÜ])', '%1#%2')
		titleparts = mw.ustring.gsub(titleparts, '^(|[^A-ZÜ%-%+]+|)([fg]|[A-ZÜ][A-ZÜ])', '%1#%2')
		if mw.ustring.match(titleparts, '|m|') then
			category['set mixed'] = true
			titleparts = mw.ustring.gsub(titleparts, '|m|', '|')
		end
		titleparts = mw.ustring.gsub(titleparts, '|([%-%+])|([fg]|.*|[A-ZÜ][A-ZÜ])', '|%1|#%2')
		titleparts = mw.ustring.gsub(titleparts, '|([%-%+])|([fg]|[A-ZÜ][A-ZÜ])', '|%1|#%2')
		titleparts = mw.ustring.gsub(titleparts, '|([^%-]+)|([htCD])|([1-4lrfgm])|([ae])|$', '|%2|%1|%3|%4|')
		titleparts = mw.ustring.gsub(titleparts, '|([^%-]+)|([htCD])|([1-4lrfgm])|([ae])|([^A-Z%-]+)|', '|%2|%1|%3|%5|%4|')
		titleparts = mw.ustring.gsub(titleparts, '|%+|([LRFG]+)|', '|+%1|')
		titleparts = mw.ustring.gsub(titleparts, '|;|', '|;|KRZ|')
		titleparts = mw.ustring.gsub(titleparts, '|;|KRZ|([A-ZÜ][A-ZÜ])', '|;|%1')
		titleparts = mw.ustring.gsub(titleparts, '|;|KRZ|([^%-]*|[A-ZÜ][A-ZÜ])', '|;|%1')
		-- remove q from KRZ/KRX because it will trip up other regexes
		titleparts = mw.ustring.gsub(titleparts, '(KR[XZ])|q|', '%1|')
		titleparts = mw.ustring.gsub(titleparts, '|([CDW])ABZ|', '|%1|ABZ|')
		titleparts = mw.ustring.gsub(titleparts, '|([CDW])WYE|', '|%1|WYE|')
		titleparts = mw.ustring.gsub(titleparts, '|ABZ|([gq]?)|?([1-4lrfgm%+])|?([1-4lrfgm%+]?)|?([1-4lrfgm%+]?)|?([1-4lrfgm%+]?)|?([1-4lrfgm%+]?)|?([1-4lrfgm%+]?)|?([1-4lrfgm%+]?)|', '|ABZ|%1%2%3%4%5%6%7%8|')
		titleparts = mw.ustring.gsub(titleparts, '|WYE|([gq]?)|?([1-4lrfgm%+])|?([1-4lrfgm%+]?)|?([1-4lrfgm%+]?)|?([1-4lrfgm%+]?)|', '|WYE|%1%2%3%4%5|')
		titleparts = mw.ustring.gsub(titleparts, '(KR[ZX])([CDLMW])|', '%1|%2|')
		titleparts = mw.ustring.gsub(titleparts, '|([CDW])(KR[ZX])|', '|%1|%2|')
		titleparts = mw.ustring.gsub(titleparts, '|([CDW])STR|', '|%1|STR|')
		titleparts = mw.ustring.gsub(titleparts, '|(SHI%d)|(g?)|?([lr%+])|?([lr%+]?)|?([lr%+]?)|?([lr%+]?)|?([lr%+]?)|?(q?)|', '|%1|%2%3%4%5%6%7%8|')
		titleparts = mw.ustring.gsub(titleparts, '|(SHI%d)|(g?[lr]?[lr]?)(q?)|', '|%1|%2+%3|')
		titleparts = mw.ustring.gsub(titleparts, '|SPL|([ae])|?([lr]?)|?([lr]?)|?(%+?)|?([lr]?)|?([lr]?)|?(%+?)|?([gq]?)|', '|SPL|%1%2%3%4%5%6%7%8|')
		titleparts = mw.ustring.gsub(titleparts, '|SPL|([ae])|g|(%+?)|?([lr]?)|?([lr]?)|?(q?)|', '|SPL|%1g%2%3%4%5|')
		titleparts = mw.ustring.gsub(titleparts, '|KRW|(g?)|?([lr%+])|?([lr%+]?)|?([lr%+]?)|?([lr%+]?)|?([lr%+]?)|?(q?)|', '|KRW|%1%2%3%4%5%6%7|')
		titleparts = mw.ustring.gsub(titleparts, '|ÜWB|([lr]?)|?(%+?)|?([lr]?)|?([lr]?)|?(q?)|', '|ÜWB|%1%2%3%4%5|')
		titleparts = mw.ustring.gsub(titleparts, '|ÜWB|([lr]?)(q?)|', '|ÜWB|%1+%2|')
		titleparts = mw.ustring.gsub(titleparts, '|([A-Z][A-Z%+]+[1-3]?)|([1-4lrfgm])|?([ou]?)|?([htCD]?)|%+|([1-4lrfgm])|?([ou]?)|', '|%1|%2+%5|%3%6|%4|')
		titleparts = mw.ustring.gsub(titleparts, '|([A-Z][A-Z%+]+[1-3]?)|%+|([1-4lrfgm])|?([ou]?)|', '|%1|+%2|%3|')
		titleparts = mw.ustring.gsub(titleparts, '|([A-Z][A-Z%+]+[1-3]?)|([1-4lrfg])|?([ou]?)|', '|%1|%2+|%3|')
		titleparts = mw.ustring.gsub(titleparts, '(|[%-%+]|[^A-ZÜ%-%+]+|)(u)|', '%1#%2|')
		titleparts = mw.ustring.gsub(titleparts, '(|[%-%+]|[^A-ZÜ%-%+]+|)(u)|', '%1#%2|')
		titleparts = mw.ustring.gsub(titleparts, '^(|[^A-ZÜ%-%+]+|)(u)|', '%1#%2|')
		titleparts = mw.ustring.gsub(titleparts, '^(|[^A-ZÜ%-%+]+|)(u)|', '%1#%2|')
		titleparts = mw.ustring.gsub(titleparts, '|([%-%+])|(u)|', '|%1|#%2|')
		
		local chunks = mw.text.split(mw.ustring.gsub(titleparts, "|([v^|]+)|%-|", "|%1|"), "|[%-%+;]|")
		local titleparts_table = {}
		for i, v in ipairs(chunks) do
			if mw.ustring.match(v, "[^|]+") then
				if i == 1 then
					v = v .. "|"
				elseif i == #chunks then
					v = "|" .. v
				else
					v = "|" .. v .. "|"
				end
				table.insert(titleparts_table, v)
			end
		end
		titleparts_categories = {}
		if #titleparts_table < 2 then
			titleparts_table = {titleparts}
		else
			if mw.ustring.match(titleparts, '|[v%-]|') then
				category['parallel lines'] = true
			end
		end
		
		for i1, v1 in ipairs(titleparts_table) do
			titleparts_categories[i1] = {}
			for k, v in pairs({
				['|c|'] = 'quarter-width',
				['|d|'] = 'half-width',
				['|cd|'] = 'three-quarter-width',
				['|b|'] = 'double-width',
				['|v|'] = 'parallel lines',
				['|%-|'] = 'parallel lines',
				['|l|'] = 'legende',
				['|h|'] = 'elevated',
				['|t|'] = 'tunnel',
				['|p|'] = 'limited',
				['|n|'] = 'narrow',
				['|C|'] = 'cutting',
				['|D|'] = 'embankment',
				['|k|'] = 'k',
				['|3|'] = '3',
				['|L|'] = 'interruption',
				['|M|'] = 'mask',
				['|P|'] = 'platform',
				['|T|'] = 'crossing',
				['|W|'] = 'water',
				['|[%+%-]?k[1-4][1-4]?[1-4]?[1-4]?|'] = 'k',
				['|[%+%-]?[ck][1-4][1-4]?[1-4]?[1-4]?|'] = 'corner',
				['|#u|'] = 'set u',
				['|#f|'] = 'set f',
				['|#g|'] = 'set g',
				['|[cdbswv%|]+|e?|?#[ufg]|'] = 'set mixed',
				['|%-|e?|?#[ufg]|'] = 'set mixed',
				['|X|'] = 'interchange',
				['|[X]|'] = 'CPIC',
				['|ABZ|'] = 'junction',
				['|WYE|'] = 'wye',
				['|BHF|'] = 'BHF',
				['|HST|'] = 'HST',
				['|DST|'] = 'DST',
				['|BST|'] = 'BST',
				['|INT|'] = 'INT',
				['|ACC|'] = 'ACC',
				['|INTACC|'] = 'INTACC',
				['|HSTACC|'] = 'HSTACC',
				['|SBHF|'] = 'SBHF',
				['|S%+BHF|'] = 'S+BHF',
				['|SHST|'] = 'SHST',
				['|S%+HST|'] = 'S+HST',
				['|K|X|'] = 'terminus',
				['|K|BHF|'] = 'terminus',
				['|K|HST|'] = 'terminus',
				['|K|DST|'] = 'terminus',
				['|K|BST|'] = 'terminus',
				['|K|INT|'] = 'terminus',
				['|K|ACC|'] = 'terminus',
				['|K|INTACC|'] = 'terminus',
				['|K|HSTACC|'] = 'terminus',
				['|K|SBHF|'] = 'terminus',
				['|K|S%+BHF|'] = 'terminus',
				['|K|SHST|'] = 'terminus',
				['|K|S%+HST|'] = 'terminus',
				['|HUB|'] = 'hub',
				['|CONT|'] = 'continuation',
				['|ENDE|'] = 'line endings',
				['|GRZ|'] = 'border',
				['|KR[XZ]|'] = 'crossing',
				['|KRX|'] = 'uw',
				['|KMW|'] = 'milepost',
				['|PLT|'] = 'platform',
				['|SPL|'] = 'split',
				['|SHI%d|'] = 'shift',
				['|SHI1|'] = 'one quarter',
				['|SHI2|'] = 'two quarters',
				['|SHI3|'] = 'three quarters',
				['|SHI4|'] = 'four quarters',
				['|KRW|'] = 'krw',
				['|SHI5|'] = 'five quarters',
				['|SHI6|'] = 'six quarters',
				['|SHI7|'] = 'seven quarters',
				['|SHI8|'] = 'eight quarters',
				['|WASSER|'] = 'water',
				['|WSL|'] = 'loop',
				['|ZOLL|'] = 'customs',
				['|HUB|'] = 'hub',
				['|%-[LMR]+|'] = 'interchange',
				['|MASK|'] = 'mask',
				['|[uo]+|'] = 'crossing',
				
			}) do
				if mw.ustring.match(v1, k) then titleparts_categories[i1][v] = true end
			end
			if titleparts_categories[i1]['set mixed'] and titleparts_categories[i1]['set u'] and not titleparts_categories[i1]['other colors'] then titleparts_categories[i1]['set u'] = nil end
			if titleparts_categories[i1]['legende'] and (titleparts_categories[i1]['elevated'] or titleparts_categories[i1]['cutting'] or titleparts_categories[i1]['embankment']) then
				titleparts_categories[i1]['legende'] = nil
				titleparts_categories[i1]['formations'] = true
				if titleparts_categories[i1]['BHF'] or titleparts_categories[i1]['HST'] then
					titleparts_categories[i1]['BHF'] = nil
					titleparts_categories[i1]['HST'] = nil
					titleparts_categories[i1]['stations'] = true
				end
			end
			if titleparts_categories[i1]['3'] then
				titleparts_categories[i1]['parallel lines'] = nil
				if not titleparts_categories[i1]['junction'] then titleparts_categories[i1]['curve'] = true end
			elseif titleparts_categories[i1]['k'] then
				if not (titleparts_categories[i1]['junction'] or titleparts_categories[i1]['wye']) then
					tmp_match = mw.ustring.match(v1, '|[A-Z][A-Z%+]+[1-3]?|([1-4lrfgm]?%+[1-4lrfgm])|') or mw.ustring.match(v1, '|[A-Z][A-Z%+]+[1-3]?|([1-4lrfgm]%+[1-4lrfgm]?)|') or ''
					if mw.ustring.match(tmp_match, '[1-4]') then titleparts_categories[i1]['curve'] = true end
				end
			elseif titleparts_categories[i1]['shift'] then
				tmp_match = mw.ustring.match(v1, '|SHI%d|(g?[lr]?[lr]?%+?[lr]?[lr]?q?)|') or ''
				if mw.ustring.match(tmp_match, 'g') or mw.ustring.match(tmp_match, 'lr') or mw.ustring.match(tmp_match, 'rl') then
					titleparts_categories[i1]['junction'] = true
					if mw.ustring.match(tmp_match, 'l.*%+.*l') or mw.ustring.match(tmp_match, 'r.*%+.*r') then
						titleparts_categories[i1]['crossing'] = true
					end
					if titleparts_categories[i1]['one quarter'] and (mw.ustring.match(tmp_match, 'lr') or mw.ustring.match(tmp_match, 'rl')) then
						titleparts_categories[i1]['split'], titleparts_categories[i1]['junction'] = true, nil
					end
				elseif mw.ustring.match(tmp_match, 'l.*%+.*l') or mw.ustring.match(tmp_match, 'r.*%+.*r') then
					titleparts_categories[i1]['crossing'] = true
				end
			elseif titleparts_categories[i1]['split'] then
				tmp_match = mw.ustring.match(v1, '|SPL|([^|]+)|') or ''
				if mw.ustring.match(tmp_match, '[1-4]') then
					titleparts_categories[i1]['uw'] = true
				elseif mw.ustring.match(tmp_match, '^a[lr]%+q') or mw.ustring.match(tmp_match, '^a%+[lr]%+g') or mw.ustring.match(tmp_match, '^e[lr]%+g') or mw.ustring.match(tmp_match, '^e%+[lr]%+q') or mw.ustring.match(tmp_match, '^[ae]g%+?[lr][lr]?q?$') or  mw.ustring.match(tmp_match, '^[ae]q?$') then
					titleparts_categories[i1]['shift'] = true
					titleparts_categories[i1]['one quarter'] = true
				end
			elseif mw.ustring.match(v1, '|ÜWB|') then
				if titleparts_categories[i1]['parallel lines'] then
					tmp_match = mw.ustring.match(v1, '|ÜWB|[lr]?%+([lr]?[lr]?)q?|') or ''
					if mw.ustring.match(tmp_match, '[lr]') then
						titleparts_categories[i1]['junction'] = true
					end
					titleparts_categories[i1]['shift'] = true
					tmp_match = mw.ustring.len(mw.ustring.gsub((mw.ustring.match(v1, '|v|[v|?]*') or ''), '|', ''))
					if tmp_match > 1 then
						if tmp_match == 2 then
							titleparts_categories[i1]['four quarters'] = true
						elseif tmp_match == 3 then
							titleparts_categories[i1]['six quarters'] = true
						elseif tmp_match == 4 then
							titleparts_categories[i1]['eight quarters'] = true
						end
					else
						titleparts_categories[i1]['two quarters'] = true
					end
					titleparts_categories[i1]['crossing'] = true
				else
					titleparts_categories[i1]['track change'] = true
				end
			else
				if titleparts_categories[i1]['junction'] or titleparts_categories[i1]['wye'] then -- add other junctions?
					if mw.ustring.match(v1, '|[A-ZÜ]+|[^%-ck]*[1234]') then
						titleparts_categories[i1]['uw'] = true
						if mw.ustring.match(v1, '|u+|') then
							titleparts_categories[i1]['corner'] = true
						end
						if titleparts_categories[i1]['parallel lines'] and not (mw.ustring.match(v1, '|%-|')) then
							titleparts_categories[i1]['double'] = true
						end
					elseif titleparts_categories[i1]['junction'] and not titleparts_categories[i1]['wye'] then
						tmp_match = mw.ustring.match(v1, '|[A-Z][A-Z%+]+[1-3]?|([gq][lr]?[lr]?%+?[lr]?[lr]?)|') or ''
						if tmp_match ~= '' then
							if tmp_match == 'gl+l' or tmp_match == 'gr+r' or tmp_match == 'qlr' or tmp_match == 'qrl' or tmp_match == 'q+lr' or tmp_match == 'q+rl' then
								titleparts_categories[i1]['wye'] = true
							end
						end
					end
				else
					tmp_match = mw.ustring.match(v1, '|[A-Z][A-Z%+]+[1-3]?|([1-4lrfgm]?%+[1-4lrfgm])|') or mw.ustring.match(v1, '|[A-Z][A-Z%+]+[1-3]?|([1-4lrfgm]%+[1-4lrfgm]?)|') or ''
					if mw.ustring.match(tmp_match, '[1-4]') then
						titleparts_categories[i1]['uw'] = true
						if not ((titleparts_categories[i1]['half-width'] and mw.ustring.match(tmp_match, 'm'))
							or ((not mw.ustring.match(v1, '^[#ufgelhatpnk|]*|[ocdbsw]')) and (tmp_match == '3+1' or tmp_match == '2+4' or tmp_match == '1+3' or tmp_match == '4+2'))
							or mw.ustring.match(v1, '|K|[^|]+|[1-4]%+||$') or mw.ustring.match(v1, '|K|[^|]+|[1-4]%+||[~@][fglmrFGLMR]')
							or mw.ustring.match(v1, '|CONT|[1-4]%+||$') or mw.ustring.match(v1, '|CONT|[1-4]%+||[~@][fglmrFGLMR]')
							or mw.ustring.match(v1, '|ENDE|[1-4]%+||$') or mw.ustring.match(v1, '|ENDE|[1-4]%+||[~@][fglmrFGLMR]')) then
							titleparts_categories[i1]['curve'] = true
							if titleparts_categories[i1]['parallel lines'] and not (mw.ustring.match(v1, '|%-|')) then
								titleparts_categories[i1]['double'] = true
							end
						end
						if mw.ustring.match(v1, '|u+|') and not (mw.ustring.match(v1, '|KR[XZ]|') or mw.ustring.match(v1, '|T|')) then
							titleparts_categories[i1]['corner'] = true
						end
					elseif mw.ustring.match(tmp_match, '^[fg]?%+[lr]$') or mw.ustring.match(tmp_match, '^[lr]%+[fg]?$') or mw.ustring.match(tmp_match, '^[lr]%+[lr]$') then
						titleparts_categories[i1]['curve'] = true
					elseif mw.ustring.match(tmp_match, '^[fg]%+$') and not (titleparts_categories[i1]['continuation'] or titleparts_categories[i1]['line endings']) then
						titleparts_categories[i1]['direction'] = true
					end
				end
			end
			if titleparts_categories[i1]['corner'] and not (titleparts_categories[i1]['3'] or titleparts_categories[i1]['k'] or titleparts_categories[i1]['shift'] or titleparts_categories[i1]['krw']) then titleparts_categories[i1]['uw'] = true end
			if titleparts_categories[i1]['tunnel'] and not mw.ustring.match(v1, '|K|') and not ((titleparts_categories[i1]['continuation'] or titleparts_categories[i1]['line endings']) and not titleparts_categories[i1]['uw']) and not titleparts_categories[i1]['split'] then
				if mw.ustring.match(v1, '[A-ZÜ][A-ZÜ]+[^%-%+~]*|[ae][fgae]?|') then titleparts_categories[i1]['portal'] = true end
			end
		end
		
		for i, v in ipairs(titleparts_categories) do
			for k2, v2 in pairs(v) do
				category[k2] = true
			end
		end
		
		for i, v in pairs({
			["BHF+DST"] = {"BHF", "DST"},
			["BHF+HST"] = {"BHF", "HST"},
			["HST+BST"] = {"HST", "BST"},
			["wye"] = {"wye", "junction"},
			["curve+corner"] = {"curve", "corner"},
			["crossing+junction"] = {"crossing", "junction"},
			["crossing+corner"] = {"crossing", "corner"},
			["crossing+wye"] = {"crossing", "wye"},
			}) do
			if category[v[1]] and category[v[2]] then
				category[i] = true
				category[v[1]], category[v[2]] = nil, nil
			end
		end
		if category['corner'] then
			if mw.ustring.match(titleparts, '|%+[ck][1-4][1-4]?[1-4]?[1-4]?|') then
				category['straight+corner'] = true
				category['corner'] = nil
			end
		end
		
		local order = {
			'Category:BSicon',
			
			'railway',
			'road–rail',
			'road',
			'water',
			
			'set u',
			'set f',
			'set g',
			'set mixed',
			'set azure',
			'set black',
			'set blue',
			'set brown',
			'set carrot',
			'set cerulean',
			'set cyan',
			'set deepsky',
			'set denim',
			'set fuchsia',
			'set golden',
			'set green',
			'set grey',
			'set jade',
			'set lavender',
			'set lime',
			'set maroon',
			'set ochre',
			'set olive',
			'set orange',
			'set pink',
			'set purple',
			'set red',
			'set ruby',
			'set saffron',
			'set sky',
			'set steel',
			'set teal',
			'set violet',
			'set white',
			'set yellow',
			'mixed',
			'set u-f',
			'set u-g',
			'set black-orange',
			'set green-yellow',
			'set saffron-azure',
			'set yellow-blue',
			'generic road',
			'RP4',
			'RP2',
			'RP1',
			'RD',
			'RA',
			'RB',
			'RE',
			'RG',
			'RM',
			'RR',
			'RY',
			
			'quarter-width',
			'half-width',
			'three-quarter-width',
			'double-width',
			'parallel lines',
			
			'legende',
			'formations',
			'mask',
			'elevated',
			'tunnel',
			'portal',
			'cutting',
			'embankment',
			'interruption',
			'narrow',
			'3',
			'k',
			'uw',
			'double',
			'shift',
			'one quarter',
			'two quarters',
			'three quarters',
			'four quarters',
			'krw', -- deprecated?
			'five quarters',
			'six quarters',
			'seven quarters',
			'eight quarters',
			
			'crossing',
			'crossing+junction',
			'crossing+corner',
			'junction',
			'straight+junction',
			'split',
			'wye',
			'line endings',
			'continuation',
			'loop',
			'border', -- determine exact order
			'customs', -- determine exact order
			'milepost',
			'platform',
			'stations',
			'suburban',
			'BHF',
			'HST',
			'DST',
			'BST',
			'INT',
			'ACC',
			'SBHF',
			'SHST',
			'S+BHF',
			'S+HST',
			'HSTACC',
			'INTACC',
			'BHF+DST',
			'BHF+HST',
			'HST+BST',
			
			'hub',
			'limited',
			'interchange',
			'CPIC',
			'terminus',
			
			'direction',
			'curve',
			'corner',
			'straight+curve',
			'straight+corner',
			'curve+corner',
		}
		for k, v in ipairs(order) do
			if category[v] then table.insert(result, v) end
		end
		result = table.concat(result, '/')
		-- return (mw.dumpObject(result) or '')..'<hr>'..(mw.dumpObject(titleparts) or '')..'<hr>'..(mw.dumpObject(titleparts_table) or '')
	end
	return result
end

return p