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 and stops/',
		HST = 'stations and stops/',
		DST = 'stations and stops/',
		BST = 'stations and stops/',
		INT = 'stations and stops/',
		ACC = 'stations and stops/',
		INTACC = 'stations and stops/',
		HSTACC = 'stations and stops/',
		suburban = 'stations and stops/',
		SBHF = 'stations and stops/',
		SHST = 'stations and stops/',
		['S#BHF'] = 'stations and stops/',
		['S#HST'] = 'stations and stops/',
		['BHF#DST'] = 'stations and stops/',
		['BHF#HST'] = 'stations and stops/',
		['HST#BST'] = 'stations and stops/',
		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/',
	}
	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 stops' 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, '([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, '|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')
		-- 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]*)|%)|', '|(%1%2%3%4%5%6)|')
		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|')
		-- 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|')
		-- Add handling for icon names containing more than one root
		-- Affixes always treated the same
		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',
			['|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',
			['|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(titleparts, k) then category[v] = true end
		end
		if category['set mixed'] and category['set u'] and not category['other colors'] then category['set u'] = nil end
		if category['legende'] and (category['elevated'] or category['cutting'] or category['embankment']) then
			category['legende'] = nil
			category['formations'] = true
			if category['BHF'] or category['HST'] then
				category['BHF'] = nil
				category['HST'] = nil
				category['stations and stops'] = true
			end
		end
		if category['3'] then
			category['parallel lines'] = nil
			if not category['junction'] then category['curve'] = true end
		elseif category['k'] then
			if not (category['junction'] or category['wye']) then
				tmp_match = mw.ustring.match(titleparts, '|[A-Z][A-Z%+]+[1-3]?|([1-4lrfgm]?%+[1-4lrfgm])|') or mw.ustring.match(titleparts, '|[A-Z][A-Z%+]+[1-3]?|([1-4lrfgm]%+[1-4lrfgm]?)|') or ''
				if mw.ustring.match(tmp_match, '[1-4]') then category['curve'] = true end
			end
		elseif category['shift'] then
			tmp_match = mw.ustring.match(titleparts, '|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
				category['junction'] = true
				if mw.ustring.match(tmp_match, 'l.*%+.*l') or mw.ustring.match(tmp_match, 'r.*%+.*r') then
					category['crossing'] = true
				end
				if category['one quarter'] and (mw.ustring.match(tmp_match, 'lr') or mw.ustring.match(tmp_match, 'rl')) then
					category['split'], category['junction'] = true, nil
				end
			elseif mw.ustring.match(tmp_match, 'l.*%+.*l') or mw.ustring.match(tmp_match, 'r.*%+.*r') then
				category['crossing'] = true
			end
		elseif category['split'] then
			tmp_match = mw.ustring.match(titleparts, '|SPL|([^|]+)|') or ''
			if mw.ustring.match(tmp_match, '[1-4]') then
				category['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
				category['shift'] = true
				category['one quarter'] = true
			end
		elseif mw.ustring.match(titleparts, '|ÜWB|') then
			if category['parallel lines'] then
				tmp_match = mw.ustring.match(titleparts, '|ÜWB|[lr]?%+([lr]?[lr]?)q?|') or ''
				if mw.ustring.match(tmp_match, '[lr]') then
					category['junction'] = true
				else
					category['curve'] = true
				end
				category['shift'] = true
				tmp_match = mw.ustring.len(mw.ustring.gsub((mw.ustring.match(titleparts, '|v|[v|?]*') or ''), '|', ''))
				if tmp_match > 1 then
					if tmp_match == 2 then
						category['four quarters'] = true
					elseif tmp_match == 3 then
						category['six quarters'] = true
					elseif tmp_match == 4 then
						category['eight quarters'] = true
					end
				else
					category['two quarters'] = true
				end
				category['crossing'] = true
			else
				category['track change'] = true
			end
		else
			if category['junction'] or category['wye'] then -- add other junctions?
				if mw.ustring.match(titleparts, '|[A-ZÜ]+|[^%-ck]*[1234]') then
					category['uw'] = true
					if mw.ustring.match(titleparts, '|u+|') then
						category['corner'] = true
					end
					if category['parallel lines'] and not (mw.ustring.match(titleparts, '|%-|')) then
						category['double'] = true
					end
				elseif category['junction'] and not category['wye'] then
					tmp_match = mw.ustring.match(titleparts, '|[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
							category['wye'], category['junction'] = true, nil
						end
					end
				end
			else
				tmp_match = mw.ustring.match(titleparts, '|[A-Z][A-Z%+]+[1-3]?|([1-4lrfgm]?%+[1-4lrfgm])|') or mw.ustring.match(titleparts, '|[A-Z][A-Z%+]+[1-3]?|([1-4lrfgm]%+[1-4lrfgm]?)|') or ''
				if mw.ustring.match(tmp_match, '[1-4]') then
					category['uw'] = true
					if not ((category['half-width'] and mw.ustring.match(tmp_match, 'm'))
						or ((not mw.ustring.match(titleparts, '^[#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(titleparts, '|K|[^|]+|[1-4]%+||$') or mw.ustring.match(titleparts, '|K|[^|]+|[1-4]%+||[~@][fglmrFGLMR]')
						or mw.ustring.match(titleparts, '|CONT|[1-4]%+||$') or mw.ustring.match(titleparts, '|CONT|[1-4]%+||[~@][fglmrFGLMR]')
						or mw.ustring.match(titleparts, '|ENDE|[1-4]%+||$') or mw.ustring.match(titleparts, '|ENDE|[1-4]%+||[~@][fglmrFGLMR]')) then
						category['curve'] = true
						if category['parallel lines'] and not (mw.ustring.match(titleparts, '|%-|')) then
							category['double'] = true
						end
					end
					if mw.ustring.match(titleparts, '|u+|') and not (mw.ustring.match(titleparts, '|KR[XZ]|') or mw.ustring.match(titleparts, '|T|')) then
						category['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
					category['curve'] = true
				elseif mw.ustring.match(tmp_match, '^[fg]%+$') and not (category['continuation'] or category['line endings']) then
					category['direction'] = true
				end
			end
		end
		if category['corner'] and not (category['3'] or category['k'] or category['shift'] or category['krw']) then category['uw'] = true end
		if category['tunnel'] and not mw.ustring.match(titleparts, '|K|') and not ((category['continuation'] or category['line endings']) and not category['uw']) and not category['split'] then
			if mw.ustring.match(titleparts, '[A-ZÜ][A-ZÜ]+[^%-%+~]*|[ae][fgae]?|') then category['portal'] = true end
		end
		
		if category['crossing'] and category['junction'] then
			category['crossing+junction'] = true
			category['crossing'], category['junction'] = nil, nil
		end
		if category['corner'] then
			if category['curve'] then
				category['curve+corner'] = true
				category['curve'], category['corner'] = nil, nil
			elseif 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',
			'stations and stops',
			'suburban',
			'BHF',
			'HST',
			'DST',
			'BST',
			'INT',
			'ACC',
			'SBHF',
			'SHST',
			'S+BHF',
			'S+HST',
			'HSTACC',
			'INTACC',
			
			'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 '').."\n\n"..(mw.dumpObject(titleparts) or '').."\n\n"..(mw.dumpObject(test) or '')
	end
	return result
end

return p