پرش به محتوا

پودمان:پدیدآورنده/تمرین

از ویکی‌نبشته

توضیحات این پودمان می‌تواند در پودمان:پدیدآورنده/تمرین/توضیحات قرار گیرد.

require('strict')

local p = {}

-- Local variables
local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
local construct_header = require('Module:Header structure').construct_header
local numConv = require('پودمان:Number Converter').convert
local translate2en = require('پودمان:Number Converter')._translate2en

local dateModule = require('Module:Era')
local TableTools = require('Module:TableTools')
local ordinal = require('Module:Ordinal')._ordinal

local categories = {} -- List of categories to add page to.

local function preprocess(args)
	return mw.getCurrentFrame():preprocess(args)
end

local wd_properties = {
	birth = 'P569',
	death = 'P570',
	workperiodstart = 'P2031',
	workperiodend = 'P2032',
	flourit = 'P1317',
	
	familyname = 'P734',
	gender = 'P21',
	image = 'P18',
	instanceof = 'P31',
	medialegend = 'P2096',
	
	nationalities = 'P27',
	occupations = 'P106',
	religions = 'P140',
	movements = 'P135',
	ideologies = 'P1142',
	employer = 'P108',
	positionheld = 'P39',
	awardreceived = 'P166',
	memberof = 'P463',
	canonizationstatus = 'P411',
	contributedto = 'P3919',
	socialclassification = 'P3716',
	instrument = 'P1303',
	describedby = 'P1343'
}
-- Localization objects
local faTypes = {
	['birth'] = 'تولد',
	['death'] = 'مرگ',
	['تولد'] = 'تولد',
	['مرگ'] = 'مرگ',
	['floruit'] = 'فلوروت',
	['فلوروت'] = 'فلوروت'
}
local typeForCatName = {
	['تولد'] = 'زادگان',
	['مرگ'] = 'درگذشتگان'
}
--------------------------------------------------------------------------------
-- Add a category to the current list of categories. Do not include the Category prefix.
local function addCategory(category)
	table.insert(categories, category)
end
--------------------------------------------------------------------------------
-- Remove a category. Do not include the Category prefix.
local function removeCategory(category)
	for catPos, cat in pairs(categories) do
    	if cat == category then
    		table.remove(categories, catPos)
    	end
  	end
end

--------------------------------------------------------------------------------
-- Get wikitext for all categories added using addCategory.
local function getCategories()
	categories = TableTools.removeDuplicates(categories)
	table.sort(categories)
	local out = ''
	for _, cat in pairs(categories) do
		out = out .. '[[رده:' .. cat .. ']]'
	end
	return out
end

local function normalize_args(args)
	-- aliases
	local dup_cat = ''
	local oldKeys = {}
	local newArgs = {}
	
	for k, v in pairs(args) do
		local newkey = mw.ustring.lower(mw.ustring.gsub(mw.ustring.gsub(tostring(k), '-', '_'), ' ', '_'))
		if newkey ~= tostring(k) then
			if args[newkey] then
				addCategory('صفحه‌های دارای آرگومان تکراری در فراخوانی الگو')
			end
			newArgs[newkey] = newArgs[newkey] or v
			table.insert(oldKeys, tostring(k))
		end
	end
	for k, v in pairs(newArgs) do
		args[k] = v
	end
	for k, v in pairs(oldKeys) do
		args[v] = nil
	end
	
	args.wikidata = args.wikidata or args.wikidata_id
	args.wikidata_id = nil
	
	-- Fetch entity object for Wikidata item connected to the current page
	-- Let manually-specified Wikidata ID override if given and valid
	if args.wikidata and mw.wikibase.isValidEntityId(args.wikidata) then
		args.wd_entity = mw.wikibase.getEntity(args.wikidata)
	else
		args.wd_entity = mw.wikibase.getEntity()
	end
	
	args.use_initials = yesno(args.use_initials) ~= false and args.last_initial ~= '!NO_INITIALS'
	args.nocat = yesno(args.nocat) or false
	
	return args
end

--------------------------------------------------------------------------------
-- Get the actual parentheses-enclosed HTML string that shows the dates.
local function getFormattedDates(birthyear, deathyear)
	if not birthyear and not deathyear then
		return nil
	end
	
	local dates = '<br />('
	if birthyear then
		dates = dates .. birthyear
	end
	if birthyear ~= deathyear then
		-- Add spaces if there are spaces in either of the dates.
		local spaces = ''
		if mw.ustring.match((birthyear or '') .. (deathyear or ''), ' ') then
			spaces = ' '
		end
		dates = dates .. spaces .. '–' .. spaces
	end
	if deathyear and birthyear ~= deathyear then
		dates = dates .. deathyear
	end
	if birthyear or deathyear then
		dates = dates .. ')'
	end
	return numConv('fa', dates)
end

--------------------------------------------------------------------------------
-- Convert Gregorian year to Persian year given the month
local function gregorianToPersianYear(time)
	local year, month, day = mw.ustring.match(time, '(%d%d?%d?%d?)%-(%d%d?)%-(%d%d?)')
		month = tonumber(numConv('en', month))
		day = tonumber(numConv('en', day))
		year = tonumber(numConv('en', year))
		
	local difference = 622
	
	if month <= 3 then
		if month == 3 and day < 20 then
			difference = 621
		elseif month < 3 then
			difference = 621
		end
	end
	
	local pYear = year - difference
	
	if pYear <= 1288 then
		return nil
	end
	
	return pYear
end

--------------------------------------------------------------------------------
-- Utility function to prevent duplicated "(میلادی)" in "year" string
local function addCalendarDisambig(yearString)
	if mw.ustring.match(yearString, '%(میلادی%)') then
		return ''
	else
		return ' (میلادی)'
	end
end

--------------------------------------------------------------------------------
-- Take a statement of a given property and make a human-readable year string
-- out of it, adding the relevant categories as we go.
-- @param table statement The statement.
-- @param string type One of 'birth' or 'death'.
local function getYearStringFromSingleStatement(statement, year_type)
	local snak = statement.mainsnak
	-- We're not using mw.wikibase.formatValue because we only want years.
	
	-- No value. This is invalid for birth dates (should be 'somevalue'
	-- instead), and indicates 'still alive' for death dates.
	if snak.snaktype == 'novalue' and (year_type == 'birth' or year_type == 'تولد') then
		addCategory('پدیدآورندگان بدون سال تولد')
		return nil
	end
	if snak.snaktype == 'novalue' and (year_type == 'death' or year_type == 'مرگ') then
		addCategory('پدیدآورنده زنده')
		return nil
	end
	
	-- Unknown value.
	if snak.snaktype == 'somevalue' then
		addCategory('پدیدآورندگان دارای تاریخ ' .. faTypes[year_type] .. ' نامعلوم')
		return '?'
	end
	
	-- Extract year from the time value.
	local _, _, extractedYear = mw.ustring.find(snak.datavalue.value.time, '([%+%-]%d%d%d+)%-')
	local year = math.abs(tonumber(extractedYear))
	local pYear = gregorianToPersianYear(snak.datavalue.value.time) 			-- Persian year
	addCategory('پدیدآورندگان ' .. numConv('fa', dateModule.era(extractedYear)))
	
	 -- Century & millennium precision.
	if snak.datavalue.value.precision == 6 or snak.datavalue.value.precision == 7 then
		local ceilfactor = 100
		local precisionName = 'سده'
		if snak.datavalue.value.precision == 6 then
			ceilfactor = 1000
			precisionName = 'هزاره'
		end
		local cent = math.max(math.ceil(year/ceilfactor), 1)
		local suffixed_cent = numConv('fa', cent) .. ' (میلادی)'
		year = precisionName .. ' ' .. suffixed_cent
		addCategory('پدیدآورندگان دارای تاریخ ' .. faTypes[year_type] .. ' تقریبی')
	elseif snak.datavalue.value.precision == 8 then -- decade precision
		year = 'دهه ' .. numConv('fa', math.floor(tonumber(year)/10) * 10) .. ' (میلادی)'
		addCategory('پدیدآورندگان دارای تاریخ ' .. faTypes[year_type] .. ' تقریبی')
	end
	if tonumber(extractedYear) < 0 then
		year = numConv('fa', year) .. ' (پیش از میلاد)'
	end
	
	-- Remove from 'Living authors' if that's not possible.
	if tonumber(extractedYear) < tonumber(os.date('%Y') - 110) then
		removeCategory('پدیدآورندگان زنده')
	end
	
	-- Add to e.g. 'YYYY births' category (before we add 'c.' or 'fl.' prefixes).
	if faTypes[year_type] == 'تولد' or faTypes[year_type] == 'مرگ' then
		-- mw.logObject('Wikidata cat')
		-- mw.logObject(year .. ' ' .. year_type .. 's')
		addCategory(typeForCatName[faTypes[year_type]] .. ' ' .. numConv('fa', tostring(year)) .. addCalendarDisambig(year))
		if pYear then 
			addCategory(typeForCatName[faTypes[year_type]] .. ' ' .. numConv('fa', tostring(pYear)) .. ' (خورشیدی)')
		end
	end
	
	-- Extract circa (P1480 = sourcing circumstances, Q5727902 = circa)
	if statement.qualifiers and statement.qualifiers.P1480 then
		for _,qualifier in pairs(statement.qualifiers.P1480) do
			if qualifier.datavalue and qualifier.datavalue.value.id == 'Q5727902' then
				addCategory('پدیدآورندگان دارای تاریخ ' .. faTypes[year_type] .. ' تقریبی')
				year = 'حدود ' .. year
			end
		end
	end
	
	-- Add floruit abbreviation.
	if faTypes[year_type] == 'فلوروت' then
		year = 'فلوروت ' .. year
	end
	local snakString = ''
	for val in pairs(snak.datavalue.value) do
		snakString = snakString .. ' ' .. val
	end
	return year
end

--------------------------------------------------------------------------------
-- Get a given or family name property.
-- This concatenates (with spaces) all statements of the given property in order of the series ordinal (P1545) qualifier.
-- @TODO fix this.
local function getNameFromWikidata(item, property)
	local statements = item:getBestStatements(property)
	local out = {}
	if statements[1] and statements[1].mainsnak.datavalue then
		local itemId = statements[1].mainsnak.datavalue.value.id
		table.insert(out, mw.wikibase.label(itemId) or '')
	end
	return table.concat(out, ' ')
end

--------------------------------------------------------------------------------
local function getPropertyValue(item, property)
	local statements = item:getBestStatements(property)
	if statements[1] and statements[1].mainsnak.datavalue then
		return statements[1].mainsnak.datavalue.value
	end	
end

--------------------------------------------------------------------------------
-- The 'Wikisource' format for a birth or death year is as follows:
--     "?" or empty for unknown (or still alive)
--     Use BCE for years before year 1
--     Approximate dates:
--         Decades or centuries: "1930s" or "20th century"
--         Circa: "c/1930" or "c. 1930" or "ca 1930" or "circa 1930"
--         Tenuous year: "1932/?"
--         Choice of two or more years: "1932/1933"
-- This is a slightly overly-complicated function, but one day will be able to be deleted.
-- @param string type Either 'birth' or 'death'
-- @return string The year to display
local function formatWikisourceYear(year, year_type)
	if not year then
		return nil
	end
	
	-- mw.logObject('formatWikisourceYear')
	
	local yearParts = mw.text.split(year, '/', true)
	-- mw.logObject('yearParts')
	-- mw.logObject(yearParts)
	
	-- Ends in a question mark.
	if yearParts[2] == '?' or yearParts[2] == '؟' then
		addCategory('پدیدآورندگان دارای تاریخ ' .. faTypes[year_type] .. ' نامعلوم')
		if tonumber(yearParts[1]) then
			-- mw.logObject('unknown')
			-- mw.logObject(yearParts[1] .. ' ' .. year_type .. 's')
			addCategory('پدیدآورندگان ' .. dateModule.era(yearParts[1]))
			addCategory(typeForCatName[faTypes[year_type]] .. ' ' .. numConv('fa', yearParts[1]) .. ' (میلادی)')
		else
			addCategory('پدیدآورندگان دارای تاریخ ' .. faTypes[year_type] .. ' غیرعددی')
		end
		return yearParts[1] .. '؟'
	end
	
	-- Starts with one of the 'circa' abbreviations
	local circaNames = {'ca.', 'c.', 'ca', 'c', 'circa', 'حدود', 'ح.'}
	for _, circaName in pairs(circaNames) do
		local yearNumber
		local isCirca = false
		if yearParts[1] == circaName then
			yearNumber = mw.text.trim(yearParts[2])
			isCirca = true
		elseif mw.ustring.match(yearParts[1], '^' .. circaName) then
			yearNumber = numConv('en', mw.ustring.gsub(yearParts[1], '^' .. circaName, ''))
			isCirca = (tonumber(yearNumber) ~= nil)
		end
		
		if isCirca then
			addCategory('پدیدآورندگان دارای تاریخ ' .. faTypes[year_type] .. ' تقریبی')
			
			if tonumber(yearNumber) then
				yearNumber = tonumber(yearNumber)
				-- mw.logObject(yearNumber)
				addCategory('پدیدآورندگان ' .. dateModule.era(tostring(yearNumber)))
				addCategory(typeForCatName[faTypes[year_type]] .. ' ' .. numConv('fa', yearNumber) .. ' (میلادی)')
			else
				addCategory('پدیدآورندگان دارای تاریخ ' .. faTypes[year_type] .. ' غیرعددی')
			end
			
			return 'ح. ' .. numConv('fa', yearNumber)
		end
	end
	
	-- If there is more than one year part, and they're all numbers, add categories.
	local allPartsAreNumeric = true
	if #yearParts > 1 then
		for _, yearPart in pairs(yearParts) do
			if tonumber(numConv('en', yearPart)) then
				-- mw.logObject('numeric')
				-- mw.logObject(yearPart .. ' ' .. year_type .. 's')
				addCategory(typeForCatName[faTypes[year_type]] .. ' ' .. numConv('fa', yearPart) .. ' (میلادی)')
				addCategory('پدیدآورندگان ' .. dateModule.era(yearPart))
			else
				allPartsAreNumeric = false
			end
		end
		if allPartsAreNumeric then
			addCategory('پدیدآورندگان دارای تاریخ تولد تقریبی')
		end
	elseif #yearParts == 1 and not tonumber(numConv('en', year)) then
		allPartsAreNumeric = false
	end
	
	-- Otherwise, just use whatever's been given
	if not allPartsAreNumeric then
		addCategory('پدیدآورندگان دارای تاریخ ' .. faTypes[year_type] .. ' غیرعددی')
	end
	
	if #yearParts == 1 or allPartsAreNumeric == false then
		-- mw.logObject('not numeric')
		-- mw.logObject(year .. ' ' .. year_type .. 's')
		addCategory(typeForCatName[faTypes[year_type]] .. ' ' .. numConv('fa', year) .. ' (میلادی)')
	end
	return year
end

--[=[
Get a formatted year of the given property and add to the relevant categories
]=]
local function formatWikidataYear(item, year_type)
	-- Check sanity of inputs
	if not item or not year_type or not wd_properties[year_type] then
		return nil
	end
	
	local property = wd_properties[year_type]
	
	-- Get this property's statements.
	local statements = item:getBestStatements(property)
	if #statements == 0 then
		-- If there are no statements of this type, add to 'missing' category.
		if faTypes[year_type] == 'تولد' or faTypes[year_type] == 'مرگ' then
			addCategory('پدیدآورندگان با تاریخ ' .. faTypes[year_type] .. ' ناموجود')
		end
		local isHuman = item:formatPropertyValues(wd_properties['instanceof']).value == 'human'
		if faTypes[year_type] == 'مرگ' and isHuman then
			-- If no statements about death, assume to be alive.
			addCategory('پدیدآورندگان زنده')
		end
	end
	
	-- Compile a list of years, one from each statement.
	local years = {}
	for _, statement in pairs(statements) do
		local year = getYearStringFromSingleStatement(statement, year_type)
		if year then
			table.insert(years, year)
		end
	end
	years = TableTools.removeDuplicates(years)
	
	-- If no year found yet, try for a floruit date.
	if #years == 0 or table.concat(years, '/') == '?' or table.concat(years, '/') == '؟' then
		local floruitStatements = item:getBestStatements(wd_properties['flourit'])
		for _, statement in pairs(floruitStatements) do
			-- If all we've got so far is 'unknown', replace it.
			if table.concat(years, '/') == '?' or table.concat(years, '/') == '؟' then
				years = {}
			end
			addCategory('پدیدآورندگان دارای تاریخ‌های فلوروت')
			local year = getYearStringFromSingleStatement(statement, 'floruit')
			if year then
				table.insert(years, year)
			end
		end
	end
	years = TableTools.removeDuplicates(years)
	
	if #years == 0 then
		return nil
	end
	
	return table.concat(years, '/')
end

--------------------------------------------------------------------------------
-- Get a single formatted date, with no categories.
-- args.year, args.year_type, args.wd_entity
local function date(args)
	if args.year then
		return formatWikisourceYear(args.year, args.year_type)
	else
		return formatWikidataYear(args.wd_entity, args.year_type)
	end
end

--------------------------------------------------------------------------------
-- Get a formatted string of the years that this author lived,
-- and categorise in the appropriate categories.
-- The returned string starts with a line break (<br />).
local function dates(args)
	local outHtml = mw.html.create()
	
	--------------------------------------------------------------------------------
	-- Check a given title as having the appropriate dates as a disambiguating suffix.
	local function checkTitleDatesAgainstWikidata(title, wikidata)
		-- All disambiguated author pages have parentheses in their titles.
		local titleHasParentheses = mw.ustring.find(tostring(title), '%d%)')
		if not titleHasParentheses then
			return
		end
		
		-- The title should end with years in the same format as is used in the page header
		-- but with a normal hyphen instead of an en-dash.
		local dates = '(' .. (date({year_type = 'birth', wd_entity = args.wd_entity}) or '') .. '-' .. (date({year_type = 'death', wd_entity = args.wd_entity}) or '') .. ')'
		if mw.ustring.sub(tostring(title), -mw.ustring.len(dates)) ~= dates then 
			addCategory('پدیدآورندگان دارای تاریخ عنوان نامنطبق')
		end
	end
	
	-- Check disambiguated page titles for accuracy.
	checkTitleDatesAgainstWikidata(args.pagetitle or mw.title.getCurrentTitle(), args.wikidata)

	-- Get the dates (do death first, so birth can override categories if required):
	-- Death
	local deathyear
	local wikidataDeathyear = formatWikidataYear(args.wd_entity, 'death')
	local wikisourceDeathyear = formatWikisourceYear(args.deathyear, 'death')
	
	if args.deathyear then
		-- For Wikisource-supplied death dates.
		deathyear = wikisourceDeathyear
		addCategory('پدیدآورندگان دارای تاریخ مرگ نادیده‌گرفته‌شده')
		if args.wd_entity and wikisourceDeathyear ~= wikidataDeathyear then
			addCategory('پدیدآورندگان دارای تاریخ مرگ متفاوت با ویکی‌داده')
		end
		if tonumber(numConv('en', deathyear)) then
			addCategory('پدیدآورندگان ' .. dateModule.era(deathyear))
		end
	else
		deathyear = wikidataDeathyear
	end
	if not deathyear then
		addCategory('پدیدآورندگان دارای تاریخ مرگ ناموجود')
	end
	
	-- Birth
	local birthyear
	local wikidataBirthyear = formatWikidataYear(args.wd_entity, 'birth')
	local wikisourceBirthyear = formatWikisourceYear(args.birthyear, 'birth')
	if args.birthyear then
		-- For Wikisource-supplied birth dates.
		birthyear = wikisourceBirthyear
		addCategory('پدیدآورندگان دارای تاریخ تولد نادیده‌گرفته‌شده')
		if args.wd_entity and wikisourceBirthyear ~= wikidataBirthyear then
			addCategory('پدیدآورندگان دارای تاریخ تولد متفاوت با ویکی‌داده')
		end
		if tonumber(birthyear) then
			addCategory('پدیدآورندگان ' .. dateModule.era(birthyear))
		end
	else
		birthyear = wikidataBirthyear
	end
	if not birthyear then
		addCategory('پدیدآورندگان دارای تاریخ تولد ناموجود')
	end
	
	-- Put all the output together, including manual override of the dates.
	local dates = ''
	if args.dates then
		-- The parentheses are repeated here and in getFormattedDates()
		addCategory('پدیدآورندگان دارای تاریخ‌های نادیده‌گرفته‌شده')
		dates = '<br />(' .. args.dates .. ')'
	else
		dates = getFormattedDates(birthyear, deathyear)
	end
	
	if dates then
		outHtml:wikitext(dates)
		return tostring(outHtml)
	end
	
	return nil
end


--[=[
Match claims to configured categories.
Utility function for constructCategories.

Modifies the provided table to add categories configured in /data.
]=]
local function addCategoriesFromClaims(entity, pId, knownCategories)
	-- Abort if the provided category mappings are missing or undefined
	if not knownCategories then
		error('نگاشت‌های رده‌بندی تعریف نشده‌اند. لطفاً [[پودمان:پدیدآورنده/داده]] را بررسی کنید.')
	end
	
	-- Get statements for the property provided (ignore deprecated statements)
	local statements = entity:getBestStatements(pId)
	
	-- Get the category for each statement's value if a mapping exists
	for _, v in pairs(statements) do
		-- Sometimes the property exists on the item but has no value,
		-- or it has an unknown value,
		-- so in the output from mw.wikibase.getEntity()
		-- .mainsnak's .datavalue will be nil.		
		if v.mainsnak.snaktype == 'value' then
			local valueId = v.mainsnak.datavalue.value.id
			-- Add the category if we have a mapping for this statement
			local knownCat = knownCategories[valueId]
			if knownCat then
				addCategory(knownCat)
			end
		end
	end
end

--[=[
Get categories for nationality, occupations, etc.

Returns categories as a string of wikicode
]=]
local function constructCategories(entity)
	if not entity then
		return nil
	end
	
	-- Load the property to category mappings
	local DATA = mw.loadData('پودمان:پدیدآورنده/داده')
	
	-- Add categories from properties for which we have a configured mapping
	addCategoriesFromClaims(entity, wd_properties['nationalities'],   DATA.categories.nationalities)
	addCategoriesFromClaims(entity, wd_properties['occupations'],  DATA.categories.occupations)
	addCategoriesFromClaims(entity, wd_properties['religions'],  DATA.categories.religions)
	addCategoriesFromClaims(entity, wd_properties['movements'],  DATA.categories.movements)
	addCategoriesFromClaims(entity, wd_properties['ideologies'], DATA.categories.ideologies)
	addCategoriesFromClaims(entity, wd_properties['employer'],  DATA.categories.employer)
	addCategoriesFromClaims(entity, wd_properties['positionheld'],   DATA.categories.positionheld)
	addCategoriesFromClaims(entity, wd_properties['awardreceived'],  DATA.categories.awardreceived)
	addCategoriesFromClaims(entity, wd_properties['memberof'],  DATA.categories.memberof)
	addCategoriesFromClaims(entity, wd_properties['canonizationstatus'],  DATA.categories.canonizationstatus)
	addCategoriesFromClaims(entity, wd_properties['contributedto'], DATA.categories.contributedto)
	addCategoriesFromClaims(entity, wd_properties['socialclassification'], DATA.categories.socialclassification)
	addCategoriesFromClaims(entity, wd_properties['instrument'], DATA.categories.instrument)
	addCategoriesFromClaims(entity, wd_properties['describedby'], DATA.categories.describedby)
end

--------------------------------------------------------------------------------
-- Output link and category for initial letters of family name.
--
local function lastInitial(args)
	-- Handle special override
	if not args.use_initials then
		return nil
	end
	
	-- Allow manual override of initials.
	local initials = args.last_initial
	
	-- If a lastname is provided, get the initials from that.
	if not initials and args.lastname then
		initials = mw.ustring.sub(args.lastname, 1, 1)
	end
	
	-- Fetch from Wikidata.
	if not initials then
		local item = args.wd_entity
		if item then
			-- Get the first family name statement.
			local familyNames = item:getBestStatements(wd_properties['familyname'])
			if #familyNames > 0 then
				local familyNameId = familyNames[1].mainsnak.datavalue.value.id
				local familyName = mw.wikibase.getEntity(familyNameId)
				if familyName.labels and familyName.labels.fa then
					-- Take the first two characters of the English label
					-- (this avoids issues with 'native label P1705' and is fine for English Wikisource).
					initials = mw.ustring.sub(familyName.labels.fa.value, 1, 1) 
				end
			end
		end
	end
	
	-- Put it all together and output
	local out
	if initials then
		out = '[[ویکی‌نبشته:پدیدآورندگان-' .. initials .. '|شاخص پدیدآورنده: ' .. initials .. ']]'
		local authorCategory = mw.title.new('پدیدآورندگان-' .. initials, 'رده')
		addCategory(authorCategory.text)
		if authorCategory.exists ~= true then
			addCategory('صفحه‌های پدیدآورنده با رده حروف اول ناموجود')
		end
	else
		addCategory('پدیدآورندگان فاقد حروف اول')
		out = '[[:رده:پدیدآورندگان فاقد حروف اول|پدیدآورندگان فاقد حروف اول]]'
	end
	
	return out
end

--------------------------------------------------------------------------------
-- Header assembly
local function ucfirst(s)
	return mw.ustring.sub(s, 1, 1) .. mw.ustring.sub(s, 2)
end

local function gender_from_wd(wd_entity)
	if not wd_entity then
		return nil
	end
	local statements = wd_entity:getBestStatements(wd_properties['gender'])
	if #statements == 0 then
		return nil
	end
	
	local genders = {}
	for _, statement in pairs(statements) do
		local snak = statement.mainsnak
		if snak.snaktype ~= 'value'
			or not snak
			or snak.datatype ~= 'wikibase-item'
			or not snak.datavalue
			or not snak.datavalue.value
			or not snak.datavalue.value.id then
			break
		end
		local gender_item = mw.wikibase.getEntity(snak.datavalue.value.id)
		if not gender_item or not gender_item.labels or not gender_item.labels.en or not gender_item.labels.en.value then
			break
		end
		table.insert(genders, gender_item.labels.en.value)
	end
	
	return genders[1]
end

local function generateImagePlaceholder(imagesTable, image, caption, gender, defaults)
	if image then
		return table.insert(imagesTable, {image = image, image_caption = caption})
	elseif gender then
		if gender == 'transgender female' or gender == 'female' then
			return table.insert(imagesTable, {image = defaults['female'], image_caption = caption})
		elseif gender == 'transgender male' or gender == 'male' then
			return table.insert(imagesTable, {image = defaults['male'], image_caption = caption})
		else
			return table.insert(imagesTable, {image = defaults['female_and_male'], image_caption = caption})
		end
	else
		return table.insert(imagesTable, {image = defaults['female_and_male'], image_caption = caption})
	end
	
end

local function image_from_wd(wd_entity)
	local images = {}
	local gender = gender_from_wd(wd_entity)
	local defaultImages = {
		['male']			= 'Silver - replace this image male.svg',
		['female']			= 'Silver - replace this image female.svg',
		['female_and_male'] = 'Silver - replace this image female and male.svg'
	}
	
	if not wd_entity then
		generateImagePlaceholder(images, nil, '', gender, defaultImages)
	end
	
	local statements = wd_entity:getBestStatements(wd_properties['image'])
	
	if #statements == 0 then
		generateImagePlaceholder(images, nil, '', gender, defaultImages)
	end
	
	
	--[=[
	local langcode = mw.getCurrentFrame():callParserFunction('int', {'lang'})
	langcode = (mw.language.isKnownLanguageTag(langcode) and langcode) or mw.language.getContentLanguage().code
	local base_langcode = mw.text.split(langcode, '-', true)[1]
	local langcode_fallbacks_ordered = {base_langcode}
	for i, lang in ipairs(mw.language.getFallbacksFor(base_langcode)) do
		table.insert(langcode_fallbacks_ordered, lang)
	end
	if base_langcode ~= langcode then
		table.insert(langcode_fallbacks_ordered, 2, langcode)
		for i, lang in ipairs(mw.language.getFallbacksFor(langcode)) do
			table.insert(langcode_fallbacks_ordered, lang)
		end
	end
	
	local langcode_fallbacks = {}
	for i, v in ipairs(langcode_fallbacks_ordered) do
		langcode_fallbacks[v] = i
	end
	]=]
	
	local langcode_fallbacks = {[mw.language.getContentLanguage().code] = 1}
	
	for _, statement in pairs(statements) do
		local image = statement.mainsnak.datatype == 'commonsMedia' and statement.mainsnak.datavalue and statement.mainsnak.datavalue.value
		
		local captions = {}
		
		local legends = statement['qualifiers'] and statement['qualifiers'][wd_properties['medialegend']]
		
		if legends then
			for i, legend in ipairs(legends) do
				if legend.datatype == 'monolingualtext' and legend.datavalue and legend.datavalue.value and legend.datavalue.value.text and legend.datavalue.value.language then
					local legend_langcode = legend.datavalue.value.language
					local legend_base_langcode = mw.text.split(legend_langcode, '-', true)[1]
					local legend_text = legend.datavalue.value.text
					
					if langcode_fallbacks[legend_langcode] then
						captions[langcode_fallbacks[legend_langcode]] = legend_text
					elseif langcode_fallbacks[legend_base_langcode] then
						captions[langcode_fallbacks[legend_base_langcode]] = captions[langcode_fallbacks[legend_base_langcode]] or legend_text
					end
				end
			end
			captions = TableTools.compressSparseArray(captions)
		end
		generateImagePlaceholder(images, image, captions[1], gender, defaultImages)
	end
	
	return images[1]
end

function p.image_from_wd(wd_entity)
	return image_from_wd(wd_entity)
end

local function get_image(args)
	local image = args.image
	local wd_image_info = image_from_wd(args.wd_entity)
	local template_ns = args.template_ns or 'پدیدآورنده'
	
	if image then
		addCategory('صفحه‌های ' ..template_ns .. ' دارای تصویر')
	else
		image = wd_image_info.image
		if image then
			addCategory('صفحه‌های ' .. template_ns .. ' دارای تصویر از ویکی‌داده')
		else
			addCategory('صفحه‌های ' .. template_ns .. ' فاقد تصویر')
		end
	end
	
	local image_display = ''
	if image then
		local upright = (yesno(args.upright) and '|upright=0.6') or ''
		
		args.image_caption = args.image_caption or wd_image_info.image_caption or args.name_text
		
		local caption_div = (args.image_caption and tostring(mw.html.create('div'):css({['text-align'] = 'center'}):wikitext(args.image_caption))) or ''
		
		image_display = '[[File:' .. image .. '|thumb' .. upright .. '|' .. caption_div .. ']]'
	end
	
	return image_display
end

local function construct_defaultsort(args)
	local defaultsort = args.defaultsort
	if not defaultsort then
		if args.firstname and args.lastname then
			defaultsort = args.lastname .. '، ' .. args.firstname
		elseif args.firstname or args.lastname then
			defaultsort = (args.lastname or '') .. (args.firstname or '')
		end
	end
	if defaultsort then
		return mw.getCurrentFrame():callParserFunction('DEFAULTSORT', {defaultsort})
	end
	return ''
end

local function author(args)
	args = normalize_args(args)
	
	-- Always tell Header structure that we're {{author}}
	-- TODO: Is there a use case for letting our clients set this?
	args.template = 'author'

	local current_title = mw.title.getCurrentTitle()
	
	local firstname = args.firstname
	local lastname = args.lastname
	local last_initial = args.last_initial
	
	if firstname then
		firstname = preprocess(firstname)
	end
	if lastname then
		lastname = preprocess(lastname)
	end
	if last_initial then
		last_initial = preprocess(last_initial)
	end
	
	args.header_class = 'wst-author ws-header'
	
	-- main block
	
	args.main_class = 'authortemplate'
	
	args.next = lastInitial(args)
	
	if firstname and lastname then
		if yesno(args.invert_names) then
			args.name_text = lastname .. ' ' .. firstname
		else
			args.name_text = firstname .. ' ' .. lastname
		end
	else
		args.name_text = firstname or lastname
	end
	
	local name_text_span = ''
	if args.name_text then
		name_text_span = tostring(mw.html.create('span'):css({['font-weight'] = 'bold'}):wikitext(args.name_text))
	end
	
	local dates_text = ''
	if not yesno(args.disambiguation) then
		dates_text = dates(args) or ''
	end
	
	args.main_title = name_text_span .. dates_text
	
	-- notes block
	args.notes_class = 'author_notes'
	args.commonscat = args.commonscat or (
		args.wd_entity
		and args.wd_entity.claims
		and args.wd_entity.claims.P373
		and args.wd_entity.claims.P373[1]
		and args.wd_entity.claims.P373[1].mainsnak
		and args.wd_entity.claims.P373[1].mainsnak.datatype == 'string'
		and args.wd_entity.claims.P373[1].mainsnak.datavalue
		and args.wd_entity.claims.P373[1].mainsnak.datavalue.value
	)
	args.notes = args.description
	
	-- image
	local image_display = get_image(args)
	
	-- defaultsort
	args.defaultsort = args.defaultsort or args.sortkey
	local defaultsort_magicword = construct_defaultsort(args)
	
	-- categories
	if not args.categories then
		-- Author index category
		if last_initial then
			addCategory('پدیدآورندگان-' .. last_initial)
		elseif not last_initial then
			addCategory('پدیدآورندگان فاقد حروف اول')
		end
		
		-- Categorisation of author pages by gender, based on Wikidata sex or gender Property (P21)
		-- The main are: male (Q6581097), female (Q6581072), transgender female (Q1052281), transgender male (Q2449503)
		if args.wd_entity then
			local gender = gender_from_wd(args.wd_entity)
			if gender == 'transgender female' or gender == 'female' then
				addCategory('پدیدآورندگان زن')
				addCategory('صفحه‌های پدیدآورنده با جنسیت در ویکی‌داده')
			elseif gender == 'transgender male' or gender == 'male' then
				addCategory('پدیدآورندگان مرد')
				addCategory('صفحه‌های پدیدآورنده با جنسیت در ویکی‌داده')
			elseif gender then
				addCategory('صفحه‌های پدیدآورنده با جنسیت نامعلوم در ویکی‌داده')
			else
				addCategory('صفحه‌های پدیدآورنده فاقد جنسیت در ویکی‌داده')
			end
			if gender == 'transgender female' or gender == 'transgender male' then
				addCategory('پدیدآورندگان تراجنسی و تراجنسیتی')
			end
		else
			addCategory('صفحه‌های پدیدآورنده با رده‌بندی دستی جنسیت')
		end
		
		constructCategories(args.wd_entity)
		
		-- Categorisation of author pages with interwiki links (used for maintenance view only, links come from WD)
		if args.wikipedia then
			addCategory('صفحه‌های پدیدآورنده دارای پیوند به ویکی‌پدیا')
		end
		if args.wikiquote then
			addCategory('صفحه‌های پدیدآورنده دارای پیوند به ویکی‌گفتاورد')
		end
		if args.commons then
			addCategory('صفحه‌های پدیدآورنده دارای پیوند به ویکی‌انبار')
		end
		if args.commonscat then
			addCategory('صفحه‌های پدیدآورنده دارای پیوند به رده‌های ویکی‌انبار')
		end
		
		-- Whether page is connected to Wikidata
		if (args.namespace or current_title.nsText) == 'پدیدآورنده' then
			if args.wd_entity then
				addCategory('صفحه‌های پدیدآورنده متصل به ویکی‌داده')
			else
				addCategory('صفحه‌های پدیدآورنده فاقد اتصال به ویکی‌داده')
			end
		end
	end
	
	-- microformat
	local microformat = mw.html.create('div')
		:attr('id', 'ws-data')
		:addClass('vcard ws-noexport')
		:css({['display'] = 'none', ['speak'] = 'none'})
	local microformat_wikitext = {
		tostring(mw.html.create('span'):attr('id', 'ws-article-id'):wikitext(current_title.id))
	}
	if args.wd_entity then
		table.insert(
			microformat_wikitext,
			tostring(mw.html.create('span')
				:attr('id', 'wd-article-id')
				:wikitext(args.wd_entity.id)
			)
		)
	end
	if args.name_text then
		table.insert(
			microformat_wikitext,
			tostring(mw.html.create('span')
				:attr('id', 'ws-name')
				:addClass('fn')
				:wikitext(args.name_text)
			)
		)
		table.insert(
			microformat_wikitext,
			tostring(mw.html.create('span')
				:addClass('n')
				:wikitext(table.concat({
					tostring(mw.html.create('span'):addClass('given-name'):wikitext(firstname or '')),
					tostring(mw.html.create('span'):addClass('family-name'):wikitext(lastname or ''))
				}))
			)
		)
	end
	if args.defaultsort then
		table.insert(microformat_wikitext, tostring(mw.html.create('span'):attr('id', 'ws-key'):wikitext(args.defaultsort)))
	end
	if args.image then
		table.insert(microformat_wikitext, tostring(mw.html.create('span'):attr('id', 'ws-image'):wikitext(args.image)))
	end
	if args.birthyear then
		table.insert(
			microformat_wikitext,
			tostring(mw.html.create('span')
				:attr('id', 'ws-birthdate')
				:addClass('bday')
				:wikitext(date({['year_type'] = 'birth', ['year'] = args.birthyear}))
			)
		)
	end
	if args.deathyear then
		table.insert(
			microformat_wikitext,
			tostring(mw.html.create('span')
				:attr('id', 'ws-deathdate')
				:addClass('dday')
				:wikitext(date({['year_type'] = 'death', ['year'] = args.deathyear}))
			)
		)
	end
	if args.wikipedia then
		table.insert(microformat_wikitext, tostring(mw.html.create('span'):attr('id', 'ws-wikipedia'):wikitext(args.wikipedia)))
	end
	if args.wikiquote then
		table.insert(microformat_wikitext, tostring(mw.html.create('span'):attr('id', 'ws-wikiquote'):wikitext(args.wikiquote)))
	end
	if args.commonscat then
		table.insert(microformat_wikitext, tostring(mw.html.create('span'):attr('id', 'ws-commons'):wikitext(args.commonscat)))
	elseif args.commons then
		table.insert(microformat_wikitext, tostring(mw.html.create('span'):attr('id', 'ws-commons'):wikitext('Category' .. args.commons)))
	end
	if args.description then
		table.insert(microformat_wikitext, tostring(mw.html.create('span'):attr('id', 'ws-description'):addClass('note'):wikitext(args.description)))
	end
	microformat:wikitext(table.concat(microformat_wikitext))
	
	-- assemble
	local cats = ''
	if not args.nocat then
		cats = (args.categories or '') .. getCategories()
	end
	args.post_notes = defaultsort_magicword .. cats .. tostring(microformat)
	args.wikidataswitch = true
	
	return mw.getCurrentFrame():extensionTag('templatestyles', '', {src = 'الگو:پدیدآورنده/styles.css'}) .. construct_header(args)
end

-- for testing

function p._getCategories(args)
	args = normalize_args(args)
	p._author(args)
	return getCategories()
end
function p.getCategories(frame)
	return p._getCategories(getArgs(frame))
end

function p._date(args)
	args = normalize_args(args)
	args.year_type = args.year_type or args['type'] or 'birth'
	return date(args)
end
function p.date(frame)
	return p._date(getArgs(frame))
end

function p._lastInitial(args)
	args = normalize_args(args)
	return lastInitial(args)
end
function p.lastInitial(frame)
	return p._lastInitial(getArgs(frame))
end

-- Debugging 1: mw.log(p._lastInitial({last_initial = 'Qx'}))
-- Debugging 2: mw.log(p._lastInitial({wikidata = 'Q1107985'}))
-- Debugging 1: mw.log(p._lastInitial({lastname = 'Qqxxx'}))
-- Debugging 3: mw.log(p._lastInitial({last_initial = 'Qx', wikidata_id='Q1107985'}))

-- used by [[Module:Person]]

function p._dates(args)
	args = normalize_args(args)
	return dates(args)
end
function p.dates(frame)
	return p._dates(getArgs(frame))
end

function p._get_image(args)
	args = normalize_args(args)
	if args.nocat then
		return get_image(args)
	else
		return get_image(args) .. getCategories()
	end
end
function p.get_image(frame)
	return p._get_image(getArgs(frame))
end

function p._construct_defaultsort(args)
	args = normalize_args(args)
	return construct_defaultsort(args)
end
function p.construct_defaultsort(frame)
	return p._construct_defaultsort(getArgs(frame))
end

-- Used by [[Module:Disambiguation]] and [[Template:Author]]
function p._author(args)
	return author(args)
end
function p.author(frame)
	return p._author(getArgs(frame))
end

return p