Modul niki kaangen ring mal-mal puniki;

Cingak asil tes ring Mal:Header/testcases.


--[=[
This is a module to implement logic for [[Template:Header]] and [[Template:Translation header]]

TODO:
- centuries are defined as starting on XX01, but WS categorizes them as starting on XX00
-- check whether that's a considered policy choice
]=]

require('strict')

local p = {} --p stands for package

local yesno = require('Modul:Yesno')
local getArgs = require('Modul:Arguments').getArgs
local headerStructure = require('Modul:Header structure')
local headerAttributions = require('Modul:Header/attribution')
local tableTools = require('Modul:TableTools')
local ISO_639_language_name = require('Modul:ISO 639').language_name
local parent_links = require('Modul:Auto parents')._parent_links

local current_title = mw.title.getCurrentTitle()

--[=[
Get badge if any
]=]
local function badge()
	return require('Modul:Edition').badge({args = {category = '1', indicator = '1'}})
end

--[=[
Construct the [[Help:Microformat]] for the page.

This is in the form:
<div id="ws-data" ...>
  <span id="ws-title">Title here...</span>
  ...
<div>
]=]
local function construct_microformat(args)
	local mf_div =  mw.html.create('div')
		:addClass('ws-noexport')
		:attr('id', 'ws-data')
		:css({speak = 'none'})
	
	-- override to show the microformat
	if yesno(args['show-microformat']) then
		mf_div:addClass('ws-data-show')
	end
	
	-- collect the MF values here
	local mf = {};

	mf['ws-article-id'] = current_title.id 
	
	-- add the title
	if args['title'] then
		mf['ws-title'] = args['title']

		-- append section if there is one
		if args['section'] then
			mf['ws-title'] = mf['ws-title'] .. " — " .. args['section']
		end
	end
	
	local author = args['override-section-author'] or args['section-author'] or args['override-author'] or args['author']
	if author then
		mf['ws-author'] = author
	end
	
	local translator = args['override-translator'] or args['translator']
	if translator then
		mf['ws-translator'] = translator
	end
	
	local year = args['override-year'] or args['year']
	if year then
		mf['ws-year'] = year
	end
	
	if args['cover'] then
		mf['ws-cover'] = args['cover']	
	end
	
	for k, v in pairs(mf) do
		mf_div:tag('span'):attr('id', k):wikitext(v)
	end
	
	return tostring(mf_div)
end

--[=[
Detect explicit formatting in fields like "section" and "title"
]=]
local function explicit_formatting(str)
	return string.match(str, "'''?")
		or string.match(str, "<%s*/?%s*[iIbB]%s*>")
		-- add more cases here or come up with a less silly way to do things
end

local function check_non_existent_author_pages(args, param, categories)
	if args[param] then
		-- some pages expect an invalid author
		local lower_arg = string.lower(args[param])
		local attr_data = headerAttributions.attr_data[param] or headerAttributions.attr_data[string.gsub(param, 'section-', '')]
		if not attr_data or not attr_data['special_cases'][lower_arg] then
			local target = mw.title.makeTitle("Pangawi", args[param])
			-- expensive function!
			if not target or not target.exists then
				table.insert(categories, "Works with non-existent author pages")
			end
		end
	end
end

--[=[
Construct the automatic categories for the header
]=]
local function construct_categories(args)
	local categories = {}
	
	if args['override-author'] then
		table.insert(categories, "Pages with override author")
	end
	
	if current_title:inNamespaces(0, 114) or args.testing then
		check_non_existent_author_pages(args, 'author', categories)
		check_non_existent_author_pages(args, 'editor', categories)
		check_non_existent_author_pages(args, 'translator', categories)
		check_non_existent_author_pages(args, 'section-translator', categories)
		check_non_existent_author_pages(args, 'section-author', categories)
	end
	
	if args['section-author'] then
		table.insert(categories, 'Pages with contributor')
	end
	if args['override-section-author'] then
		table.insert(categories, 'Pages with override contributor')
	end
	
	local author = args['override-author'] or args['author']
	if author and (string.lower(author) == 'unknown') then
		if args.template_name == 'Translation header' then
			table.insert(categories, 'Translations of anonymous works')
		else
			table.insert(categories, "Anonymous texts")
		end
	end
	
	local editor = args['override-editor'] or args['editor']
	if editor then
		editor = string.lower(editor)
		if editor == 'unknown' or editor == '?' then
			table.insert(categories, "Works with unknown editors")
		elseif editor == 'not mentioned' then
			table.insert(categories, "Works with unmentioned editors")
		end
	end
	
	local translator = args['override-translator'] or args['translator']
	if translator then
		translator = string.lower(translator)
		if translator == 'unknown' or translator == 'not mentioned' or translator == '?' then
			table.insert(categories, 'Translations without translator information specified')
		end
	end
	
	if args["shortcut"] then
		if current_title:inNamespaces(0) then
			table.insert(categories, 'Mainspace pages with shortcuts')
		elseif current_title:inNamespaces(114) then
			table.insert(categories, 'Translation namespace pages with shortcuts')
		end
	end
	
	if args['override-year'] then
		table.insert(categories, "Pages with override year")
	end
	if args['noyear'] then
		table.insert(categories, "Pages with noyear")
	end
	if yesno(args['noyearcat']) then
		table.insert(categories, "Pages with noyearcat")
	end
	
	if args['cover'] then
		table.insert(categories, "Pages with an export cover")
	end
	
	-- sanity/maintenance checks on various parameters
	
	-- allow_explicit_formatting parameter suppresses this check
	-- used by, for example, [[Template:Versions]]
	if not yesno(args['allow-explicit-formatting']) then
		if args['title'] and explicit_formatting(args['title']) then
			table.insert(categories, "Pages with explicit formatting in header fields")
		end
		if args['section'] and explicit_formatting(args['section']) then
			table.insert(categories, "Pages with explicit formatting in header fields")
		end
	end
	
	-- translation header categories
	if args.template_name == 'Translation header' then
		if args.language then
			table.insert(categories, 'Works originally in ' .. (args.language_name or 'an undefined language'))
		else
			table.insert(categories, 'Wikisource translations with no original language')
		end
		
		if not args.original then
			table.insert(categories, 'Wikisource translations with no original source')
		end
		
		if not current_title.isSubpage then
			table.insert(categories, 'Wikisource translations')
		end
	end
	
	-- detect inappropriate template use
	--[=[
	if (args['template-name'] ~= 'Translation header' and translator and string.lower(translator) == 'wikisource')
		or (current_title:inNamespaces(114) and args['template-name'] ~= 'Translation header') then
		-- tracking category for pages that should be using translation header?
	end
	if current_title:inNamespaces(0) and args['template-name'] == 'Translation header' then
		-- tracking category for translation header in mainspace?
	end
	]=]
	
	local category_links = {}
	for k, v in pairs(categories) do
		table.insert(category_links, '[[Kategori:' .. v .. ']]')
	end
	return table.concat(category_links)
end

--[=[
Check for numerical parameters (which shouldn't be used)
]=]
local function check_for_numerical_arguments(args)
	for k, v in pairs(args) do
		if type(k) == 'number' then
			return '[[Kategori:' .. 'Headers with numerical arguments' .. ']]'
		end
	end
	return ''
end

--[=[
Add categories from the categories parameter
]=]

local function manual_categories(args)
	if not args.categories then
		return ''
	end
	
	-- Replace each string that ends in a slash with a category definition.
	-- This does include the final one because we're adding a slash to the input string.
	local categories = mw.ustring.gsub(args.categories .. '/', '([^/]+)/*', '[[Kategori:%1]]')
	
	return '[[Kategori:' .. 'Works using categories parameter' .. ']]' .. categories
end

--[=[
Categorize subpages
]=]
local function check_subpages()
	local title = current_title
	local parent_exists = false
	while title.isSubpage and not parent_exists do
		title = mw.title.new(title.baseText, title.nsText)
		parent_exists = title.exists
	end
	if parent_exists and title:inNamespaces(0) then
		return '[[Kategori:' .. 'Subpages' .. ']]'
	elseif parent_exists then
		return '[[Kategori:' .. title.nsText .. ' subpages' .. ']]'
	else
		return ''
	end
end

--[=[
Generate a {{DEFAULTSORT}} magic word according to the defaultsort parameter
]=]
local function construct_defaultsort(args)
	local defsortKey
	local title = current_title.text
	
	if args.sortkey then
		defsortKey = args.sortkey
	else
		-- construct defaultsort automatically by stripping A/An/The as needed
		local title_table = mw.text.split(title, '/')
		local sorted_title
		local sorted_title_table = {}
		
		local articles = {'A', 'An', 'The'}
		for k, part in pairs(title_table) do
			local sorted_part = part
			local disambig = string.match(sorted_part, ' %(.*%)$')
			if disambig then
				sorted_part = string.gsub(sorted_part, ' %(.*%)$', '')
			else
				disambig = ''
			end
			
			for j, article in pairs(articles) do
				if string.len(sorted_part) > string.len(article) and string.sub(sorted_part, 1, string.len(article) + 1) == (article .. ' ') then
					sorted_part = string.sub(sorted_part, string.len(article) + 2) .. ', ' .. article .. disambig
					break
				end
			end
			table.insert(sorted_title_table, sorted_part .. disambig)
		end
		
		defsortKey = table.concat(sorted_title_table, '/')
		if defsortKey == title then
			defsortKey = nil
		end
	end
	
	-- if a suitable key is found or constructed, apply it
	if defsortKey == title then
		return '[[Kategori:' .. 'Headers with DefaultSort equal to page title' .. ']]'
	elseif defsortKey then
		return mw.getCurrentFrame():callParserFunction('DEFAULTSORT', {defsortKey}) .. '[[Kategori:' .. 'Headers applying DefaultSort key' .. ']]'
	end
	
	-- otherwise, don't do anything and use the default
	return ''
end

--[=[
Construct the year span
--]=]
local function getYearFromTimestamp(timestamp)
	-- example timestamps: +2016-10-05T00:00:00Z, -1752-00-00T00:00:00Z
	local split = mw.text.split(timestamp, '-', true)
	local year
	local bce = ''
	if split[1] == '' then
		year = tonumber(split[2])
		bce = ' BCE'
	else
		year = tonumber(split[1])
	end
	return {year = year, bce = bce}
end

local function formatYear(year, precision)
	if precision == 7 then
		local suffixes = {
			[1] = 'st',
			[2] = 'nd',
			[3] = 'rd',
			default = 'th'
		}
		local century = (year - year % 100)/100 + 1
		return century .. (suffixes[century] or suffixes['default']) .. ' century'
	elseif precision == 8 then
		return year .. 's'
	else
		return year
	end
end

local function getYearFromSingleStatement(statement, args)
	local snak = statement.mainsnak
	if not snak or not snak.datavalue or not snak.datavalue.value or not snak.datavalue.value.time then
		return nil
	end
	
	if args['noprint'] and args['nocat'] then
		return nil
	end
	
	local cats = {} -- tracking categories
	
	--[=[ Precision: 
		0 - billion years
		1 - hundred million years,
		2 - ten million years,
		3 - million years,
		4 - hundred thousand years,
		5 - ten thousand years,
		6 - millenia,
		7 - centuries,
		8 - decades,
		9 - years,
		10 - months,
		11 - days
		12 - hours
		13 - minutes
		14 - seconds
	]=]
	local precision = math.min(snak.datavalue.value.precision, 9)
	
	local year
	local circa = ''
	
	local start_times = {}
	local end_times = {}
	local start_years = {}
	local end_years = {}
	local start_year
	local end_year
	
	if statement.qualifiers then
		-- Check if date is approximate
		-- P1480 = sourcing circumstances, Q5727902 = circa
		if statement.qualifiers.P1480 then
			for _, qualifier in pairs(statement.qualifiers.P1480) do
				if qualifier.datavalue and qualifier.datavalue.value.id == 'Q5727902' then
					precision = precision - 1
					circa = 'c. '
					break
				end
			end
		end
		
		-- P580 = start time
		if statement.qualifiers.P580 then
			for k, v in pairs(statement.qualifiers.P580) do
				local startt = getYearFromSingleStatement({mainsnak = v}, {nocat = true, noprint = false, ['table'] = true})
				if startt then
					table.insert(start_times, startt)
				end
			end
			for k, v in pairs(start_times) do
				table.insert(start_years, v.year)
			end
			start_years = tableTools.removeDuplicates(start_years)
			table.sort(start_years)
			if #start_years > 1 then
				table.insert(cats, '[[Kategori:' .. 'Works of uncertain date' .. ']]')
			end
			start_year = start_years[1]
		end
		
		-- P582 = end time
		if statement.qualifiers.P582 then
			for k, v in pairs(statement.qualifiers.P582) do
				local endt = getYearFromSingleStatement({mainsnak = v}, {nocat = true, noprint = false, ['table'] = true})
				if endt then
					table.insert(end_times, endt)
				end
			end
			for k, v in pairs(end_times) do
				table.insert(end_years, v.year)
			end
			end_years = tableTools.removeDuplicates(end_years)
			table.sort(end_years)
			if #end_years > 1 then
				table.insert(cats, '[[Kategori:' .. 'Works of uncertain date' .. ']]')
			end
			end_year = end_years[1]
		end
	end
	
	if precision < 9 then
		table.insert(cats, '[[Kategori:' .. 'Works of uncertain date' .. ']]')
	end
	
	if precision < 7 then
		if args['noprint'] then
			year = ''
		else
			year = string.gsub(string.gsub(mw.wikibase.formatValue(statement.mainsnak), '^<span>', ''), '</span>$', '')
		end
		return circa .. year .. table.concat(cats)
	end
	
	local timestamp_info = getYearFromTimestamp(snak.datavalue.value.time)
	year = timestamp_info.year
	local bce = timestamp_info.bce
	
	-- approximate year, precision 8: year - year % 10 for decade
	-- approximate decade, precision 7: year - year % 100 for century
	if circa ~= '' then
		year = year - (year % math.pow(10, 9 - precision))
	end
	
	year = formatYear(year, precision)
	
	if start_year and start_year ~= year then
		table.insert(cats, '[[Kategori:' .. 'Works of uncertain date' .. ']]')
	end
	
	local printed_year = circa .. year .. bce
	
	if (start_year or end_year) and start_year ~= end_year then
		printed_year = circa .. (start_year or '') .. '–' .. (end_year or '') .. bce
	end
	if start_year and end_year and start_year ~= end_year then
		local start_decade = start_year - start_year % 10
		local end_decade = end_year - end_year % 10
		
		local start_century = start_year - start_year % 100
		local end_century = end_year - end_year % 100
		
		if start_decade == end_decade then
			table.insert(cats, '[[Kategori:' .. start_decade .. 's' .. bce .. ' works' .. ']]')
		elseif start_century == end_century then
			table.insert(cats, '[[Kategori:' .. formatYear(end_year, 7) .. bce .. ' works' .. ']]')
		end
	else
		table.insert(cats, '[[Kategori:' .. year .. bce .. ' works' .. ']]')
	end
	
	if args['noprint'] then
		printed_year = ''
	end
	if not args['nocat'] then
		cats = tableTools.removeDuplicates(cats)
		printed_year = printed_year .. table.concat(cats)
	end
	
	if args['table'] then
		return {year = year, printed_year = printed_year, circa = circa, bce = bce}
	end
	return printed_year
end

local function parse_wikidata_year_and_categorise(args)
	-- Fetch entity object for Wikidata item connected to the current page
	-- Let manually-specified Wikidata ID override if given and valid
	if not (args.wikidata and mw.wikibase.isValidEntityId(args.wikidata)) then
		args.wikidata = mw.wikibase.getEntityIdForCurrentPage()
	end
	local item = mw.wikibase.getEntity(args.wikidata)
	if not item then
		return nil
	end
	
	local statements = item:getBestStatements('P577') -- publication date
	if #statements == 0 then
		return nil
	end
	
	local years = {}
	for _, statement in pairs(statements) do
		local year = getYearFromSingleStatement(statement, args)
		if year then
			table.insert(years, year)
		end
	end
	
	local cat = ''
	years = tableTools.removeDuplicates(years)
	if #years == 0 then
		return nil
	elseif #years > 1 and not args['nocat'] then
		cat = '[[Kategori:' .. 'Works of uncertain date' .. ']]'
	end
	return table.concat(years, '/') .. cat
end

local function parse_year_and_categorise(args)
	if args['override-year'] then
		return args['override-year']
	end
	
	local year = args['year']
	
	-- Use Wikidata if year param is empty
	if not year then
		return parse_wikidata_year_and_categorise(args)
	end
	
	local cats = {}
	local bce = ''
	local circa = ''
	local ret = ''
	
	-- Extract common era info to make it easier to process
	if string.match(year, "BCE$") then
		bce = ' BCE'
		year = string.gsub(year, '%s*BCE', '')
		-- Also tag it as a non-numeric year
		table.insert(cats, '[[Kategori:' .. 'Works with non-numeric dates' .. ']]')
	end
	
	-- If the year provided is a plain year (all digits)
	if tonumber(year) then
		table.insert(cats, '[[Kategori:' .. year .. bce .. ' works]]')
		
		if not args['noprint'] then
			ret = year .. bce
		end
		if not args['nocat'] then
			ret = ret .. table.concat(cats)
		end
		
		-- For simple years we're done.
		return ret
	end
	
	-- Add tracking category for all non-numeric dates
	table.insert(cats, '[[Kategori:' .. 'Works with non-numeric dates' .. ']]')
	
	-- Explicitly tagged as being of unknown date
	if year == '?' or string.lower(year) == 'unknown' then
		table.insert(cats, '[[Kategori:' .. 'Works of unknown date' .. ']]')
		if not args['noprint'] then
			ret = 'unknown'
		end
		if not args['nocat'] then
			ret = ret .. table.concat(cats)
		end
		
		-- For explicitly given unknown years we're done.
		return ret
	end
	
	-- Now figure out a complex date
	
	-- Year ranges
	year = string.gsub(string.gsub(year, '%-', '–'), '—', '–')
	
	-- Approximate years
	-- Lua patterns can't do ^c(irca)?( |%.|/)* because they don't do alternation or apply quantifiers to groups
	if string.match(year, '^circa') or string.match(year, '^c%s*%.') or string.match(year, '^c%s*/') then
		circa = 'c. '
		year = string.gsub(string.gsub(string.gsub(year, '^circa%s*', ''), '^c%s*%.%s*', ''), '^c%s*/%s*', '')
		table.insert(cats, '[[Kategori:' .. 'Works of uncertain date' .. ']]')
		
		-- circa a specific year
		if tonumber(year) then
			year = tonumber(year)
			local decade = (year - year % 10) .. 's'
			
			table.insert(cats, '[[Kategori:' .. decade .. bce .. ' works]]')
			if not args['noprint'] then
				ret = circa .. year .. bce
			end
			if not args['nocat'] then
				ret = ret .. table.concat(cats)
			end
			
			-- For approximate years with a known decade we're done
			return ret
		end
	end
	
	-- Check if it looks like a decade
	if string.match(year, '^%d*0s$') then
		table.insert(cats, '[[Kategori:' .. 'Works of uncertain date' .. ']]')
		table.insert(cats, '[[Kategori:' .. year .. bce .. ' works]]')
		
		-- For explicitly given decades we're done
		if not args['noprint'] then
			ret = circa .. year .. bce
		end
		if not args['nocat'] then
			ret = ret .. table.concat(cats)
		end
		return ret
	end
	
	-- Or a century
	-- Lua patterns can't do '^%d*(st|nd|rd|th) century$'
	if string.match(year, '^%d*st century$') or string.match(year, '^%d*nd century$') or string.match(year, '^%d*rd century$') or string.match(year, '^%d*th century$') then
		table.insert(cats, '[[Kategori:' .. 'Works of uncertain date' .. ']]')
		table.insert(cats, '[[Kategori:' .. year .. bce .. ' works]]')
		
		-- For explicitly given centuries we're done
		if not args['noprint'] then
			ret = circa .. year .. bce
		end
		if not args['nocat'] then
			ret = ret .. table.concat(cats)
		end
		return ret
	end
	
	-- Or a range of years
	local start_year, end_year
	if string.match(year, '^%d*–%d*$') then
		start_year, end_year = string.match(year, '^(%d*)–(%d*)$')
	elseif string.match(year, '^%d*/%d*$') then
		start_year, end_year = string.match(year, '^(%d*)/(%d*)$')
		table.insert(cats, '[[Kategori:' .. 'Works of uncertain date' .. ']]')
	end
	
	if start_year or end_year then
		start_year = tonumber(start_year)
		end_year = tonumber(end_year)
		
		if start_year and end_year and start_year == end_year and circa == '' then
			table.insert(cats, '[[Kategori:' .. start_year .. bce .. ' works' .. ']]')
			if not args['noprint'] then
				ret = circa .. start_year .. bce
			end
			if not args['nocat'] then
				ret = ret .. table.concat(cats)
			end
			return ret
		elseif start_year and end_year then
			local start_decade = start_year - start_year % 10
			local end_decade = end_year - end_year % 10
			
			local start_century = start_year - start_year % 100
			local end_century = end_year - end_year % 100
			
			if start_decade == end_decade then
				table.insert(cats, '[[Kategori:' .. start_decade .. 's' .. bce .. ' works' .. ']]')
			elseif start_century == end_century then
				table.insert(cats, '[[Kategori:' .. formatYear(end_year, 7) .. bce .. ' works' .. ']]')
			end
		elseif end_year then
			table.insert(cats, '[[Kategori:' .. 'Works of uncertain date' .. ']]')
		end
		
		-- check isn't redundant since the year might just be –
		if start_year or end_year then
			if not args['noprint'] then
				ret = circa .. year .. bce
			end
			if not args['nocat'] then
				ret = ret .. table.concat(cats)
			end
			return ret
		end
	end
	
	-- If we're here we didn't manage to parse it.
	table.insert(cats, '[[Kategori:' .. 'Works with unrecognised dates' .. ']]')
	if not args['noprint'] then
		ret = circa .. year .. bce
	end
	if not args['nocat'] then
		ret = ret .. table.concat(cats)
	end
	
	-- Return whatever we have and the tracking cats
	return ret
end

local function construct_year(args)
	local year = mw.html.create('span'):attr('id', 'header-year-text')
	
	local year_args = {
		['year'] = args['year'],
		['override-year'] = args['override-year'],
		noprint = yesno(args['noyear']) or false,
		wikidata = args.wikidata
	}
	year_args['nocat'] = yesno(args['noyearcat'])
	if year_args['nocat'] == nil then
		if args.testing then
			year_args['nocat'] = false
		else
			year_args['nocat'] = (
				yesno(args['disambiguation']) -- Disambiguations never categorise
				or not current_title:inNamespaces(0, 114) -- Only categorize in mainspace and Translation
				or current_title.isSubpage -- Only categorise if this is a base page
			)
		end
	end
	local year_text = parse_year_and_categorise(year_args)
	
	if year_text then
		year:wikitext(year_text)
		if year_args['noprint'] then
			return tostring(year)
		else
			return '&nbsp;(' .. tostring(year) .. ')'
		end
	elseif year_args['nocat'] then
		return ''
	else
		return '[[Kategori:' .. 'Undated works' .. ']]'
	end
end

--[=[
Assemble the title
]=]
local function header_title(args)
	return table.concat({
		tostring(mw.html.create('span'):attr('id', 'header-title-text'):wikitext(args.title)),
		construct_year(args),
		headerAttributions.construct_attributions(args),
		headerAttributions.construct_section(args)
	})
end

--[=[
[[Template:Header]]
]=]
function p._header(args, argsWithBlanks)
	argsWithBlanks = argsWithBlanks or args
	
	-- aliases
	local dup_cat = ''
	local newArgs = {}
	
	for k, v in pairs(args) do
		local newkey = string.lower(string.gsub(string.gsub(tostring(k), '_', '-'), ' ', '-'))
		if newkey ~= tostring(k) then
			if argsWithBlanks[newkey] then
				dup_cat = '[[Kategori:' .. 'Pages using duplicate arguments in template calls' .. ']]'
			end
			if not args[newkey] then
				newArgs[newkey] = newArgs[newkey] or v
			end
		end
	end
	for k, v in pairs(newArgs) do
		args[k] = v
		argsWithBlanks[k] = v
	end
	
	newArgs = {}
	local aliases = {
		['section-author'] = 'contributor',
		['section-translator'] = 'contributing%-translator'
	}
	for arg, alias in pairs(aliases) do
		for k, v in pairs(args) do
			local newkey = string.gsub(k, alias, arg)
			if newkey ~= tostring(k) then
				if argsWithBlanks[newkey] then
					dup_cat = '[[Kategori:' .. 'Pages using duplicate arguments in template calls' .. ']]'
				end
				if not args[newkey] then
					newArgs[newkey] = v
				end
			end
		end
	end
	for k, v in pairs(newArgs) do
		args[k] = v
		argsWithBlanks[k] = v
	end
	
	args.sortkey = args.defaultsort or args.sortkey
	
	-- default values
	args.template_name = args.template_name or 'Header'
	args.title = args.title or parent_links({})
	args.testing = yesno(args.testing) or current_title.fullText == 'Template:Header/testcases' or current_title.fullText == 'Template:Translation header/testcases'
	
	-- default value for section (allow override by setting section to blank)
	if not argsWithBlanks['section'] and current_title.isSubpage then
		args['section'] = current_title.subpageText
	end
	
	-- header args
	
	args.pre_container = badge()
	args.header_class = 'wst-header ws-header ws-noexport noprint dynlayout-exempt ' .. (args.header_class or '')
	args.main_class = 'headertemplate'
	args.notes_id = args.notes_id or 'navigationNotes'
	args.language_name = ISO_639_language_name(args.language)
	
	-- title
	args.main_title = header_title(args)
	
	-- FIXME: just use Wikidata instead of interwiki links?
	local interwiki = ''
	if args.template_name == 'Translation header' and args.language then
		interwiki = tostring(mw.html.create('span'):addClass('interwiki-info'):attr('id', args.language):attr('title', '(original)'))
		if args.original and args.language_name then
			if args.language == 'ang' or args.language == 'enm' or args.language == 'sco' then
				-- cycle to mul.ws and back around to en.ws
				interwiki = interwiki .. '[[' .. args.language .. ':en:' .. args.original .. ']]'
			else
				-- general interwiki link
				interwiki = interwiki .. '[[' .. args.language .. ':' .. args.original .. ']]'
			end
		end
	end
	
	args.post_notes = table.concat({
		construct_microformat(args),
		check_subpages(),
		manual_categories(args),
		construct_categories(args),
		check_for_numerical_arguments(argsWithBlanks),
		construct_defaultsort(args),
		dup_cat,
		interwiki
	})
	
	return headerStructure.get_noexport_stylesheet('Header') .. headerStructure.construct_header(args)
end

function p.header(frame)
	return p._header(
		getArgs(frame),
		getArgs(frame, {removeBlanks = false})
	)
end

--[=[
[[Template:Translation header]]
]=]
function p._translation_header(args, argsWithBlanks)
	args.header_class = 'wst-translation-header'
	args.template_name = 'Translation header'
	args.main_id = 'translationheadertemplate'
	args.notes_id = 'translation-notes'
	args.notes_class = 'header-notes'
	
	return headerStructure.get_noexport_stylesheet('Translation header') .. p._header(args, argsWithBlanks)
end

function p.translation_header(frame)
	return p._translation_header(
		getArgs(frame),
		getArgs(frame, {removeBlanks = false})
	)
end

return p