Documentation for this module may be created at Module:Cite Q/sandbox/doc

require('strict')
local wdib = require('Module:WikidataIB')
local getValue = wdib._getValue
local getPropOfProp = wdib._getPropOfProp
local followQid = wdib._followQid

local simple_properties = { -- PXXX, is multiple?, linked?
	publisher = {id = "P123", maxvals = 1},
	oclc = {id = "P243", maxvals = 1},
	['publication-place'] = {id = "P291", maxvals = 0, linked = 'no'}, -- publication place (don't put into |place=; is treated specially in {{citation}} if both are given)
	doi = {id = "P356", maxvals = 1}, -- take care of |doi-broken-date= (WD "reason for deprecation"/"stated as") and |doi-access= (WD "access status")?
	issue = {id = "P433", maxvals = 0, populate_from_journal = true}, -- distinguish from |number= ("P1545"?) if both are given (still blocked by {{citation}}, but will be supported in the future)
	pmid = {id = "P698", maxvals = 1},
--	gbooks = {id = "P675", maxvals = 1}, -- to be added to {{citation}}
--	ia = {id = "P724", maxvals = 1}, -- to be added to {{citation}}
	arxiv = {id = "P818", maxvals = 1},
	bibcode = {id = "P819", maxvals = 1}, -- take care of |bibcode-access=?
	jstor = {id = "P888", maxvals = 1}, -- take care of |jstor-access=?
	mr = {id = "P889", maxvals = 1},
	rfc = {id = "P892", maxvals = 1},
	zbl = {id = "P894", maxvals = 1},
	ssrn = {id = "P893", maxvals = 1},
	place = {id = "P1071", maxvals = 0, linked = 'no'}, -- written-at place
--	['total-pages'] = {id = "P1104", maxvals = 0, linked = 'no'}, -- to be added to {{citation}} / COinS &rft.tpages=
--	coden = {id = "P1159", maxvals = 1}, -- to be added to {{citation}} / COinS &rft.coden=
	s2cid = {id = "P8299", maxvals = 1}, -- take care of |s2cid-access=?
	pmc = {id = "P932", maxvals = 1}, -- take care of |pmc-embargo-date= (WD "reason for deprecation")?
	lccn = {id = "P1144", maxvals = 1},
	hdl = {id = "P1184", maxvals = 1}, -- take care of |hdl-access=?
	ismn = {id = "P1208", maxvals = 1},
	journal = {id = "P1433", maxvals = 1},
	citeseerx = {id = "P3784", maxvals = 1},
	osti = {id = "P3894", maxvals = 1}, -- take care of |osti-access=?
	biorxiv = {id = "P3951", maxvals = 1},
	asin = {id = "P5749", maxvals = 1}, -- What about |asin-tld=? (WD examples resolve to .com at present, but may change)
--	['catalog-number'] = {id = "P528", maxvals = 0}, -- to be added to {{citation}} / COinS &rft.artnum=
	isbn = {id = "P212", maxvals = 1, populate_from_journal = true}, -- ISBN 13
	issn = {id = "P236", maxvals = 1, populate_from_journal = true}, -- distinguish from |eissn= for electronic issues?
--	jfm = {id = "P?", maxvals = 1}, -- Jahrbuch über die Fortschritte der Mathematik (not Zbl)
--	sbn = {id = "P?", maxvals = 1}, -- Standard Book Number (predecessor of ISBN, not ICCU)
--	message-id = {id = "P?", maxvals = 1}, -- Usenet message ID
	chapter = {id = "P792", maxvals = 1},
	['publication-date'] = {id = "P577", maxvals = 1, populate_from_journal = true}, -- publication date (don't use |date=; is treated specially in {{citation}} if both are given.)
	series = {id = "P179", maxvals = 1, populate_from_journal = true},
	version = {id = "P348", maxvals = 0},
	edition = {id = "P393", maxvals = 0},
	volume = {id = "P478", maxvals = 0, populate_from_journal = true},
--	part = {id = "P1545"?, maxvals = 0}, --  to be added to {{citation}} / COinS &rft.part=
	title = {id = "P1476", maxvals = 1},
	url = {id = "P953", maxvals = 1}, -- full work available at
	pages = {id = "P304", maxvals = 0, populate_from_journal = true},
	at = {id = "P958", maxvals = 0, populate_from_journal = true}, -- also incorporate lines (P7421) and columns (P3903) into this (cite map also supports |section=)
--	sheets = {id = "P7416", maxvals = 0, populate_from_journal = true},
--	interviewer = {id = "P?", maxvals = 0}, -- does **not** go to "others" section! Multiple interviewers should be n-enumerated
	illustrator = {id = "P110", maxvals = 0, others = true}, -- goes to "others" section

-- foreword and afterword, when contributions to another author's work, are contributions so belong in |contribution=;
-- the writer's name goes in |contributor=; requires |title= and |author=
-- However, this might need to add support for multiple contributors and their roles to {{citation}}, see Help_talk:Citation_Style_1#Others
--	foreword = {id = "P2679", maxvals = 0, others = true}, -- goes to "others" section
--	afterword = {id = "P2680", maxvals = 0, others = true}, -- goes to "others" section

	composer = {id = "P86", maxvals = 0, others = true}, -- goes to "others" section
	animator = {id = "P6942", maxvals = 0, others = true}, -- goes to "others" section
	director = {id = "P57", maxvals = 0, others = true}, -- goes to "others" section
	screenwriter = {id = "P58", maxvals = 0, others = true}, -- goes to "others" section
	signatory = {id = "P1891", maxvals = 0, others = true}, -- goes to "others" section
	presenter = {id = "P371", maxvals = 0, others = true}, -- goes to "others" section
	performer = {id = "P175", maxvals = 0, others = true}, -- goes to "others" section
}

local citeq = {}

--[[--------------------------< I S _ S E T >--------------------------------------------------------------


Returns true if argument is set; false otherwise. Argument is 'set' when it exists (not nil) or when it is not an empty string.

]]
local function is_set( var )
	return not (nil == var or '' == var);
end

--[[--------------------------< I N _ A R R A Y >--------------------------------------------------------------

Whether needle is in haystack (taken from Module:Citation/CS1/Utilities)

]]

local function in_array( needle, haystack )
	if needle == nil then
		return false;
	end
	for n, v in ipairs( haystack ) do
		if v == needle then
			return n;
		end
	end
	return false;
end


--[[--------------------------< A C C E P T _ V A L U E >-------------------------------------------------------

Accept WD value by framing in ((...)) if param_val is equal to keyword; else pass-through WD value as is.

]]

local function accept_value( param_val, wd_val )
	local val = param_val;

	if val then
		if in_array (val, {'accept', '))((', ':d:'}) then
			val = '((' .. wd_val .. '))';
		elseif '((accept))' == val then
			val = 'accept';
		elseif '(())(())' == val then
			val = '))((';
		elseif '((:d:))' == val then
			val = ':d:';
		else
			val = wd_val;
		end
	end

	return val;
end


--[=[-------------------------< G E T _ N A M E _ L I S T >----------------------------------------------------

get_name_list -- adapted from getAuthors code taken from Module:RexxS
arguments:
	nl_type - type of name list to fetch: nl_type = 'author' for authors; 'editor' for editors; 'translator' for translators
	args - pointer to the parameter arguments table from the template call
	qid - value from |qid= parameter; the Q-id of the source (book, etc.) in qid
	wdl - value from the |wdl= parameter; a Boolean passed to enable links to Wikidata when no article exists

returns nothing; modifies the args table

]=]

local function get_name_list (nl_type, args, qid, wdl)
	local propertyID = "P50"
	local fallbackID = "P2093" -- author name string
	
	if 'author' == nl_type then
		propertyID = 'P50';														-- for authors
		fallbackID = 'P2093';
	elseif 'editor' == nl_type then
		propertyID = 'P98';														-- for editors
		fallbackID = 'P5769'; -- "editor-in-chief" TBD. Only as fallback for now, should better be checked in parallel
		-- TBD. Take book series editors into account as well (if they have a separate P code as well)?
	elseif 'translator' == nl_type then
		propertyID = 'P655';														-- for translators
		fallbackID = nil;
--	elseif 'contributor' == nl_type then
--		f.e. author of forewords (P2679) and afterwords (P2680); requires |contribution=, |title= and |author=
--		propertyID = 'P';														-- for contributors
--		fallbackID = nil;
	else
		return;																	-- not specified so return
	end
	
	-- wdl is a Boolean passed to enable links to Wikidata when no article exists
	-- if "false" or "no" or "0" is passed set it false
	-- if nothing or an empty string is passed set it false
	if wdl and (#wdl > 0) then
		wdl = wdl:lower()
		wdl = in_array (wdl, {"false", "no", "0"})
	else
		-- wdl is empty, so
		wdl = false
	end
	
	local entity = mw.wikibase.getEntity(qid)
	local props = nil
	local fallback = nil
	if entity and entity.claims then
		props = entity.claims[propertyID]
		if fallbackID then
			fallback = entity.claims[fallbackID]
		end
	end
	
	-- Make sure it actually has at least one of the properties requested
	if not (props and props[1]) and not (fallback and fallback[1]) then 
		return nil
	end
	
	-- So now we have something to return:
	-- table 'out' is going to store the names(s):
	-- and table 'link' will store any links to the name's article
	local out = {}
	local link = {}
	local maxpos = 0
	if props and props[1] then
		for k, v in pairs(props) do
			local qnumber = "Q" .. v.mainsnak.datavalue.value["numeric-id"]
			local sitelink = mw.wikibase.sitelink(qnumber)
			local label = mw.wikibase.label(qnumber)
			if label then
				label = mw.text.nowiki(label)
			else
				label = qnumber
			end
			local position = maxpos + 1 -- Default to 'next' author.
			-- use P1545 (series ordinal) instead of default position.
			if v["qualifiers"] and v.qualifiers["P1545"] and v.qualifiers["P1545"][1] then
				position = tonumber(v.qualifiers["P1545"][1].datavalue.value)
			end
			maxpos = math.max(maxpos, position)
			if sitelink then
				-- just the plain name,
				-- but keep a record of the links, using the same index
				out[position] = label
				link[position] = sitelink
			else
				-- no sitelink, so check first for a redirect with that label
				-- this code works, but causes the article to appear in WhatLinksHere for the possible destination, so remove
				-- local artitle = mw.title.new(label, 0)
				-- if artitle.id > 0 then
				--	if artitle.isRedirect then
						-- no sitelink,
						-- but there's a redirect with the same title as the label;
						-- so store the link to that
				--		out[position] = label
				--		link[position] = label
				--	else
						-- no sitelink and not a redirect but an article exists with the same title as the label
						-- that's probably a dab page, so output the plain label
				--		out[position] = label
				--	end
				--else
				-- no article or redirect with the same title as the label
				if wdl then
					-- show that there's a Wikidata entry available
					out[position] = "[[:d:Q" .. v.mainsnak.datavalue.value["numeric-id"] .. "|" .. label .. "]]&nbsp;<span title='" .. i18n["errors"]["local-article-not-found"] .. "'>[[File:Wikidata-logo.svg|16px|alt=|link=]]</span>"
				else
					-- no Wikidata links wanted, so just give the plain label
					out[position] = label
				end
				-- end
			end
		end
	end
	if fallback and fallback[1] then
		-- Fallback to name-only authors / editors
		for k, v in pairs(fallback) do
			local label = v.mainsnak.datavalue["value"]
			local position = maxpos + 1 -- Default to 'next' author.
			-- use P1545 (series ordinal) instead of default position.
			if v["qualifiers"] and v.qualifiers["P1545"] and v.qualifiers["P1545"][1] then
				position = tonumber(v.qualifiers["P1545"][1].datavalue.value)
			end
			maxpos = math.max(maxpos, position)
			out[position] = label
		end
	end

	-- if there's anything to return, then insert the additions in the template arguments table
	-- in the form |author1=firstname secondname |author2= ...
	-- Renumber, in case we have inconsistent numbering
	local keys = {}
	for k, v in pairs(out) do
		keys[#keys + 1] = k
	end
	table.sort(keys) -- as they might be out of order
	for i, k in ipairs(keys) do
		mw.log(i .. " " .. k .. " " .. out[k])
		if args[nl_type .. i] then -- name gets overwritten
			-- pull corresponding -link only if overwritten name is same as WD name
			if link[k] and (args[nl_type .. i] == out[k]) then
				args[nl_type .. '-link' .. i] = args[nl_type .. '-link' .. i] or link[k] -- author-linkn or editor-linkn
			end
		else -- name does not get overwritten, so pull name from WD
			args[nl_type .. i] = out[k]
			if link[k] then
				args[nl_type .. '-link' .. i] = args[nl_type .. '-link' .. i] or link[k] -- author-linkn or editor-linkn
			end
		end
	end
end


--[[-------------------------< C I T E _ Q >------------------------------------------------------------------

Takes standard CS1|2 template parameters and passes all to {{citation}}.  If neither of |author= and |author1=
are set, calls get_authors() to try to get an author name-list from Wikidata.  The result is passed to 
{{citation}} for rendering.

]]

local function wrap_nowiki(str)
	return mw.text.nowiki(str or '')
end

function citeq.cite_q (frame)
	local citeq_args = {};
	local expand = ''; -- when set to anything, causes {{cite q}} to render <code><nowiki>{{citation|...}}</nowiki></code>

	for k, v in pairs(frame:getParent().args) do
		if in_array (k, {'expand', '_debug'}) then
			if is_set(v) then
				expand = v; -- record setting but don't pass |expand= to {{citation}}
			end
		else
			citeq_args[k] = v
		end
	end

	for k, v in pairs(frame.args) do
		citeq_args[k] = v
	end
	
	local qid = citeq_args.qid
	local wdl = citeq_args.wdl
	citeq_args.qid = nil
	citeq_args.wdl = nil
	
	local oth = {}
	
	citeq_args.language = citeq_args.language or getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P218", ps = 1} )
	if citeq_args.language == '' then
		citeq_args.language = nil
	end
	if not citeq_args.language then
		-- try fallback to journal's language
		local journal_qid = followQid({qid = qid, props = "P1433"})
		citeq_args.language = journal_qid and getPropOfProp( {qid = journal_qid, prop1 = "P407", prop2 = "P218", ps = 1} )
	end

	for name, data in pairs(simple_properties) do
		citeq_args[name] = getValue( {data.id, ps = 1, qid = qid, maxvals = data.maxvals, linked = data.linked, citeq_args[name] } )
		
		if data.populate_from_journal then
			citeq_args[name] = getValue( {"P1433", ps = 1, qid = qid, maxvals = 0, citeq_args[name], qual = data.id, qualsonly = 'yes'} )
			citeq_args[name] = citeq_args[name] or getPropOfProp({qid = qid, prop1 = "P1433", prop2 = data.id, maxvals = data.maxvals, ps = 1})
		end
		if citeq_args[name] and citeq_args[name]:find('[[Category:Articles with missing Wikidata information]]', 1, true) then
			-- try fallback to work's native language
			citeq_args[name] = getValue( {data.id, ps = 1, qid = qid, maxvals = data.maxvals, linked = "no", lang = citeq_args.language } )
			if citeq_args[name]:find('^Q%d+$') then -- qid was returned
				-- try fallback to qid's native language
				local qid_language = getPropOfProp( {qid = citeq_args[name], prop1 = "P407", prop2 = "P218", ps = 1} )
				citeq_args[name] = getValue( {data.id, ps = 1, qid = qid, maxvals = data.maxvals, linked = "no", lang = qid_language } )
				if citeq_args[name]:find('^Q%d+$') then -- qid was returned again
					citeq_args[name] = nil
				end
			end
		end
		if data.others then
			oth[#oth + 1] = citeq_args[name] and (name:gsub("^%l", string.upper) .. ": " .. citeq_args[name])
			citeq_args[name] = nil
		end
	end

	citeq_args.others = citeq_args.others or table.concat(oth, ". ")
	if '' == citeq_args.others then
		citeq_args.others = nil
	end

	citeq_args.journal = citeq_args.journal and citeq_args.journal:gsub("^''", ""):gsub("''$", ""):gsub("|''", "|"):gsub("'']]", "]]")

	citeq_args.ol = (getValue( {"P648", ps = 1, qid = qid, maxvals = 1, citeq_args.ol } ) or ''):gsub("^OL(.+)$", "%1")
	if '' == citeq_args.ol then
		citeq_args.ol = nil
	end
	-- TBD. Take care of |ol-access=?

	citeq_args.biorxiv = citeq_args.biorxiv and ("10.1101/" .. citeq_args.biorxiv)

	citeq_args.isbn = getValue( {"P957", ps = 1, qid = qid, maxvals = 0, citeq_args.isbn } ) -- try ISBN 10
	
	citeq_args.url = getValue( {"P856", ps = 1, qid = qid, maxvals = 0, citeq_args.url } ) -- try official website
	citeq_args.url = getValue( {"P2699", ps = 1, qid = qid, maxvals = 0, citeq_args.url } ) -- try url

	local slink = mw.wikibase.getSitelink(qid)
	local label = mw.wikibase.getLabel(qid) or citeq_args.language and mw.wikibase.getLabelByLang(qid, citeq_args.language)
	local slink_flag = false
	local wrap_title = ''
	if citeq_args.title then
		if slink then
			citeq_args.url = nil
			wrap_title = wrap_nowiki(citeq_args.title)
			slink_flag = true
		else
			citeq_args.title = wrap_nowiki(citeq_args.title)	
		end
	else
		if slink then
			citeq_args.url = nil
			if slink:lower() == label:lower() then
				citeq_args.title = '[[' .. slink .. ']]'
			else
				wrap_title = wrap_nowiki(slink:gsub("%s%(.+%)$", ""):gsub(",.+$", ""))
				slink_flag = true
			end
		else
			citeq_args.title = wrap_nowiki(label)
		end
	end
	if slink_flag then
		if slink == wrap_title then -- direct link
			citeq_args.title = '[[' .. slink .. ']]'
		else -- piped link
			citeq_args.title = '[[' .. slink .. '|' .. wrap_title .. ']]'
		end
	end

	-- TBD: incorporate |at, |sheets= and |sheet= here as well
	-- Sort out what should happen if several of them are given at the same time
	if citeq_args.page or citeq_args.p then -- let single take precedence over multiple
		citeq_args.pages = nil
		citeq_args.pp = nil
	end
	if citeq_args.pages then
		local _, count = string.gsub(citeq_args.pages, "[,;%s]%d+", "")
		if count == 1 then
			citeq_args.page = citeq_args.pages
			citeq_args.pages = nil
		end
	end

	if is_set (qid) then
		if not is_set (citeq_args.author) and not is_set (citeq_args.author1)
			and not is_set (citeq_args.subject) and not is_set (citeq_args.subject1)
			and not is_set (citeq_args.host) and not is_set (citeq_args.host1)
			and not is_set (citeq_args.last) and not is_set (citeq_args.last1)
			and not is_set (citeq_args.surname) and not is_set (citeq_args.surname1)
			and not is_set (citeq_args['author-last']) and not is_set (citeq_args['author-last1']) and not is_set (citeq_args['author1-last'])
			and not is_set (citeq_args['author-surname']) and not is_set (citeq_args['author-surname1']) and not is_set (citeq_args['author1-surname1']) then	-- if neither are set, try to get authors from Wikidata
			get_name_list ('author', citeq_args, qid, wdl);						-- modify citeq_args table with authors from Wikidata
		end

		if not is_set (citeq_args.editor) and not is_set (citeq_args.editor1)
			and not is_set (citeq_args['editor-last']) and not is_set (citeq_args['editor-last1']) and not is_set (citeq_args['editor1-last'])
			and not is_set (citeq_args['editor-surname']) and not is_set (citeq_args['editor-surname1']) and not is_set (citeq_args['editor1-surname']) then	-- if neither are set, try to get editors from Wikidata
			get_name_list ('editor', citeq_args, qid, wdl);						-- modify citeq_args table with editors from Wikidata
		end

		if not is_set (citeq_args.translator) and not is_set (citeq_args.translator1)
			and not is_set (citeq_args['translator-last']) and not is_set (citeq_args['translator-last1']) and not is_set (citeq_args['translator1-last'])
			and not is_set (citeq_args['translator-surname']) and not is_set (citeq_args['translator-surname1']) and not is_set (citeq_args['translator1-surname']) then	-- if neither are set, try to get translators from Wikidata
			get_name_list ('translator', citeq_args, qid, wdl);						-- modify citeq_args table with translators from Wikidata
		end
	end

	for k, v in pairs(citeq_args) do
		if in_array (v, {'(())', 'unset', 'ignore'}) or 'string' ~= type(k) then -- empty accept-as-is-written (()) markup to indicate an empty/unused parameter value, other ((...)) markups are deliberately passed down to {{citation}}
			citeq_args[k] = nil
		elseif in_array (v, {'((unset))', '((ignore))'}) then -- strip off markup for free-text values clashing with local keywords
			citeq_args[k] = 'unset'
		end
	end
	
	-- local author_count = 0
	-- for k, v in pairs(citeq_args) do
	--	if k:find("^author%d+$") then
	--		author_count = author_count + 1
	--	end
	-- end
	-- if author_count > 8 then -- convention in astronomy journals, optional mode for this?
	--	citeq_args['display-authors'] = citeq_args['display-authors'] or 3
	-- end
	
	-- local editor_count = 0
	-- for k, v in pairs(citeq_args) do
	-- 	if k:find("^editor%d+$") then
	-- 		editor_count = editor_count + 1
	-- 	end
	-- end
	-- if editor_count > 8 then -- convention in astronomy journals, optional mode for this?
	--	citeq_args['display-editors'] = citeq_args['display-editors'] or 3
	-- end
	
	-- |id= could hold more than one identifier pulled from Wikidata not supported by {{citation}}, right now only add our qid to the list
	local list_sep = '. ';
	if citeq_args.mode ~= 'cs1' then
		list_sep = ', ';
	end
	local id = '[[WDQ (identifier)|Wikidata]]&nbsp;[[:d:' .. qid .. '|' .. qid .. ']]'; -- go through "WDQ (identifier)" redirect to reduce clutter in "What links here" and improve reverse lookup. Keep in sync with {{QID}}.
	local old_id = citeq_args.id;
	if wdl then -- show WD logo
		id = id .. '[[File:Wikidata-logo.svg|16px|alt=|link=]]'; -- possibly replace by WD edit icon?
	end
	if is_set (old_id) then
		citeq_args.id = old_id .. list_sep .. id; -- append to user-specified contents
	else
		citeq_args.id = id;
	end

	if is_set(expand) then																-- if |expand=<anything>, write a nowiki'd version to see what the {{citation}} template call looks like
		local expand_args = {'{{citation'};										-- init with citation template
		if 'self' == expand then
			citeq_args.id = old_id; -- restore original |id= parameter
			expand_args = {'{{cite Q|' .. qid}; -- expand to itself
		end
		for p, v in pairs (citeq_args) do										-- spin through citeq_args and 
			table.insert (expand_args, p .. '=' .. v);								-- add parameter name = value
		end
																				-- make the nowiki'd string and done
		return table.concat ({'<code>', frame:callParserFunction ('#tag:nowiki', table.concat (expand_args, ' |') .. '}}'), '</code>'})
	end

	return frame:expandTemplate{title = 'citation', args = citeq_args} -- render the template
end

return citeq