Permanently protected module

Module:Header

From Wikisource
Jump to navigation Jump to search

--[=[
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('Module:Yesno')
local getArgs = require('Module:Arguments').getArgs
local TableTools = require('Module:TableTools')
local ISO_639_language_name = require('Module:ISO 639').language_name
local parent_links = require('Module:Auto parents')._parent_links

local construct_header = require('Module:Header structure').construct_header
local headerAttributions = require('Module:Header/attribution')
local construct_defaultsort = require('Module:Header/sort')._construct_defaultsort
local construct_year = require('Module:Header/year').construct_year

local current_title = mw.title.getCurrentTitle()

--[=[
Wrap stylesheet in noexport div
]=]
local function get_noexport_stylesheet(template)
	return tostring(mw.html.create('div'):addClass('ws-noexport'):wikitext(mw.getCurrentFrame():extensionTag('templatestyles', '', {src = template .. '/styles.css'})))
end

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

--[=[
Detect explicit formatting in fields like 'section' and 'title'
]=]
local function explicit_formatting(str)
	return str and (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, categories, checkArgs)
	-- check for cases that aren't supposed to produce a valid link
	
	local param = checkArgs.param
	local tracking_cat = checkArgs.tracking_cat or 'Works with non-existent author pages'
	
	if not param or not args[param] or yesno(args[param .. '-nolink']) then
		return
	end
	
	local lower_arg = string.lower(args[param])
	local attr_data = headerAttributions.attr_data[param] or headerAttributions.attr_data[string.gsub(param, 'section%-', '')]
	if attr_data and attr_data['special_cases'] and attr_data['special_cases'][lower_arg] then
		return
	end
	
	-- check if page exists
	local target = mw.title.makeTitle('Author', args[param])
	-- expensive function!
	if not target or not target.exists then
		table.insert(categories, tracking_cat)
	end
	
	return
end

--[=[
Construct the automatic categories for the header
]=]
local function language_category_name(cat_works_start, lang)
	local cat_language_name = ISO_639_language_name(lang) or 'an undefined language'
	if lang == 'el' then
		cat_language_name = 'Modern Greek'
	end
	return cat_works_start .. ' ' .. cat_language_name
end

local function construct_categories(args, argsWithBlanks)
	local categories = {}
	
	-- categorize 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
		table.insert(categories, 'Subpages')
	elseif parent_exists then
		table.insert(categories, title.nsText .. ' subpages')
	end
	
	-- add categories from the categories parameter
	local manual_categories = (args.categories and mw.text.split(args.categories, '%s*/%s*', false)) or {}
	local using_cat = false
	for i, category in ipairs(manual_categories) do
		local cat = mw.text.trim(category)
		if cat ~= '' then
			table.insert(categories, cat)
			using_cat = true
		end
	end
	if using_cat then
		table.insert(categories, 'Works using categories parameter')
	end
	
	local known_override_types = {
		['default'] = 'contributor type',
		['author'] = 'author',
		['translator'] = 'translator',
		['section-author'] = 'contributor'
	}
	for k, v in pairs(argsWithBlanks) do
		-- Check for numerical parameters (which shouldn't be used)
		if type(k) == 'number' then
			table.insert(categories, 'Headers with numerical arguments')
		-- Check for 'override-' parameters
		elseif string.match(k, '^override%-') then
			local contrib_type = mw.text.split(k, '-', true)
			contrib_type = string.gsub(contrib_type[2], '%d*$', '')
			table.insert(categories, 'Pages with override ' .. (known_override_types[contrib_type] or known_override_types['default']))
		end
	end
	
	-- check contributor parameters
	
	if args['section-author'] then
		table.insert(categories, 'Pages with contributor')
	end
	
	local editor = args['override-editor'] or args['editor']
	if editor and not args['nocat'] 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 and not args['nocat'] 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
	
	local author = args['override-author'] or args['author']
	if author and (string.lower(author) == 'unknown') and not args['nocat'] then
		if args.template_name == 'Translation header' then
			table.insert(categories, 'Translations of anonymous works')
		else
			table.insert(categories, 'Anonymous texts')
		end
	end
	
	-- check for non-existent contributor pages
	if current_title:inNamespaces(0, 114) or args.testing then
		local params_to_check = {
			{param = 'author'},
			{param = 'editor'},
			{param = 'translator'},
			{param = 'composer', tracking_cat = 'Works with non-existent composer pages'},
			{param = 'illustrator', tracking_cat = 'Works with non-existent illustrator pages'}
		}
		for k, v in pairs(params_to_check) do
			check_non_existent_author_pages(args, categories, v)
			check_non_existent_author_pages(args, categories, {param = 'section-' .. v.param, tracking_cat = v.tracking_cat})
		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['noyear'] then
		table.insert(categories, 'Pages with noyear')
	end
	if args['noyearcat'] then
		table.insert(categories, 'Pages with noyearcat')
	end
	if args['nolanguagecat'] then
		table.insert(categories, 'Pages with nolanguagecat')
	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 args['allow-explicit-formatting'] and (explicit_formatting(args['title']) or explicit_formatting(args['section'])) then
		table.insert(categories, 'Pages with explicit formatting in header fields')
	end
	
	-- translation header categories
	local isMainPage = ((current_title:inNamespaces(0, 114) and not current_title.isSubpage) or args['testing'])
	
	if not args['nocat'] and isMainPage then
		local cat_works_start = 'Works originally in'
		local cat_translations_start = 'Translations'
		
		if args.template_name == 'Translation header' then
			cat_works_start = 'Wikisource translations of works in'
			cat_translations_start = 'Wikisource translations'
		end
		
		if not args['nolanguagecat'] then
			if args['languages'] then
				for i, lang in ipairs(args['languages']) do
					table.insert(categories, language_category_name(cat_works_start, lang))
				end
			end
			if args['interwiki-prefix'] then
				table.insert(categories, language_category_name(cat_works_start, args['interwiki-prefix']))
			end
		end
		
		if not args['languages'] and args['language-required'] then
			table.insert(categories, cat_translations_start .. ' with no original language')
		end
		
		if args.template_name == 'Translation header' and not args.original then
			table.insert(categories, 'Wikisource translations with no original source')
		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
	]=]
	
	categories = TableTools.removeDuplicates(categories)
	
	local category_links = {}
	for k, v in pairs(categories) do
		table.insert(category_links, '[[Category:' .. v .. ']]')
	end
	return table.concat(category_links)
end

--[=[
For debugging
]=]
--[=[
function p.construct_categories(args)
	return construct_categories(args, args)
end
]=]

--[=[
Assemble the title
]=]
local function header_title(args)
	local title = args.title or ''
	local titleSpan = tostring(mw.html.create('span'):attr('id', 'header-title-text'):wikitext(title))
	
	local year = construct_year(args)
	local attr = headerAttributions.construct_attributions(args)
	local section = headerAttributions.construct_section(args)
	
	if attr ~= '' and title ~= '' then
		attr = tostring(mw.html.create('br'):attr('id', 'header-title-break')) .. attr
	end
	
	return table.concat({titleSpan, year, attr, section})
end

local function get_languages(args, prefix)
	prefix = (prefix and prefix .. '%-') or ''
	
	-- language handling
	local languages = {}
	for k, v in pairs(args) do
		local n
		local nText = string.match(k, '^' .. prefix .. 'language%d*$')
		if nText then
			n = string.gsub(nText, 'language(%d*)$', '%1')
			n = tonumber(n) or 1
			languages[n] = v
		end
	end
	languages = TableTools.compressSparseArray(languages)
	if #languages == 0 then
		return {}
	end
	
	local language_name
	local language_names = {}
	for i, lang in ipairs(languages) do
		local name = ISO_639_language_name(lang)
		if name then
			table.insert(language_names, name)
		end
	end
	
	if #language_names == 1 then
		language_name = language_names[1]
	elseif #language_names > 1 then
		language_name = table.concat(language_names, ', ', 1, #language_names - 1) .. ' and ' .. language_names[#language_names]
	end
	
	return {
		languages = languages,
		language_name = language_name
	}
end

--[=[
[[Template:Header]]
]=]
function p._header(args, argsWithBlanks)
	argsWithBlanks = argsWithBlanks or args
	
	-- aliases
	local dup_cat
	local newArgs = {}
	
	local aliases = {
		['section-author'] = 'contributor',
		['section-translator'] = 'contributing%-translator'
	}
	for k, v in pairs(args) do
		local newkey = string.lower(string.gsub(string.gsub(tostring(k), '_', '-'), ' ', '-'))
		for arg, alias in pairs(aliases) do
			newkey = string.gsub(newkey, alias, arg)
		end
		if newkey ~= tostring(k) then
			if argsWithBlanks[newkey] then
				dup_cat = 'Pages using duplicate arguments in template calls'
			end
			if not args[newkey] then
				newArgs[newkey] = newArgs[newkey] or v
			end
		end
		if newkey == 'testing' or newkey == 'nocat' or newkey == 'nolanguagecat' or newkey == 'language-required' or string.match(newkey, '%-nolink$') then
			newArgs[newkey] = newArgs[newkey] or yesno(v)
		end
	end
	for k, v in pairs(newArgs) do
		args[k] = v
		argsWithBlanks[k] = v
	end
	if dup_cat then
		table.insert(categories, dup_cat)
	end
	
	args.sortkey = args.defaultsort or args.sortkey
	
	-- default values
	args.template_name = args.template_name or 'Header'
	
	if args.testing == nil then
		args.testing = current_title.fullText == 'Template:Header/testcases' or current_title.fullText == 'Template:Translation header/testcases'
	end
	
	-- noyearcat has different behavior for nil and false
	args.noyearcat = args.nocat == true or nil
	
	local language_res = get_languages(args)
	args.languages = language_res.languages
	args.language_name = language_res.language_name
	
	local section_language_res = get_languages(args, 'section')
	args.section_languages = section_language_res.languages
	args.section_language_name = section_language_res.language_name
	
	if not args['interwiki-prefix'] and args['languages'] then
		if #(args['languages']) > 1 then
			args['interwiki-prefix'] = 'mul'
		else
			args['interwiki-prefix'] = args['languages'][1]
		end
	end
	
	-- add values to argsWithBlanks
	for k, v in pairs(args) do
		if not argsWithBlanks[k] then
			argsWithBlanks[k] = v
		end
	end
	
	-- default values for title and section (allow override by setting to blank)
	if not argsWithBlanks['title'] then
		args['title'] = parent_links({})
		argsWithBlanks['title'] = args['title']
	end
	if not argsWithBlanks['section'] and current_title.isSubpage then
		args['section'] = current_title.subpageText
		argsWithBlanks['section'] = args['section']
	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'

	-- 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['interwiki-prefix'] then
		interwiki = tostring(mw.html.create('span'):addClass('interwiki-info'):attr('id', args['interwiki-prefix']):attr('title', '(original)'))
		if args.original and (args['interwiki-prefix'] == 'ang' or args['interwiki-prefix'] == 'enm' or args['interwiki-prefix'] == 'sco') then
			-- cycle to mul.ws and back around to en.ws
			interwiki = interwiki .. '[[' .. args['interwiki-prefix'] .. ':en:' .. args.original .. ']]'
		elseif args.original then
			-- general interwiki link
			interwiki = interwiki .. '[[' .. args['interwiki-prefix'] .. ':' .. args.original .. ']]'
		end
	end
	
	-- set defaultsort tracking categories
	args.equalsortcat = '[[Category:' .. 'Headers with DefaultSort equal to page title' .. ']]'
	args.diffsortcat = '[[Category:' .. 'Headers applying DefaultSort key' .. ']]'
	
	args.post_notes = table.concat({
		headerAttributions.construct_microformat(args),
		construct_categories(args, argsWithBlanks),
		construct_defaultsort(args),
		interwiki
	})
	
	return get_noexport_stylesheet('Header') .. 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)
	argsWithBlanks = argsWithBlanks or args
	
	args.header_class = 'wst-translation-header'
	args.template_name = 'Translation header'
	args.notes_class = 'header-notes'
	args['language-required'] = true
	
	return 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