Module:String/Nano

From IdleOn MMO Wiki

Documentation for this module may be created at Module:String/Nano/doc

--<pre>--------------------------------------------------------------------------
-- This module houses helper functions for use on strings. In general this module
-- is loaded as the "string" variable in other modules for conistency.
-- Most string functions will accept a `table` or `number` besides a string
-- using `parseDualArg()`.
-- See the comments on the functions for futher details.
-- The `stringable` type repersents the types `string, `table`, and `number`.
--
----------------[ CONTENTS ]-----------------
-- * function: split(text: string, pattern: string, plain?: boolean)
-- * function: gsplit(text: string, pattern: string, plain?: boolean)
-- * function: matchNum(s: string, pattern: string, pos?: number|boolean, escape?: boolean)
-- * function: charAt(s: string, pos?: number)
-- * function: styleString(s, css, classes, attrs)
-- * function: escape(s: string)
-- * function: unescape(s: string)
-- * function: includes(s: string, pattern: string, pos?: number|boolean, escape?: boolean)
-- * function: codePointAt(s: string, pos?: number)
-- * function: concat(s: string, ...items: string)
-- * function: indexOf(s: string, searcher: string, pos?: string|boolean, escape?: boolean)
-- * function: lastIndexOf(s: string, searcher: string, pos?: string|boolean, escape?: boolean)
-- * function: endsWith(s: string, searcher: string, pos?: string)
-- * function: startsWith(s: string, searcher:string, pos?: string)
-- * function: matchAll(s: string, pattern: string, escape?: boolean)
-- * function: allMatched(s: string, t: table)
-- * function: anyMatched(s: string, t: table)
-- * function: gsubAll(s: string, ...)
-- * function: gsubMulti(s: string, replacements: table)
-- * function: orderedFormat(formatString: string, ...subsitutions)
-- * function: ucfirst(s: string)
-- * function: lcfirst(s: string)
-- * function: toCamelCase(s: string)
-- * function: bold(s: stringable)
-- * function: italic(s: stringable)
-- * function: underline(s: stringable)
-- * function: reverseSymbol(s: string, reverse?: boolean)
-- * function: pcall(f: function [function to call], ...params?: any [function parameters])
-- * function: makeImage(name: string | table, table, options: table)
-- * function: makeTitle(s: string, title: string [the title to make the element with], options: table)
-- * function: parseDualArg(arg: stringable [argument to parse])
-- * function: wrapLink(val: string, dest: string)
-- * function: parseUrlQuery(s: string | table [query to parse])
-- * function: isStringable(v: any [value to check])
-- * function: urlQueryToString(s: table [query table to parse], url?: string | table)
-- * function: getUrlParam(url: string | table, param: string, default: any)
-- * function: externalUrl(url: stringable, query: string | table, alt?: stringable)
-- * function: stringUtil(s: stringable)
-- * function: fullUrl(page: string, query: string | table, alt: string | table)
-- * function: _formatShortNumber(number: string|number)
-- * function: _toNumber(str: string)
-- * function: error(msg: string, ...<string.format() arguments>)
-- * function: preprocess(val: string)
-- * function: centerText(s: string)
-- * function: trimWhitespace(s: string)
-- * function: toRoman(s: string)
-- * function: toArabic(s: string)
-- * function: roundNumber(num: number, posistion: number)
-- * function: sublength(frame: table)
-- * function: matchTemplate(frame: table)
----------------[ TODO ]-----------------
-- *None yet.
---------------------------------------------------------------------------------
local p = {}
local lang = mw.getContentLanguage()

local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
local libUtil = require('Module:LibraryUtil')
local table = require('Module:Table')
local checkType, checkArgs = libUtil.checkTypeLight, libUtil.checkArgs

-- Load string library if the module is loaded as "string" in another module
p.rep = string.rep
p.gsub = string.gsub
p.len = string.len
p.sub = string.sub
p.char = string.char
p.gmatch = string.gmatch
p.lower = string.lower
p.upper = string.upper
p.find = string.find
p.match = string.match
p.reverse = string.reverse
p.byte = string.byte
p.format = string.format

---------------------------------------------------------------------------------
-- function: .split(text: string, pattern: string, plain?: boolean)
-- 
-- Breaks up the text according to the pattern specified. `plain` may be specified
-- to make the pattern be treated as raw text.
---------------------------------------------------------------------------------
function p.split(...)
	local text, pattern, plain = checkArgs({ 'string', 'string', { 'boolean', nilOk = true } }, ...)
	local ret = {}
	
	for m in p.gsplit(tostring(text), pattern, plain) do
		ret[#ret+1] = m
	end
	
	return ret
end

---------------------------------------------------------------------------------
-- function: .unpackedSplit(text: string, pattern: string, plain?: boolean)
-- 
-- Same as `.split()` execpt it returns an unpacked table instead of a regular one.
---------------------------------------------------------------------------------
function p.unpackedSplit(...)
	return unpack(p.split(checkArgs({ 'string', 'string', { 'boolean', nilOk = true } }, ...)))
end

---------------------------------------------------------------------------------
-- function: .gsplit(text: string, pattern: string, plain?: boolean)
-- 
-- Returns an iterator function which iterates over the indices of the split string.
---------------------------------------------------------------------------------
function p.gsplit(...)
	local text, pattern, plain = checkArgs({ 'string', 'string', { 'boolean', nilOk = true } }, ...)
	text = tostring(text)
	local s, l = 1, text:len()
	
	return function()
		if s then
			local e, n = text:find(pattern, s, plain)
			local ret
			if not e then
				ret = text:sub(s)
				s = nil
			elseif n < e then
				-- Empty separator!
				ret = text:sub(s, e)
				if e < l then
					s = e + 1
				else
					s = nil
				end
			else
				ret = e > s and text:sub(s, e-1) or ''
				s = n + 1
			end
			return ret
		end
	end
end
---------------------------------------------------------------------------------
-- function: .matchNum(s: string, pattern: string, pos?: number|boolean, escape?: boolean)
--
-- Gets the number of all matches in `s` from `pattern`.
-- If no matches are found, it returns -1.
---------------------------------------------------------------------------------
function p.matchNum(...)
	local s, pattern, pos, escape = checkArgs({
		'string', 'string', { 'number', 'boolean', nilOk = true }, { 'boolean', nilOk = true } 
	}, ...)
	
	local t = {}
	
	if type(pos) == 'boolean' then 
		escape = pos
		pos = nil
	end
	
	for match in s:gmatch(pattern) do
		table.insert(t, match)
	end
	
	return #t == 0 and -1 or #t
end
---------------------------------------------------------------------------------
-- function: .dedent(s: string, isSpaces?: boolean)
-- 
-- Removes uneeded indentation from `s`. Supports both spaces and Tabs.
---------------------------------------------------------------------------------
function p.dedent(s, isSpaces)
	checkTypeLight('dedent', 1, s, 'string')
	checkTypeLight('dedent', 2, isSpaces, 'boolean', true)
	
	s = s:gsub('^[ \n\r\v]*(.-)[ \n\r\v]*$', '%1'):gsub('\\([%a\"\'%d]+)', {
		['t'] = '\t',
		['n'] = '\n',
		['r'] = '\r',
		['v'] = '\v',
		['f'] = '\f',
		['a'] = '\a',
		['b'] = '\b',
		['"'] = '\"',
		['\''] = '\'',
	})
	local lines = mw.text.split(s, '\n')
	local overallLen = {}
	local isSpaces = true
	
	-- Check spaces
	for i, v in ipairs(lines) do
		local _, num = v:find('^\t+')
		if num then
			isSpaces = false
		end
	end
	
	for i, v in ipairs(lines) do
		local _, num
		
		if isSpaces then
			_, num = v:find('^ +')
		else
			_, num = v:find('^\t+')
		end
		num = isSpaces and num/4 or num
		table.insert(overallLen, #overallLen+1, num)
	end
	local overallLen = math.min(unpack(overallLen) or 0)
	
	for i, v in ipairs(lines) do
		lines[i] = v:gsub('^' .. (not isSpaces and ('\t'):rep(overallLen) or (' '):rep(overallLen*4)), '')
	end
	return table.concat(lines, '\n')
end

---------------------------------------------------------------------------------
-- function: remove(s: string, pattern: string|table, index?: number)
-- 
-- Removes the specified match from the string and returns it.
--------------------------------------------------------------------------------
function p.remove(...)
	local s, pattern, index = checkArgs({ 'string', { 'string', 'table' }, { 'number', nilOk = true } }, ...)
	local oldString = s
	local i, match = 1
	
	if type(pattern) == 'table' then
		while not match do
			if i > #pattern then
				match = {}
				break
			end
			match = oldString:match(pattern[i], index)
			
			if match then
				s = s:gsub(pattern[i], '', 1)
				match = { oldString:match(pattern[i], index) }
			end
			i = i+1
		end
	else
		match = { oldString:match(pattern, index) }
		s = s:gsub(pattern, '', 1)
	end
	
	return unpack{ s, unpack(match) }
end

---------------------------------------------------------------------------------
-- function: .charAt(s: string, pos?: number)
--
-- Returns the character `s` at the posistion of `pos`. `pos` will default to 1. 
-- `pos` maybe fed a negative number to count from the rear of `s`.
---------------------------------------------------------------------------------
function p.charAt(...)
	local s, pos = checkArgs({ 'string', { 'number', nilOk = true } }, ...)
	
	pos = pos and -(-pos) or 1
	if pos < 0 then
		pos = #s+pos
	elseif pos == 0 then 
		pos = 1
	end
	
	return (mw.ustring.isutf8(s) and mw.ustring or string).sub(s, pos, pos)
end

---------------------------------------------------------------------------------
-- function: .charAt(s: string, pos?: number)
--
-- Returns the character `s` at the posistion of `pos`. `pos` will default to 1. 
-- `pos` maybe fed a negative number to count from the rear of `s`.
---------------------------------------------------------------------------------
function p.charCodeAt(...)
	local s, pos, useHex = checkArgs({ 'string', { 'number', nilOk = true }, { 'boolean', nilOk = true } }, ...)
	s = p.charAt(s, pos)
	
	local ret = (mw.ustring.isutf8(s) and mw.ustring.codepoint or string.byte)(s) or -1
	
	return useHex and p.toHex(ret) or ret
end

---------------------------------------------------------------------------------
-- function: .toHex(num: number)
-- 
-- Converts a number to a hex number repersentation.
---------------------------------------------------------------------------------
function p.toHex(...)
	local num = checkArgs({ { 'number', 'string', base = 16 } }, ...)
	
	return string.format('%x', type(num) == 'string' and tonumber(num, 16) or num)
end

---------------------------------------------------------------------------------
-- function: .unicodeConvert(s: string)
-- 
-- Turns unicode encodings like `\u0F303`, `\u+0F303`, and `\u{FFFFF}` to actual characters.
---------------------------------------------------------------------------------
function p.unicodeConvert(...)
	local s = checkArgs('string', ...)
	
	function replUnicode(code)
		local success, res = pcall(mw.ustring.char, tonumber(code, 16))
		if not success then formattedError('The unicode codepoint "\\u{%x}" is out of range', 4, p.toHex(code)) end
		return res
	end
		
	return (s:gsub('\\u%+?(%x%x?%x?%x?)', replUnicode):gsub('\\u%{%+?(%x+)%}', replUnicode))
end

---------------------------------------------------------------------------------
-- function: .styleString(s, css, classes, attrs)
-- 
-- Styles text according to the css properties provided.
---------------------------------------------------------------------------------
function p.styleString(...)
	local s, css, classes, attrs = checkArgs({ { 'string', numberOk = true }, 'table', { 'table', nilOk = true }, { 'table', nilOk = true } }, ...)
	
	return p.wrapHtml(s, '<font>', table.merge({ style = css, class = classes }, attrs))
end

---------------------------------------------------------------------------------
-- function: .escape(s: string)
--
-- Escapes any special characters in `s` that are used in patterns.
---------------------------------------------------------------------------------
function p.escape(...)
	local s, skipPow = checkArgs({ 'string', { 'boolean', nilOk = true } }, ...)
	
	local pat = '([%*%+%-%(%)%[%]%%%$%^%?%.])'
	if skipPow then
		pat = pat:gsub('%%%^', '')
	end
	
	return (s:gsub(pat, function(m) return '%' .. m end))
end

---------------------------------------------------------------------------------
-- function: .unescape(s: string)
--
-- Unescapes any special characters in `s` that are used in patterns.
---------------------------------------------------------------------------------
function p.unescape(...)
	local s, skipPow = checkArgs({ 'string', { 'boolean', nilOk = true } }, ...)
	
	local pat = '(%%([%*%+%-%(%)%[%]%%%$%^%?%.]))'
	if skipPow then
		pat = pat:gsub('%%%^', '')
	end
	
	return (s:gsub(pat, '%2'))
end

---------------------------------------------------------------------------------
-- function: .includes(s: string, pattern: string, pos?: number|boolean, escape?: boolean)
--
-- Checks if `s` has `pattern` inside of it. `pos` can be set to where to start searching.
---------------------------------------------------------------------------------
function p.includes(...)
	local s, pattern, pos, escape = checkArgs({ 
		'string', 'string', { 'number', 'boolean', nilOk = true }, { 'boolean', nilOk = true } 
	}, ...)
	
	if type(pos) == 'boolean' then 
		escape = pos
		pos = nil
	end
	
	pos = pos or 1
	
	return not not s:match(escape and p.escape(pattern) or pattern, pos)
end

---------------------------------------------------------------------------------
-- function: .codePointAt(s: string, pos?: number)
--
-- Gets the codepoint for the character at `pos`.
---------------------------------------------------------------------------------
function p.codePointAt(...)
	local s, pos = checkArgs({ 'string', { 'number', nilOk = true } }, ...)
	
	local success, res = pcall(mw.ustring.isutf8(s) and mw.ustring.codepoint or string.byte, p.charAt(s, pos))
	
	if not success then
		error(res, 2)
	end
	
	return res
end

---------------------------------------------------------------------------------
-- function: .concat(s: string, ...items: string)
--
-- Appends each string to `s`.
---------------------------------------------------------------------------------
function p.concat(...)
	checkArgs({ 'string' }, ...)
	
	local ret = {}
	for i, v in forEachArgs({ 'any', startIndex = 1 }, ...) do
		v = tostring(v)
		table.push(ret, v)
	end
	
	return table.concat(ret)
end

---------------------------------------------------------------------------------
-- function: .indexOf(s: string, searcher: string, pos?: string|boolean, escape?: boolean)
--
-- Gets the index of the found string in `s` found by `searcher`.
---------------------------------------------------------------------------------
function p.indexOf(...)
	local s, searcher, pos, escape = checkArgs({ 
		{ 'string', numberOk = true }, 'string', { 'number', 'boolean', nilOk = true, }, { 'boolean', nilOk = true } 
	}, ...)
	
	if type(pos) == 'boolean' then 
		escape = pos
		pos = nil
	end
	
	pos = pos or 1
	local index, endsAt = s:find(searcher, pos, escape)
	
	return index or -1
end

---------------------------------------------------------------------------------
-- function: .lastIndexOf(s: string, searcher: string, pos?: string|boolean, escape?: boolean)
--
-- Gets the last index of the found string in `s` found by `searcher`.
---------------------------------------------------------------------------------
function p.lastIndexOf(...)
	local s, searcher, pos, escape = checkArgs({ 
		'string', 'string', { 'number', 'boolean', nilOk = true }, { 'boolean', nilOk = true } 
	}, ...)
	
	local index = p.indexOf(s:reverse(), searcher, pos and -pos-1, escape)
	
	if index ~= -1 then
		return -(index-#s)
	else
		return index
	end
end

---------------------------------------------------------------------------------
-- function: .endsWith(s: string, searcher: string, pos?: string)
--
-- Checks if `s` ends with `searcher`. Begins searching at `pos` in `s`.
---------------------------------------------------------------------------------
function p.endsWith(...)
	local s, searcher, pos = checkArgs({ 'string', 'string', { 'number', nilOk = true } }, ...)
	
	return not not s:match(p.escape(searcher) .. '$', pos)
end

---------------------------------------------------------------------------------
-- function: .startsWith(s: string, searcher: string, pos?: string)
--
-- Checks if `s` starts with `searcher`. Begins searching at `pos` in `s`.
---------------------------------------------------------------------------------
function p.startsWith(...)
	local s, searcher, pos = checkArgs({ 'string', 'string', { 'number', nilOk = true } }, ...)
	
	return not not s:match('^' .. p.escape(searcher), pos)
end

---------------------------------------------------------------------------------
-- function: .matchAll(s: string, pattern: string, escape?: boolean)
--
-- Gets all matches of `s` with `string.gmatch()` with `pattern` as the matcher.
---------------------------------------------------------------------------------
function p.matchAll(...)
	local s, pattern, escape = checkArgs({ 'string', 'string',{ 'boolean', nilOk = true } }, ...)
	
	pattern = escape and p.escape(pattern) or pattern
	
	local ret
	local n = p.matchNum(s, pattern, escape)
	local mt = {}
	
	local function log()
		return mw.logObject(ret) or ret	
	end
	
	ret = setmetatable({ n = n, origPattern = pattern, input = s }, {
		__index = {
			print = log,
			log = log,
			dump = function()
				return mw.dumpObject(ret)
			end,
		}
	})
	
	local function callMatcher(f, i)
		return (function(...)
			local t = { ... }
			t.index = i
			t.n = table.pack(...).n
			
			return t
		end)(f())
	end
	
	local matcher = s:gmatch(escape and p.escape(pattern) or pattern)
	
	for i = 1, n do
		local res = callMatcher(matcher, i)
		table.insert(ret, res)
	end
	
	return ret
end

---------------------------------------------------------------------------------
-- function: allMatched(s: string, t: table)
-- 
-- Attempt to match each pattern in the table/subsequent args,
-- then return true if all matches exists
---------------------------------------------------------------------------------
function p.allMatched(s, ...)
	checkType('allMatched', 1, s, 'string')
	
	local t = { ... }
	if type(t[1]) == 'table' then
		t = t[1]
	end
	
	for _, v in ipairs(t) do
		if not(s:match(v)) then
			return false
		end
	end
	
	return true
end

---------------------------------------------------------------------------------
-- function: anyMatched(s: string, t: table)
-- 
-- Attempt to match each pattern in the table/subsequent args,
-- returns true if any match exists
---------------------------------------------------------------------------------
function p.anyMatched(s, ...)
	checkType('anyMatched', 1, s, 'string')
	
	local t = { ... }
	if t[1] == 'table' then
		t = t[1]
	end
	
	for _, v in ipairs(t) do
		if s:match(v) then
			return true
		end
	end
	
	return false
end

---------------------------------------------------------------------------------
-- function: gsubAll(s: string, ...)
-- 
-- For each pair of values in subsequent args, get pattern and replacement string
-- Apply all replacements on a string
---------------------------------------------------------------------------------
function p.gsubAll(s, ...)
	checkType('gsubAll', 1, s, 'string')
	
	local t, i = { ... }, 1
	if t[1] == 'table' then
		t = t[1]
	end
	t = table.flat(t)
	if #t % 2 ~= 0 then
		error('[gsubAll] Pattern/Replacement pairs mismatch (Odd number arguments)')
	end
	while i * 2 <= #t do
		s = s:gsub(t[i*2-1], t[i*2])
		i = i + 1
	end
	
	return s
end

---------------------------------------------------------------------------------
-- function: gsubMulti(s: string, replacements: table)
-- 
-- Takes multiple patterns and replacements in a table and calls `:gsub()` on `s`.
---------------------------------------------------------------------------------
function p.gsubMulti(...)
	local s, replacements = checkArgs({ 'string', 'table', { 'number', nilOk = true } }, ...)
	
	for search, repl in pairs(replacements) do
		s = s:gsub(search, type(search) == 'number' and '' or repl)	
	end
	
	return s
end

---------------------------------------------------------------------------------
-- function: .orderedFormat(formatString: string, ...subsitutions)
-- 
-- Works like `string.format()`, execpt the formatters work with ordered arguments.
-- Directives are exactly the same as `string.format()`.
---------------------------------------------------------------------------------
function p.orderedFormat(...)
	local s = checkArgs({ 'string' }, ...)
	
	local subsitutions = table.tableUtil()
	local nArgs = select('#', ...)
	local match = p.matchAll('\127' .. s, '[^%%]?%%([%d%.%#+%-]*)(%w-)(%d)%{?([^}]+)%}?')
	local args = { ... }
	local values = args
	
	if ('\127' .. s .. '\127'):gsub('%%%%', '%%%%' .. '\127'):match('[^%%]%%[^%d%w%%]') then
		formattedError('options must be escaped or be followed by an option or argument number (at string posistion #%s)', 2, ('\127' .. s .. '\127'):find('[^%%]?%%[^%d%w]'))
	elseif (s .. '\127'):gsub('%%%%', '%%%%' .. '\127'):match('%%[%a[^%d]]+') then
		formattedError('options must have a argument to subsitute from (at string posistion #%s)', 2, (s .. '\127'):find('[^%%]?%%%a+[^%d]'))
	end
	
	local optionTypes = {
		['c'] = 'number',
		['d'] = 'number',
		['i'] = 'number',
		['o'] = 'number',
		['x'] = 'number',
		['X'] = 'number',
		['e'] = 'number',
		['E'] = 'number',
		['f'] = 'number',
		['g'] = 'number',
		['G'] = 'number',
		['u'] = 'number',
		['un'] = 'number',
		['q'] = 'string',
		['s'] = 'string',
		['l'] = 'string',
		['uc'] = 'string',
		['tm'] = 'string',
		['tg'] = 'string',
		['t'] = 'table',
	}
	
	local customOptions = {
		['t'] = p.parseDualArg,
		['l'] = string.lower,
		['uc'] = string.upper,
		['tm'] = p.trim,
		['tg'] = function(value, ...)
			return p.wrapTag(value, {...})
		end,
		['un'] = function(value)
			return p.unicodeConvert('\\u{' .. -(-value) .. '}')
		end,
	}
	
	local optionsTemp = {}
	
	-- Find the greatest index first and check for bad indexes
	local greatestIndex = 0
	for i, v in ipairs(match) do
		local new = - -v[3]
		
		if greatestIndex+1 < new then
			formattedError('options must be sequential (at option #%s)', i, new)
		end
		
		if greatestIndex < new then
			greatestIndex = new
		end
	end
	
	-- Check the types of arguments, error if one is missing or is an invalid type
	for i = 1, #match do
		local index = - -match[i][3]
		local option = match[i][2]
		
		if match[i] ~= nil then
			local arg = args[i+1]
			
			if not optionTypes[option] and option ~= '' then formattedError('invalid option formatting "%%%s"', 2, option) end
			if optionsTemp[index] and optionsTemp[index] ~= option then formattedError('options may not have conflicting types (at option #%s)', 2, index) end
			optionsTemp[index] = option
			
			if i > nArgs-1 then
				checkType('no value', i+1, { optionTypes[option] or 'string' })
			end	
		end
	end
	
	for i = 1, match.n do
		if match[i] ~= nil then
			local index = match[i][3]
			local value = args[index+1]
			
			checkType(- -index+1, value, { optionTypes[match[i][2]] or 'string', numberOk = true })
			
			subsitutions:push(value)
		end
	end	
	
	local count = 0
	local s, _ = (string.gsub('\127' .. s, '([^%%]?)%%([%d%.%#+%-]*)(%w-)(%d)%{?([^{}]+)%}?', function(before, modifier, option, number, options)
			count = count+1
			if customOptions[option] then
				local t = p.split(options:gsub('\\,', '\255'), '%s*,%s*')
				local success, res 
				
				for i = 1, #t do
					t[i] = t[i]:gsub('\255', ',')
				end
				
				if t[1] == '{' and #t == 1 then
					success, res = pcall(customOptions[option], values[count+1])
				else
					success, res = pcall(customOptions[option], values[count+1], unpack(t))
				end
				
				assertTrue(success, res, 4)
				return before .. res
			else
				return before .. '%' .. modifier .. (option ~= '' and option or 's') .. '\127'
			end
		end)):gsub('\127', '')

	return s:format(unpack(subsitutions))
end

---------------------------------------------------------------------------------
-- function: parseTextList(s, phrase): table
-- 
-- Parses a text list into a table.
---------------------------------------------------------------------------------
function p.parseTextList(s, phrase, removeTrailing)
	checkType('parseTextList', 1, s, 'string')
	checkType('parseTextList', 2, phrase, 'string')
	checkType('parseTextList', 3, includeTrailing, 'boolean', true)
	
	phrase = p.trim(phrase)
	removeTrailing = removeTrailing ~= false
	s = p.trim(s):gsub('^%s*' .. phrase .. '%s*', '')
	
	if removeTrailing then
		s = s:gsub('%s*' .. phrase .. '%s*$', '')
	end
	
	local t = p.split(s, '%s*' .. p.escape(phrase) .. '+%s*')
	
	return t
end

---------------------------------------------------------------------------------
-- function: parserTag(tagName: string, t?: table)
-- 
-- 
---------------------------------------------------------------------------------
function p.parserTag(tagName, t)
	checkType('parserTag', 1, tagName, 'string')
	checkType('parserTag', 2, t, 'table', true)
	
	local buffer = {'{{#', tagName, ':'}
	
	if t then
		table.push(buffer, '\n')
		for key, val in pairs(t) do
			local isNum = tonumber(key)
			if type(val) == 'table' then
				for _, value in ipairs(val) do
					table.push(buffer, '| ', key, ' = ', value, '\n')
				end
			else
				table.push(buffer, '| ', key, ' = ', val, '\n')
			end
		end
	end
	
	table.insert(buffer, '}}')
	
	return table.concat(buffer)
end

---------------------------------------------------------------------------------
-- function: .ucfirst(s: string)
---------------------------------------------------------------------------------
function p._ucfirst(s)
	checkTypeLight('ucfirst', 1, s, 'string')
	
	if s == nil then s = '' end
	return s:gsub('(%w)([%w\']*)', function(a, b) return string.upper(a) .. string.lower(b) end)
end
---------------------------------------------------------------------------------
-- function: .lcfirst(s: string)
---------------------------------------------------------------------------------
function p._lcfirst(s)
	checkTypeLight('lcfirst', 1, s, 'string')
	if s == nil then s = '' end
	return s:gsub('(%w)([%w\']*)', function(a, b) return string.lower(a) .. string.lower(b) end)
end
p.lcfirst = p._lcfirst
p.ucfirst = p._ucfirst

--------------------------------------------------------------------------------
-- function: .toCamelCase(s: string)
--
-- converts a string to camel case
--------------------------------------------------------------------------------
function p.toCamelCase(s)
	checkTypeLight('toCamelCase', 1, s, 'string')
	
	return s
		:gsub('(%w)(%w*)', 
			function(a, b) 
				return table.concat{ 
					a:upper(), 
					b,
				} 
			end
		)
		:gsub('[^%a]+', '')
		:gsub('^(%w)(%w*)', 
			function(a, b) 
				return table.concat{ 
					a:lower(), 
					b,
				} 
			end
		)
end
---------------------------------------------------------------------------------
-- function: .bold(s: stringable)
--
-- Makes bold text
---------------------------------------------------------------------------------
function p.bold(s)
	return table.concat{ '<b>', p.parseDualArg(s), '</b>' }
end

---------------------------------------------------------------------------------
-- function: .italic(s: stringable)
--
-- Makes italic text
---------------------------------------------------------------------------------
function p.italic(s)
	return table.concat{ '<i>', p.parseDualArg(s), '</i>' }
end

---------------------------------------------------------------------------------
-- function: .underline(s: stringable)
--
-- Makes underlined text
---------------------------------------------------------------------------------
function p.underline(s)
	return table.concat{ '<u>', p.parseDualArg(s), '</u>' }
end

---------------------------------------------------------------------------------
-- function: .blankCell()
--
-- Makes a blank table cell, made for use in `<td>` tags
-- See {{Template:BlankCell}} for CSS and further details
---------------------------------------------------------------------------------
function p.blankCell()
	return p.wrapHtml('', 'div', {
		class = {'cellTemplate', 'blankCell'}, 
		title = 'This cell is intentionally left blank', 
		['data-sort-value'] = 0
	})
end
---------------------------------------------------------------------------------
-- function: .infoNeeded()
--
-- Makes info needed text
---------------------------------------------------------------------------------
function p.infoNeeded()
	return(tostring(
		mw.html.create('div')
			:tag('font')
				:attr({ color = '#910000' })
				:wikitext('More Info Needed')
			:done()
			:wikitext('[[Category:More info needed]]')
		:done()
		)
	)
end

---------------------------------------------------------------------------------
-- function: .reverseSymbol(s: string, reverse?: boolean)
--
-- Reverses the symbols in a given string
---------------------------------------------------------------------------------
function p.reverseSymbol(s, reverse)
	if type(s) ~= 'string' then error(string.format('bad argument #1 to \"reverseSymbol\": string expected, got %s', type(s)), 2) end
	local replacementSymbols = {
		['['] = ']',
		[']'] = '[',
		['('] = ')',
		[')'] = '(',
		['{'] = '}',
		['}'] = '{',
		['<'] = '>',
		['>'] = '<',
		['/'] = '\\',
		['\\'] = '/',
	}
	local s = string.gsub(s, '([%[%]{}%(%)<>\\/])', replacementSymbols)
	
	if yesno(reverse, false) then
		return s:reverse()
		else return s
	end
end

---------------------------------------------------------------------------
-- function: .makeImage(name: string | table, table, options: table)
-- 
-- Creates a wikitext image element. You can specify the file extention by adding 
-- `.<extension>` at the end of the file name.
--
------------[ OPTIONS ]----------
-- Specify one of these fields below in the `options` argument to configure the 
-- output of this function.
-- 
-- *size: ? - 
-- *extension: ? - 
-- *vertAlign: ? - 
-- *horizAlign: ? - 
-- *link: ? - 
-- *alt: ? - 
-- *page: ? - 
-- *class: ? - 
-- *noLink: ? - 
-- *format: ? - 
-- *caption: ? - 
-- *lang: ? - 
-- *upright: ? - 
---------------------------------------------------------------------------
function p.makeImage(name, options)
	checkTypeLight('makeImage', 1, name, {'string', 'table'})
	checkTypeLight('makeImage', 2, options, 'table', true)
	
	name = p.parseDualArg(name)
	options = options or {}
	
	local size, extension, vertAlign, horizAlign = unpack{
			options.size or options.s or 21,
			options.extension or options.ext or options.e or 'png',
			options.vertalign or options.vertical_align or options.vAlign or options.v_align or options.vl,
			options.horizalign or options.horizontal_align or options.hAlign or options.h_align or options.hl,
		}
		
	local link = options.link or options.l
	local alt = options.alt or options.a
	local page = options.page or options.p
	local class = options.class or options.cl
	local noLink = options.nolink or options.nl
	local format = options.format or options.form or options.f
	local caption = options.caption or options.cap or options.c
	local lang = options.langauge or options.lang or options.lan
	local upright = options.upright or options.ur
	
	vertAlign = vertAlign and vertAlign:lower()
	horizAlign = horizAlign and horizAlign:lower()
	format = format and format:lower()
	
	local hl_aliases = {
		['l'] = 'left',
		['le'] = 'left',
		['lef'] = 'left',
		['left'] = 'left',
		
		['r'] = 'right',
		['ri'] = 'right',
		['rig'] = 'right',
		['righ'] = 'right',
		['right'] = 'right',
		
		['c'] = 'center',
		['ce'] = 'center',
		['cen'] = 'center',
		['cent'] = 'center',
		['cente'] = 'center',
		['center'] = 'center',
		['cnt'] = 'center',
		['cntr'] = 'center',
		['centr'] = 'center',
		['cnter'] = 'center',
		['ct'] = 'center',
		
		['n'] = 'none',
		['no'] = 'none',
		['non'] = 'none',
		['none'] = 'none',
	}
	
	local vl_aliases = {
		['bl'] = 'baseline',
		['basel'] = 'baseline',
		['baselin'] = 'baseline',
		['bline'] = 'baseline',
		['baseline'] = 'baseline',
		['baslin'] = 'baseline',
		['baseli'] = 'baseline',
		
		['s'] = 'sub',
		['su'] = 'sub',
		['sub'] = 'sub',
		
		['sup'] = 'super',
		['supr'] = 'super',
		['sper'] = 'super',
		['spr'] = 'super',
		['super'] = 'super',
		
		['t'] = 'top',
		['to'] = 'top',
		['top'] = 'top',
		
		['tt'] = 'text-top',
		['t-t'] = 'text-top',
		['txt-tp'] = 'text-top',
		['text-top'] = 'text-top',
		['texttop'] = 'text-top',
		['txttp'] = 'text-top',
		['text-tp'] = 'text-top',
		
		['m'] = 'middle',
		['md'] = 'middle',
		['mid'] = 'middle',
		['midle'] = 'middle',
		['mdle'] = 'middle',
		['middle'] = 'middle',
		['midway'] = 'middle',
		
		['b'] = 'bottom',
		['bottom'] = 'bottom',
		['bt'] = 'bottom',
		['btm'] = 'bottom',
		['bot'] = 'bottom',
		['botom'] = 'bottom',
		
		['tb'] = 'text-bottom',
		['t-b'] = 'text-bottom',
		['txt-bt'] = 'text-bottom',
		['text-bot'] = 'text-bottom',
		['text-botom'] = 'text-bottom',
		['txt-bottom'] = 'text-bottom',
		['text-bottom'] = 'text-bottom',
	}
	
	local format_aliases = {
		['t'] = 'thumb',
		['tn'] = 'thumb',
		['thmb'] = 'thumb',
		['thumb'] = 'thumb',
		['thumbnail'] = 'thumb',
		['tmb'] = 'thumb',
		['tmbnl'] = 'thumb',
		
		['fl'] = 'frameless',
		['frmlss'] = 'frameless',
		['frameless'] = 'frameless',
		['fless'] = 'frameless',
		['framel'] = 'frameless',
		['frmless'] = 'frameless',
		['framelss'] = 'frameless',
		
		['f'] = 'frame',
		['fr'] = 'frame',
		['fra'] = 'frame',
		['fram'] = 'frame',
		['frame'] = 'frame',
		['frm'] = 'frame',
		['frme'] = 'frame',
		
		['b'] = 'border',
		['bo'] = 'border',
		['br'] = 'border',
		['bdr'] = 'border',
		['bord'] = 'border',
		['border'] = 'border',
		['bordr'] = 'border',
	}
	
	local vertAlign, horizAlign, format = vl_aliases[vertAlign], hl_aliases[horizAlign], format_aliases[format]
	
	if name:match('^(.*)%.(%a+)$') then
		name, extension = name:match('^(.*)%.(%a+)$')
	end
	
	local tpSize = type(size)
	local size = 
		(tpSize == 'number' or (tpSize == 'string' and size:match('^%d-(px)$')))
			and table.concat{ tonumber((tostring(size):gsub('px', ''))), 'px' }
		or (tpSize == 'table' and #size ~= 0)
			and table.concat{ tonumber(size[1]), 'x', tonumber(size[2]), 'px' }
		or size
	
	return table.concat{
		'[[File:',
		-- Filename
		name,
		'.',
		-- Extension
		extension,
		-- File format
		format and '|' or '',
		format or '',
		-- Vertical Alignment
		vertAlign and '|' or '',
		vertAlign or '',
		-- Horizontal Alignment
		horizAlign and '|' or '',
		horizAlign or '',
		-- Size
		'|',
		size,
		-- Link/No Link
		(noLink 
			and '|link=' 
			or table.concat{
				link and '|link=' or '',
				link or '',
			}
		),
		-- Class
		class and '|class=' or '',
		p.parseDualArg(class or ''),
		-- Alt
		alt and '|alt=' or '',
		p.parseDualArg(alt or ''),
		-- Page number
		tonumber(page) and '|page=' or '',
		tonumber(page) and page or '',
		-- Langauge
		lang and '|lang=' or '',
		lang or '',
		-- Upright
		(yesno(upright)
			and '|upright'
		or tonumber(upright)
			and table.concat{ '|upright=', tonumber(upright) }
		or upright == ''
			and '|upright='
		or ''
		),
		-- Caption
		caption and '|' or '',
		caption or '',
		-- Finish
		']]',
	}
end

---------------------------------------------------------------------------
-- function: .makeTitle(s: string, title: string [the title to make the element with], options: table)
--
-- Returns a html `<abbr>` element with `s` as its inner text and `title` as
-- its `title` attribute.
--
-----------[ OPTIONS ]----------
-- Specify one of the fields below in the `options` argument
-- to configure the output of this function.
---------------------------------------------------------------------------
function p.makeTitle(s, title, options)
	checkTypeLight('makeTitle', 1, s, {'string', 'table'})
	checkTypeLight('makeTitle', 2, title, {'string', 'table', 'nil'})
	checkTypeLight('makeTitle', 3, options, {'table', 'nil'})
	
	local options = options or {}
	
	local disableAbbr, ignoreTitleNil = unpack{
		options.disableAbbr or options.noAbbr,
		options.ignoreTitleNil or options.ignoreTitle or options.ignTitle
	}
	if ignoreTitleNil and not title then return s end
	return p.wrapHtml(s, disableAbbr and '<span>' or '<abbr>', { title = p.parseDualArg(title) })
end

---------------------------------------------------------------------------
-- function: .parseDualArg(arg: stringable [argument to parse])
-- 
-- Parses the argument to a string if it is a table using `table.concat()`
-- and the `sep` field if the table has one. Else, it returns the argument.
-- If `arg` is a number, it converts it a string.
---------------------------------------------------------------------------
function p.parseDualArg(arg)
	if arg == nil then return end
	checkType('parseDualArg', 1, arg, {'string', 'table', 'number'})
	
	if type(arg) == 'number' then arg = tostring(arg) end
	
	return type(arg) == 'table' and table.concat(arg, arg.sep or arg.s or '') or arg
end
p.makeHover = p.makeTitle

---------------------------------------------------------------------------
-- function: .wrapLink(val: string, dest: string)
--
-- Makes a wikitext link
---------------------------------------------------------------------------
function p.wrapLink(page, displayText)
	checkType('wrapLink', 1, page, {'string', 'table' }, true)
	checkType('wrapLink', 2, displayText, {'string', 'table' }, true)
	
	if not page or page == '' then return '' end
	
	local page = p.parseDualArg(page)
	local displayText = p.parseDualArg(displayText)
	
	return table.concat{
		'[[',
		page, 
		displayText and '|' or '', 
		displayText and displayText or '', 
		']]'
	}
end
p.makeLink = p.wrapLink
---------------------------------------------------------------------------
-- function: .parseUrlQuery(s: string | table [query to parse])
--
-- Parses a url query string into a table in the following format: a format where the parameter name is 
-- the field, and the value of that field the parameter.
---------------------------------------------------------------------------
function p.parseUrlQuery(s)
	local s = p.isStringable(s) and p.parseDualArg(s) or ''
	
	if type(query) == 'table' then return setmetatable(mt, s) end
	
	s = p.parseDualArg(s):gsub('^[%?&]+', ''):gsub(' ', '+')
	-- Remove preceding unesscessary params
	if s:match('^(https?:%/%/%S-)(%?.-=.+)$') then
		url, s = s:match('^(https?:%/%/%S-)(%?.*)$') 
		s = p.parseDualArg(s):gsub('^[%?&]+', ''):gsub(' ', '+')
		
	elseif not s:match('^(%w-)=(.*)$') and not s:match('^(https?:%/%/%S-)(%?.*)$') then
		error(string.format('Syntax error in parsing URL query: invalid URL query %q', s), 2)
	end
	local tmp = {}
	
	if s == '' then 
		error('Syntax error in parsing URL query: query is empty', 2)
	end
	
	-- Detect errors
	if s:match('%&$') then
		error('Syntax error in parsing URL query: trailing \"&\" found in input', 2)
	end
	
	local proto = {}
	
	-- Metatable methods
	function proto:extend(t, k)
		checkType('extend', 1, t, { 'table', 'string' })
		
		if type(t) == 'table' and k == nil then
			for key, v in pairs(t) do
				tmp[key] = v
			end
		else
			tmp[t] = k
		end
		
		return tmp, url
	end
	
	function proto:tostring(addUrl)
		ret = p.urlQueryToString(tmp, addUrl and url or nil)
		
		if not addUrl then
			return ret, url
		else
			return ret
		end
	end
	
	function proto:getParam(k)
		checkType('getParam', 1, k, {'string', 'number', 'function', 'boolean', 'table'})
		
		if type(k) == 'table' then
			local ret = {}
			for i, v in ipairs(k) do
				-- customFieldError(tmp[v] == nil, i, 1, 'getParam', 'attempted to get a non-existent query parameter %q', v)
				ret[v] = tmp[v]
			end
			return ret, url
		else
			assertTrue(tmp[k] ~= nil, 'bad argument #1 to getParam (attempted to get a non-existent query parameter %s)', 1, k)
			return tmp[k], url
		end
	end
	
	function proto:setUrl(s, isInternal)
		checkType('setUrl', 1, s, 'string')
		
		url = isInternal and tostring(mw.uri.fullUrl(s)) or s
		return tmp, url
	end
	
	function proto:setFragment(s)
		checkType('setFragment', 1, s, 'string')
		
		proto.fragment = s
		url = table.concat{ url, '#', mw.uri.anchorEncode(mw.uri.anchorDecode(s)) }
		
		return tmp, url
	end
	
	local mt = { 
		__index = function(t, k) 
			return proto[k]
		end, 
	}
	
	-- Check if the url query is only 1 param long
	if s:match('^(%w-)=([^&]*)$') then
		param, value = s:match('^%??(%w-)=(.*)$')
		tmp[param] = value
	
	-- Else use multiple method
	elseif #p.split(s, '&') > 1 then
		s = p.split(s, '&')
		
		for i = 1, #s, 1 do
			local param, value = s[i]:match('^(%w-)=(.*)$')
			
			-- Detect if param is empty
			if param == '' or not param then
				error('Syntax error in parsing URL query at index #' .. i .. ': url parameter name expected', 2)
			end
			
			value = type(value) ~= 'nil' and value or ''
			value = type(value) == 'string' and mw.uri.encode(mw.uri.decode(value)) or value
			
			tmp[param] = value
		end
	end
	
	setmetatable(tmp, mt)
	
	-- Return
	return tmp, url
end

---------------------------------------------------------------------------
-- function: .isStringable(v: any [value to check])
--
-- Checks if the value if the value is able to be turned into a string 
-- through any method with its data intact.
---------------------------------------------------------------------------
function p.isStringable(v)
	local vType = type(v)
	if vType == 'string' or (vType == 'table' and #v ~= 0 and v[1]) or vType == 'number' then
		return true
	else
		return false
	end
end

---------------------------------------------------------------------------
-- function: .urlQueryToString(s: table [query table to parse], url?: string | table)
--
-- Parses a query table in the following format into a string, where the following 
-- format can be: a format where the parameter name is the field, and the value 
-- of that field the parameter.
---------------------------------------------------------------------------
function p.urlQueryToString(t, url)
	-- Exit if the string is a query in string format
	if type(t) == 'string' and t:match('^[%?&]+(.-)=(.*)$') then
		return t
	end
	
	checkType('urlQueryToString', 1, t, { 'table' })
	
	-- Variables
	local ret, j = {}, 0
	
	for k, v in pairs(t) do
		j = j + 1
		v = mw.uri.encode(mw.uri.decode(p.isStringable(v) and p.parseDualArg(v) or ''))
		
		if j == 1 then
			ret[#ret+1] = table.concat{ '?', k, '=', v }
		else
			ret[#ret+1] = table.concat{ '&', k, '=', v }
		end
	end
	
	-- Return
	return table.concat{ p.parseDualArg(url or ''), table.concat(ret, '') }
end

--------------------------------------------------------------------------
-- function: .getUrlParam(url: string | table, param: string, default: any)
-- 
-- gets a url query parameter from a string
---------------------------------------------------------------------------
function p.getUrlParam(url, param, default)
	checkType('getUrlParam', 1, url, { 'string', 'table' })
	checkType('getUrlParam', 2, param, { 'string' })
	
	local query, url = p.parseUrlQuery(url)
	
	return query[param] ~= nil and query[param] or default
end

--------------------------------------------------------------------------
-- function: .externalUrl(url: stringable, query: string | table, alt?: stringable)
-- 
-- Makes a external link with an optional query and alternate text
---------------------------------------------------------------------------
function p.externalUrl(url, query, alt, fragment)
	checkType('externalUrl', 1, url, { 'string', 'table' })
	checkType('externalUrl', 2, query, { 'string', 'table', 'nil' })
	checkType('externalUrl', 3, alt, { 'string', 'table', 'nil' })
	checkType('externalUrl', 4, alt, { 'string', 'table', 'nil' })
	
	local url = p.parseDualArg(url)
	
	query = type(query) == 'string' and p.parseUrlQuery(query) or query
	query = p.urlQueryToString(query or {})
	fragment = p.parseDualArg(fragment)
	
	if not url:match('^https?:%/%/') then
		url = table.concat{ 'https://', url }
	end
	
	return p.wrapHtml{
		{
			alt and '[' or '',
			url:gsub(' ', '_'),
			query or '',
			alt and ' ' or '',
			alt and alt or '',
			alt and ']' or '',
		},
		'<span>',
		{ class = 'plainlinks' },
	}
end

function p._externalUrl(frame)
	local args = getArgs(frame)
	
	local url, alt, fragment = args[1], args[2], args[3]
	local quey = {}
	
	for k, v in pairs(args) do
		if type(k) == 'table' then
			query[k] = v
		end
	end
	
	return p.pcall(p.externalUrl, url, query, alt, fragment)
end

---------------------------------------------------------------------------------
-- function: .methods(s: stringable)
-- 
-- Takes all methods from this module and makes them avaible as methods on the object.
---------------------------------------------------------------------------------
function p.methods(s)
	local Methods = {
		__tostring = function(self)
			return self.value
		end,
		__concat = function(self, a, b)
			if a == self then
				self.value = self.value .. b
			else
				self.value = a .. self.value
			end
			return self
		end,
	}
	
	function Methods:getValue()
		return self.value
	end
	
	Methods.tostring = Methods.__tostring
	
	function Methods:constructor(s)
		self.value = s
	end
	
	for k, v in pairs(p) do
		if k ~= 'methods' then
			Methods[k] = function(self, ...)
				return v(self.value, ...)
			end
			Methods['_' .. k] = function(self, ...)
				self.value = v(self.value, ...)
				return self
			end
			Methods['__' .. k] = function(self, ...)
				return self:constructor(v(self.value, ...))
			end
		end
	end
	
	return table.makeClass(Methods)(s)
end

---------------------------------------------------------------------------
-- function: .fullUrl(page: string, query: string | table, alt: string | table)
--
-- Makes a full url
---------------------------------------------------------------------------
function p.fullUrl(page, query, alt, fragment)
	if not page then page = '2034890239833333' end
	
	if page == true and type(query) == 'table' then
		page, query, alt, fragment = unpack{
			query.page or query.p or query[1],
			query.query or query.quer or query.q or query[2],
			query.alt or query.a or query[3],
			query.fragment or query.frag or query.f or query[4],
		}
	end
	
	checkType('fullUrl', 1, page, { 'string', 'table' })
	checkType('fullUrl', 2, query, { 'string', 'table', 'nil' })
	checkType('fullUrl', 3, alt, { 'string', 'table', 'nil' })
	
	local page = p.parseDualArg(page)
	query = type(query) == 'string' and p.parseUrlQuery(query) or query
	query = type(query) == 'table' and p.urlQueryToString(query) or query
	
	return table.concat{
		alt and '[' or '',
		tostring(mw.uri.fullUrl(page:gsub(' ', '_'), query)):gsub('2034890239833333', ''),
		(fragment and fragment ~= '') and '#' or '',
		(fragment and fragment ~= '') and p.parseDualArg(fragment) or '',
		alt and ' ' or '',
		alt and (p.parseDualArg(alt)) or '',
		alt and ']' or '',
	}
end

function p._fullUrl(frame)
	local query = {}
	local args = getArgs(frame)
	
	for arg, value in pairs(args) do
		if type(arg) == 'string' then
			query[arg] = value
		end
	end
	
	return p.pcall(p.fullUrl, args[1], query, args[2], args[3])
end
---------------------------------------------------------------------------
-- Template:Repeat
-- 
-- Repeats the given string a specified number of times
---------------------------------------------------------------------------
function p.repeatF(frame)
	local args = getArgs(frame, {removeBlanks = false, trim = false })
	local s = args[1] or ''
	local num = p.trim(args[2]) or 1
	local sep = p.trim(args[3])
	
	assertFalse(num:match('[^%d%-%+]+'), 'number expected, got %q', 2, num)
	assertFalse(0 >= tonumber(num), 'repeat delimiter must be greater than 0, got %q', 2, num)
	
	return p._repeat(s, num, sep, yesno(p.trim(args[4]), false))
end
---------------------------------------------------------------------------
-- Template:Repeat Module Access Point
---------------------------------------------------------------------------
function p._repeat(s, num, sep, isRoman)
	checkTypeLight('_repeat', 1, s, { 'string', 'number', 'table' })
	assertTrue(tonumber(num), 'argument #2 is not convertable to a number', 2)
	checkTypeLight('_repeat', 3, sep, { 'string', 'number', 'table' }, true)
	
	local num = tonumber(num) or 1
	local start = tonumber(sep) or 1
	
	assertTrue(num < 0 or num < 1e308, 'number out of range', 2)
	
	local t = {}
	s, sep = s and s:gsub('\\n', '\n'), sep and sep:gsub('\\n', '\n')
	for i = start, num, 1 do
		local parsedNum = isRoman and p._toRoman(i) or i
		
		t[i] = p.parseDualArg(s):gsub('([^\\])%$n', '%1' .. parsedNum):gsub('^%$n', parsedNum):gsub('\\%$', '$')
	end
	
	return table.concat(t, sep)
end

---------------------------------------------------------------------------------
-- Html Wrapping Function
-- 
-- Allows for easy creating of html elements in a lua format 
-- This function is meant to make single html elements. 
-- Use mw.html.create() for more complex elements.
---------------------------------------------------------------------------------
local function parseHtmlAttrs(attrs)
	local attrsTable = {}
	
	for k, v in pairs(attrs) do
		local kType, vType = type(k), type(v)
		if kType ~= 'string' and kType ~= 'number' then
			error(string.format('Invalid table index (attribute name/string expected, got %s)', kType), 2)
		elseif k == '' then
			error('Invalid table index (attribute name expected)', 2)
		elseif not tostring(k):match('^([%w%_%:%.%-]+)$') then
			error(string.format('Invalid HTML attribute name "%s"', k), 2)
		end
		
		k = tostring(k):lower()
		if k == 'style' and vType == 'table' then
			cssTables = {}
			for key, val in pairs(v) do
				cssTables[#cssTables+1] = (type(key) == 'string' and key:lower() .. ':' or '') .. tostring(val)
			end
			v = table.concat(cssTables, ';')
		elseif k == 'style' and not v:match('(;)$') then
			v = v .. ';'
		elseif k == 'id' then
			v = v:gsub(' ', '-')
		elseif k == 'class' and vType == 'table' then
			classTables = {}
			for i, val in pairs(v) do
				classTables[#classTables+1] = val:gsub(' ', '-')
			end
			v = table.concat(classTables, ' ')
		elseif k == 'href' then
			v = mw.uri.encode(mw.uri.encode(v))
		elseif k == 'title' and vType == 'table' then
			v = p.parseDualArg(v)
		end
		attrsTable[#attrsTable+1] = table.concat{k, '="', p.parseDualArg(v), '"'}
	end
	return table.concat(attrsTable, ' ')
end

local selfClosingTags = {
	['area'] = true,
	['base'] = true,
	['br'] = true,
	['col'] = true,
	['command'] = true,
	['embed'] = true,
	['hr'] = true,
	['img'] = true,
	['input'] = true,
	['keygen'] = true,
	['link'] = true,
	['meta'] = true,
	['param'] = true,
	['source'] = true,
	['track'] = true,
	['wbr'] = true,
	['path'] = true,
	['svg'] = true,
	['defs'] = true,
}

function p.wrapHtml(val, wrap, attrs)
	if type(val) == 'table' and not wrap then
		attrs = val[3] or val.attrs
		wrap = val[2] or val.tag
		sep = val.sep or val.s
		val = val[1] or val.text
	end
	
	checkTypeLight('wrapHtml', 1, val, { 'string', 'table', 'number' })
	checkTypeLight('wrapHtml', 2, wrap, 'string')
	checkTypeLight('wrapHtml', 3, attrs, 'table', true)
	
	if wrap == '' or not wrap
		then error('bad argument #2 to \'wrapHtml\' (tag name expected)', 2)
	end
	
	sep = type(val) == 'table'
		and (val.seperator or val.sep or val.s)
		or ''
	
	wrap = wrap:lower():gsub('%<[%\\%/]?([^%/%\\]+)[%\\%/]?>', '%1')
	local selfClosing = selfClosingTags[wrap]
		
	if attrs ~= nil
		then 
		attrs = ' ' .. parseHtmlAttrs(attrs)
		else attrs = ''
	end
	
	if wrap:match('([^%a0-9]+)')
		then error(string.format('Invalid HTML tag name \"<%s>\"', wrap), 2)
	end
	
	return table.concat{
		'<',
		wrap, 
		attrs,
		selfClosing and ' />' or '>',
		type(val) == 'table' and table.concat(val, sep) or val,
		selfClosing and '' or '</',
		selfClosing and '' or wrap,
		selfClosing and '' or '>',
	}
end

local function _makeTag(text, tag)
	tag = tag:lower():gsub('%<[%\\%/]?([^%/%\\]+)[%\\%/]?>', '%1')
	assertTrue(not tag:match('([^%a0-9]+)'), 'bad argument #2 to \'wrapTag\' (Invalid HTML tag name \"<%s>\")', 2, tag)
	
	text = p.parseDualArg(text) or ''
	local selfClosing = selfClosingTags[tag]
	return table.concat{
		'<',
		tag,
		selfClosing and ' />' or '>',
		text,
		selfClosing and '' or '</',
		selfClosing and '' or tag,
		selfClosing and '' or '>',
	}
end
function p.wrapTag(text, tag, attrs)
	checkTypeLight('wrapTag', 1, text, { 'string', 'table', 'number' }, true)
	checkTypeLight('wrapTag', 2, tag, { 'string', 'table' })
	assertTrue(#tag > 0, 'bad argument #2 to \'wrapTag\' (tag name expected)', 2)
	assertTrue(not attrs, 'Tag name contains attributes, please string.wrapHtml()', 2)
	
	text = p.parseDualArg(text) or ''
	tag = type(tag) ~= 'table' and { tag } or tag
	for i, tg in ipairs(tag) do
		text = _makeTag(text, tg)
	end
	
	return text
end

---------------------------------------------------------------------------------
-- function: ._formatShortNumber(number: string|number)
--
-- This function takes a number value and returns a short version of it
-- Example: 10000 = 10k
---------------------------------------------------------------------------------
function p._formatShortNum(number)
	local steps = {
		{1, ''},	-- units
		{1e3, 'k'},	-- thousand
		{1e6, 'M'},	-- million
		{1e9, 'B'},	-- billion
		{1e12, 'T'},	-- trillion
		{1e15, 'Qu'},	-- quadrillion
		{1e18, 'Qi'},	-- quintillion
		{1e21, 'Se'},	-- sextillion
		{1e24, 'Sp'},	-- septillion
		{1e27, 'O'},	-- octillion
		{1e30, 'N'},	-- nonillion
		{1e33, 'De'},	-- decillion
		{1e36, 'UDe'},	-- undecillion
		{1e39, 'DDe'},	-- duodecillion
		{1e42, 'TDe'},	-- tredecillion
		{1e45, 'QaDe'},	-- quattuordecillion
		{1e48, 'QiDe'},	-- quindecillion
		{1e51, 'SeDe'},	-- sexdecillion
		{1e54, 'SpDe'},	-- septemdecillion
		{1e57, 'ODe'},	-- octodecillion
		{1e60, 'NDe'},	-- novemdecillion
		{1e63, 'v'},	-- vigintillion
		{1e66, 'Uv'},	-- unvigintillion
		{1e69, 'Dv'},	-- duovigintillion
		{1e72, 'Tv'},	-- trevigintillion
		{1e75, 'Qav'},	-- quattuorvigintillion
		{1e78, 'Qiv'},	-- quinvigintillion
		{1e81, 'Sev'},	-- sexvigintillion
		{1e84, 'Spv'},	-- septemvigintillion
		{1e87, 'Ov'},	-- octovigintillion
		{1e90, 'Nv'},	-- novemvigintillion
	}
	for _, b in ipairs(steps) do
		if b[1] <= number+1 then
			steps.use = _
		end
	end
	local result = string.format('%.1f', number / steps[steps.use][1])
	if tonumber(result) >= 1e3 and steps.use < #steps then
		steps.use = steps.use + 1
		result = string.format('%.1f', tonumber(result) / 1e3)
	end
	result = string.sub(result, 0, string.sub(result, -1) == '0' and -3 or -1)
	return result .. steps[steps.use][2]
end

---------------------------------------------------------------------------------
-- function: trimTrailingZeros(n)
-- 
-- Removes trailing zeros from a number.
---------------------------------------------------------------------------------
function p.trimTrailingZeros(s)
	return tonumber((tostring(s):gsub('0*$', '')))
end

---------------------------------------------------------------------------------
-- function: ._toNumber(str: string)
---------------------------------------------------------------------------------
function p._toNumber(str)
	-- check normal string -> number
	local num = tonumber(lang:parseFormattedNumber(tostring(str)))
	if num then return num end
	
	-- check if number short form
	local steps = {
		{1e3, 'k'},
		{1e6, 'm'},
		{1e9, 'b'},
		{1e12, 't'},
		{1e15, 'qa'},
		{1e18, 'qi'},
		{1e21, 'se'},
		{1e24, 'sp'},
		{1e27, 'o'},
		{1e30, 'n'},
		{1e33, 'de'}
	}
	for i, step in pairs(steps) do
		num = string.match(string.lower(tostring(str)), '([%d%.,]+)' .. step[2] )
		if num then
			local exp = step[1]
			num = num * exp
			break
		end
	end
	if num then return num end
	
	-- else invalid
	return nil
end

---------------------------------------------------------------------------------
-- Template:FormatNum
---------------------------------------------------------------------------------
function p.formatNum( frame )
	local args = getArgs(frame)
	return p._formatNum( args[1] )
end

---------------------------------------------------------------------------------
-- Template:FormatNum Module Access point
---------------------------------------------------------------------------------
function p._formatNum(num)
	local num = tostring(num or 0):gsub('[%, ]', '')
	local neg
	
	if not tonumber(num) then
		return tonumber(lang:parseFormattedNumber(num))
	end
	
	if #num < 3 then return num end
	if num:match('^%-') then 
		neg = true
		num = num:gsub('^%-', '')
	end
	
	local decimal = num:match('%.([0-9]+)$')
	num = num:gsub('%.([0-9]+)$', '')
	num, ret = tostring(num), {}
	
	local _, len = num:gsub('%d%d%d', '')
	
	for i = len, 1, -1 do
		table.insert(ret, 1, num:match('%d%d%d$'))
		num = num:gsub('%d%d%d$', '')
	end
	
	if num ~= '' then
		table.insert(ret, 1, num)
	end
	
	return (neg and '-' or '') .. table.concat(ret, ',') .. (decimal and '.' .. decimal or '')
end

---------------------------------------------------------------------------------
-- Internal implementation of p.error() for more flexibility
---------------------------------------------------------------------------------
local function errorWithStack(msg, level)
	if msg and msg:match('^[Tt]emplate:(.-):%s*') then
		template = msg:match('^[Tt]emplate:(.-):%s*')
		msg = msg:gsub('^[Tt]emplate:(.-):%s*', '')
	end
	
	local tmp = msg
	local errorMsg = debug.traceback(tmp or 'Unknown error', level or 0)
	local msg = tmp or 'Unknown error'
	
	if msg:match('bad argument.*') then
		msg = msg:gsub('^.-%((.-)%).-$', '%1')
	end
	local errorMsg = (errorMsg:match('^(M?o?d?u?l?e?:?[%w%.%(%)%:]+):(%d+)') and '' or 'Lua Error: ') .. errorMsg
		:gsub('^(M?o?d?u?l?e?:?[%w%.%(%)%:%/%_ %(%)]+):(%d+)', function(name, line)
			return table.concat{ 
				'Lua Error in ', 
				name:match('Module:') and p.fullUrl(
					name,
					{ action = 'edit' },
					{ name, '&#x0200b;:', line },
					{ 'mw-ce-l', line }
				) or name, 
				' at line ',
				line 
			}
		end, 1)
		:gsub('\n\t([%w%.%(%)%:%/%_ %(%)%[%]]+):', '\n\t<b>%1</b>:')
		:gsub('\n', '<br>')
		:gsub(
			'Module:([%w%.%(%)%:%/%_ %(%)]+):(%d+)', 
			function(module, line)
				return p.fullUrl(
					{ 'Module:', module }, 
					{ action = 'edit' },
					{ 'Module:', module, ':', line },
					{ 'mw-ce-l', line }
				)
			end
		)
		
	local ret = table.concat{
		p.wrapHtml{
			{
				'➤ ',
				template and 'Template Error in [[Template:' .. template .. '|'.. p.wrapHtml('Template:' .. template, 'strong', {class = 'error'}) .. ']]:&nbsp;' or 'Template Error:&nbsp;',
				msg,
			},
			'<span>',
			{
				class = {
					'error',
					'mw-customtoggle-callstack',
				},
			},
		},
		p.wrapHtml{
			errorMsg,
			'<div>',
			{
				id = 'mw-customcollapsible-callstack',
				class = {
					'mw-collapsible',
					'mw-collapsed',
				},
				style = {
					['background-color'] = 'rgba(0,0,0,0.1)',
					['line-height'] = '14px',
					['overflow'] = 'auto',
					['padding'] = '8px',
					['word-wrap'] = 'normal',
					['display'] = 'block',
					['white-space'] = 'pre',
					['margin'] = '1em 0px',
					['font-family'] = 'monospace',
					['tab-size'] = 4,
				},
			},
		},
		'[[Category:Pages with template errors]]',
	}
	
	mw.addWarning(ret)
	return ret
end

---------------------------------------------------------------------------------
-- function: .error(msg: string, ...<string.format() arguments>)
--
-- Returns a error message with the lua call stack. "Template:<template>" 
-- may be added at the front of the message to link a template to the error message.
-- Arguments may be passed as they are in `string.format()`.
---------------------------------------------------------------------------------
function p.error(msg, ...)
	local vArgs = { ... }
	for i, v in pairs(vArgs) do
		if type(v) == 'number' then
			level = v
			table.remove(vArgs, i)
			break
		end
	end
	for i, v in pairs(vArgs) do
		vArgs[i] = p.parseDualArg(v)
	end
	return errorWithStack(string.format(msg, ...), 0)
end
p.templateError = p.error
p._error = p.error

---------------------------------------------------------------------------
-- function: .pcall(f: function [function to call], ...params?: any [function parameters])
--
-- Returns all values by the called function if no error was raised.
-- If there was an error in calling the function, it returns a formatted error message.
---------------------------------------------------------------------------
function p.pcall(f, ...)
	checkType('pcall', 1, f, 'function')
	
	local len = select('#', ...)
	local t = { ... }
	local success, response = xpcall(bind(function(...)
		return f(...)
	end, ...), function(e)
		local t = p.split(errorWithStack(e, 4), '<br>')
		
		return table.concat(t, '<br>')
	end)
	
	return response
end

---------------------------------------------------------------------------------
-- function: .preprocess(val: string)
--
-- parse wikitext into html
---------------------------------------------------------------------------------
function p._preprocess(val)
	local frame = mw.getCurrentFrame()
	
	return frame:preprocess(val)
end
p.preprocess = p._preprocess

---------------------------------------------------------------------------------
-- function: .centerText(s: string)
--
-- aligns text to the center of an element
---------------------------------------------------------------------------------
function p.centerText(s)
	return p.wrapHtml(s, 'div', {style = 'text-align: center;'}) 
end

---------------------------------------------------------------------------------
-- function: .trimWhitespace(s: string)
--
-- Trims the whitespace from the string
---------------------------------------------------------------------------------
function p.trimWhitespace(s, removeDoubles)
	checkTypeLight('trimWhitespace', 1, s, 'string', true)
	checkTypeLight('trimWhitespace', 2, removeDoubles, 'boolean', true)
	
	if not s then return s end
	
	s = s:gsub('^%s*(.-)%s*$', '%1')
	if removeDoubles then
		s = s:gsub('(%s)%s*', '%1')
	end
	return s
end
p.trim = p.trimWhitespace

------------------------------------------------
-- function: .toRoman(s: string)
-- original source: https://gist.github.com/efrederickson/4080372
--[[
Symbol	Value
I		1
V		5
X		10
L		50
C		100
D		500
M		1000
If a lesser number comes before a greater number (e.g. IX), then
the lesser number is subtracted from the greater number (IX -> 9, 900 -> CM)
So, 
Symbol	Value
IV		4
IX		9
XL		40
XC		90
CD		400
CM		900
LM		950
VX is actually valid as 5, along with other irregularities, such as IIIIIV for 8
Copyright (C) 2012 LoDC
]]
---------------------------------------------------------------------------------
local map = { 
	I = 1,
	V = 5,
	X = 10,
	L = 50,
	C = 100, 
	D = 500, 
	M = 1000,
}
local numbers = { 1, 5, 10, 50, 100, 500, 1000 }
local chars = { 'I', 'V', 'X', 'L', 'C', 'D', 'M' }

function p._toRoman(s)
	s = p._toArabic(s)
	
	if not s or s ~= s then return nil end
	if s == math.huge then error('Unable to convert infinity', 2) end
	
	s = math.floor(s)
	
	if s <= 0 then return s end
	
	local ret = ''
		for i = #numbers, 1, -1 do
		local num = numbers[i]
		while s - num >= 0 and s > 0 do
			ret = ret .. chars[i]
			s = s - num
		end
		-- for j = i - 1, 1, -1 do
		for j = 1, i - 1 do
			local n2 = numbers[j]
			if s - (num - n2) >= 0 and s < num and s > 0 and num - n2 ~= n2 then
				ret = ret .. chars[j] .. chars[i]
				s = s - (num - n2)
				break
			end
		end
	end
	return ret
end

---------------------------------------------------------------------------------
-- Template:ToRomanNum
--
-- Convert Numbers to roman numerals
---------------------------------------------------------------------------------
function p.roman(frame)
	local args = getArgs(frame)
	local num = args[1]
	local min = tonumber(args['min']) or 1
	local max = tonumber(args['max']) or 9999999
	
	-- if already a roman numeral, convert to number
	if string.match(num:upper(), '^[IVXLCDM]+$') then
		num = p._toArabic(num)
	end
	
	num = tonumber(num)
	if tonumber(num) then
		if num >= min and num <= max then
			return p.wrapHtml(p._toRoman(num), 'span', {style = 'font: bold 100% times new roman;'})
		else
			-- if we set a min/max and the number doesn't fall between it then it's invalid
			return '[out of range][[Category:Pages with roman numeral errors]]'
		end
	end
	return 'INVALID INPUT[[Category:Pages with roman numeral errors]]'
end

---------------------------------------------------------------------------------
-- function: .toArabic(s: string)
--
-- converts roman numerals to regular numbers
---------------------------------------------------------------------------------
function p._toArabic(s)
	s = tostring(s):upper()
	local ret = 0
	local i = 1
	
	if s:match('^%d+$') then
		return tonumber(s)
	elseif s == '' or s == 'NIL' then
		return 0
	elseif not s:match('^[IVXLCDM%d%-]+$') then
		return nil
	end
	
	while i <= s:len() do
	-- for i = 1, s:len() do
		local c = s:sub(i, i)
		if c ~= ' ' then -- allow spaces
			local m = map[c] or error('Unknown Roman Numeral \'' .. c .. '\'', 2)
			
			local next = s:sub(i + 1, i + 1)
			local nextm = map[next]
			
			if next and nextm then
				if nextm > m then 
				-- if string[i] < string[i + 1] then result += string[i + 1] - string[i]
				-- This is used instead of programming in IV = 4, IX = 9, etc, because it is
				-- more flexible and possibly more efficient
					ret = ret + (nextm - m)
					i = i + 1
				else
					ret = ret + m
				end
			else
				ret = ret + m
			end
		end
		i = i + 1
	end
	return tonumber(ret)
end

function p._splitNameAndTier(str)
	local out = {}
	if str:find('%s[%dIVXLCDMivxlcdm]+$') then
		out[1], out[2] = str:match('^(.+)%s([%dIVXLCDMivxlcdm]+)$')
	else
		out[1] = str
		out[2] = nil
	end
	return out
end

---------------------------------------------------------------------------------
-- Template: Skydate
--
-- Converts an ingame date to a date compatible with skydate.js
---------------------------------------------------------------------------------
-- function p.skydate(str)
-- 	local conversions = {
-- 		['early spring'] = 'ESP',
-- 		['^spring'] = 'SP',
-- 		['late spring'] = 'LSP',
-- 		['early summer'] = 'ESU',
-- 		['^summer'] = 'SU',
-- 		['late summer'] = 'LSU',
-- 		['early autumn'] = 'EAU',
-- 		['^autumn'] = 'AU',
-- 		['late autumn'] = 'LAU',
-- 		['early fall'] = 'EAU',
-- 		['^fall'] = 'AU',
-- 		['late fall'] = 'LAU',
-- 		['early winter'] = 'EWI',
-- 		['^winter'] = 'WI',
-- 		['late winter'] = 'LWI',
-- 		['(%d)st'] = '%1',
-- 		['(%d)nd'] = '%1',
-- 		['(%d)rd'] = '%1',
-- 		['(%d)th'] = '%1',
-- 	}
-- 	local str = getArgs(frame)[1] or ''
-- 	for k, v in pairs(conversions) do
-- 		str = str:gsub(k, v)
-- 	end
-- 	return str
-- end

---------------------------------------------------------------------------------
-- Template: Lorem
--
-- Classic lorem ispum
---------------------------------------------------------------------------------
function p.lorem(frame)
	local args = getArgs(frame)
	local num = args[1]
	if not num then num = '1p' end
	return p._lorem(num)
end
---------------------------------------------------------------------------------
-- Template: Lorem module access point
---------------------------------------------------------------------------------
function p._lorem(num)
	lorem = require('Module:String/Lorem')
	
	local suffix
	suffix = num:match('^%d*(%a)$') or nil
	num = num:match('^(%d*)%a?$') or error('No number provided')
	num = tonumber(num)
	
	if not (suffix == nil or suffix == 'w' or suffix == 'p') then p._error('invalid suffix', suffix) end
	
	local str = {}
	if suffix == nil then suffix = 'w' end
	if suffix == 'p' then 
		if num > 10 or num == 0 then return p._error('Invalid number. Maximum number accepted: 10') end
		str[#str+1] = lorem[i]
		for i = 2, num, 1 do
			str[#str+1] = '\n' .. lorem[i]
		end
	elseif suffix == 'w' then
		if num > 1008 or num == 0 then return p._error('Invalid number. Maximum number accepted: 10') end
		full = lorem['full']
		full = p.split(full, '[%s\n]')
		str[#str+1] = full[i]
		for i = 1, num, 1 do
			str[#str+1] = ' ' .. full[i]
		end
	end
	return table.concat(str)
end
---------------------------------------------------------------------------------
-- function: .roundNumber(num: number, posistion: number)
--
-- rounds numbers nicely
---------------------------------------------------------------------------------
function p.roundNumber(num, position)
	if not position then position = 0 end
	position = 10^position
	num = num * position
	num = math.floor(num + 0.5)
	num = num / position
	return num
end

---------------------------------------------------------------------------------
-- function _delDoubleSpace(text: string)
--
-- Remove duplicate spaces form strings
---------------------------------------------------------------------------------
function p._delDoubleSpace(text)
	text = text:gsub('(%s)%s*', '%1')
	return text
end

---------------------------------------------------------------------------------
-- Template:Lower
---------------------------------------------------------------------------------
function p._lower(frame)
	local args = getArgs(frame)
	local s = args[1]
	
	if not s then s = '' end
	return s:lower()
end
---------------------------------------------------------------------------------
-- Template:Upper
---------------------------------------------------------------------------------
function p._upper(frame)
	local args = getArgs(frame)
	local s = args[1] or ''
	
	return s:upper()
end

function p.warning(frame)
	return mw.addWarning(getArgs(frame)[1])
end

---------------------------------------------------------------------------------
-- function: sublength(frame: table)
-- 
-- returns a substring of a given string at a specific index and length
-- originally from [[WP:Module:String]]
---------------------------------------------------------------------------------
function p.sublength(frame)
	local args = getArgs(frame)
	
	local i = tonumber(args.i) or 0
	local len = tonumber(args.len)
	return mw.ustring.sub(args.s, i + 1, len and (i + len))
end

---------------------------------------------------------------------------------
-- function: matchTemplate(frame)
--
-- a version of string.match() that can be used with {{#invoke:String|matchTemplate}}
---------------------------------------------------------------------------------
function p.matchTemplate(frame)
	local args = getArgs(frame)
	
	local s = args['s'] or args[1] or ''
	local pattern = args['pattern'] or args[2] or ''
	local nomatch = args['nomatch']
	
	return mw.ustring.match(s, pattern) or nomatch
end

-- Finish module
return p