Documentation for this module may be created at Module:Plain sister/doc

require('strict')

local p = {}

local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')

-- table of site data
local sites = { 
-- interwiki prefix:	parameter,		label and				site id (for Wikidata)
	['w'] =				{'wikipedia',	'Wikipedia article',	'enwiki'},
	['c'] =				{'commons',		'Commons gallery',		'commonswiki'},
	['c:Category'] =	{'commonscat',	'Commons category',		'commonswiki'},
	['q'] =				{'wikiquote',	'quotes',				'enwikiquote'},
	['n'] =				{'wikinews',	'news',					'enwikinews'},
	['wikt'] =			{'wiktionary',	'definition',			'enwiktionary'},
	['b'] =				{'wikibooks',	'textbook',				'enwikibooks'},
	['v'] =				{'wikiversity',	'course',				'enwikiversity'},
	['wikispecies'] =	{'wikispecies',	'taxonomy',				'specieswiki'},
	['voy'] =			{'wikivoyage',	'travel guide',			'enwikivoyage'},
	['d'] =				{'wikidata',	'Wikidata item',		'wikidatawiki'},
	['m'] =				{'meta',		'Meta',					'metawiki'}
}

-- sites is display order (keyed as above)
local sites_in_order = {'w', 'c', 'c:Category', 'q', 'n', 'wikt', 'b', 'v', 'wikispecies', 'voy', 'd', 'm'}

-- some properties are not wanted from certain transitive links
-- for example, the P921 (main topic) should not add the Commons category
-- this is a map of WD property -> WD site ID keys
local transitiveLinkBlacklist = {
	P921 = {'commonswiki', 'wikiquote', 'wikinews', 'wiktionary', 'wikiversity', 'wikivoyage', 'meta'},
}

--------------------------------------------------------------------------------
-- Get the item associated with the current page, or specified by the 'wikidata'
-- parameter (of either the module invocation, or the parent template).
-- @return mw.wikibase.entity
local function getItem(args)
	local item = nil
	-- Firstly, see if the calling tempate or module has a "wikidata" argument.
	if args.wikidata then
		item = mw.wikibase.getEntity(args.wikidata)
	end
	-- Failing that just use the current page's item.
	if item == nil then
		item = mw.wikibase.getEntity()
	end
	return item
end

--------------------------------------------------------------------------------
-- Get the page title of the first sitelink found on the target item for the
-- given property.
-- @return string|nil
local function getFirstSitelink(item, property, sitename)
	local statements = item:getBestStatements(property)
	if #statements > 0 then
		-- Go through each 'edition of' statement.
		for _, statement in pairs(statements) do
			-- datavalue is missing if set to "unknown value"
			if statement['mainsnak']['datatype'] == 'wikibase-item'
					and statement['mainsnak']['datavalue'] then
				local otherItemId = statement['mainsnak']['datavalue']['value']['id']
				local sitelink = mw.wikibase.getSitelink(otherItemId, sitename)
				-- If the parent has the required sitelink, return it.
				if sitelink ~= '' and sitelink ~= nil then
					-- mw.log(sitename, property, sitelink)
					return sitelink
				end
			end -- if
		end
	end
	return nil
end

local function listContains(list, item)
	for _, v in pairs(list) do
		if v == item then
			return true
		end
	end
	return false
end

local function transitivePropertyBlacklisted(prop, wdSitelinkKey)
	-- reject prop/key pairs that we don't want
	local blacklisted = transitiveLinkBlacklist[prop] and
		listContains(transitiveLinkBlacklist[prop], wdSitelinkKey)
	return blacklisted
end

function p.getLinks(args)
	local item = getItem(args)
	
	local links = {}
	
	-- Build all the wikitext links.
	for prefix, site in pairs(sites) do
		local val = nil
		local wd_sitelink_key = site[3]
		local arg_name = site[1]
		
		-- Allow overriding of individual sitelinks.
		if args[arg_name] then
			val = args[arg_name]
		end
		
		if not val and wd_sitelink_key ~= '' and item then -- fetch it from wikidata
			val = item:getSitelink(wd_sitelink_key)
			if wd_sitelink_key == 'wikidatawiki' and item.id then
				val = item.id 
			elseif wd_sitelink_key == 'commonswiki' and val then -- we have link to commons 
				local catFlag = (#val>9 and string.sub(val, 1, 9) == 'Category:')
				if (arg_name == 'commonscat' and catFlag==false) or (arg_name=='commons' and catFlag==true) then
					val = nil -- link is to a wrong namespace so let's nuke it
				elseif (arg_name =='commonscat' and catFlag==true) then
					val = string.sub(val,10) -- trim 'Category:' from the front
				end
			end
		end
		-- Commons gallery.
		if not val and arg_name == 'commons' and item then
			local statements = item:getBestStatements('P935') -- get commons gallery page from P935 property
			if statements[1] and statements[1].mainsnak.datavalue then
				val = statements[1].mainsnak.datavalue.value
			end
		end 
		-- Commons category.
		if not val and arg_name == 'commonscat' and item then
			local statements = item:getBestStatements('P373') -- get commons category page from P373 property
			if statements[1] and statements[1].mainsnak.datavalue then
				val = statements[1].mainsnak.datavalue.value 
			end
		end

		-- edition or translation of (P629)
		-- category's main topic (P301)
		-- Wikimedia portal's main topic (P1204)
		-- main subject (P921)
		if item then
			for _,prop in pairs({ 'P629', 'P301', 'P1204', 'P921' }) do
				if not val and not transitivePropertyBlacklisted(prop, wd_sitelink_key) then
					local workSitelink = getFirstSitelink(item, prop, wd_sitelink_key)
					if workSitelink ~= nil then
						val = workSitelink
						break
					end
				end
			end
		end

		if val then
			links[prefix] = val
		end
	end
	
	-- tidy up redundancies in the WD data
	
	-- strip redundant commons category prefix
	if links['c:Category'] then
		links['c:Category'] = links['c:Category']:gsub('^Category:', '')
	end
	
	-- the gallery is exactly the same as the category, so just keep the category
	if links['c'] and links['c:Category']
			and ('Category:' .. links['c:Category']) == links['c'] then
		links['c'] = nil
	end
	
	return links
	
end

--------

local function construct_sisicon_span(args)
	return mw.html.create('span')
		:addClass('sisicon')
		:wikitext('[[File:' .. args.image .. '|frameless|18px|link=' .. args.link .. '|alt=' .. args.alt .. ']]')
end

-- Get an HTML list of all links to all sister projects.
function p._interprojectPart(args)
	local item = getItem(args)
	
	local link_data = p.getLinks(args)
	local links = {}
	
	-- iterate the links in the desired order and construct Wikitext links
	for k, v in pairs(sites_in_order) do
		if link_data[v] then
			local display = sites[v][2]
			local target = v .. ':' .. link_data[v]
			table.insert(links, '[[' .. target .. '|' .. display .. ']]')
		end
	end
	
	if #links == 0 then -- links table length is zero
		return nil
	end
	
	return mw.html.create('li')
		:addClass('sisitem')
		:node(construct_sisicon_span({
			image = 'Wikimedia-logo.svg',
			link = 'Special:sitematrix',
			alt = 'Sister Projects.'
		}))
		:wikitext('[[Special:sitematrix|sister projects]]: ' .. table.concat(links, ', ') .. '.')
end

function p.interprojectPart(frame)
	return p._interprojectPart(getArgs(frame))
end

local function slashed_list_to_links(itemString, prefix)
	local items = mw.text.split(itemString, '%s*/%s*', false)
	local itemLinks = {}
	for _, item in pairs(items) do
		table.insert(itemLinks, '[[' .. (prefix or '') .. item .. '|' .. item .. ']]')
	end
	return table.concat(itemLinks, ', ')
end

function p._plain_sister(args)
	local current_frame = mw.getCurrentFrame()
	local current_title = mw.title.getCurrentTitle()
	local pagename = current_title.text
	local item = getItem(args)
	
	-- construct list
	local ul_list = mw.html.create('ul'):addClass('plainSister')
	
	if yesno(args.disambiguation) then
		local dabItem = '<span class="dabicon">[[File:Disambiguation.svg|frameless|17px|link=WS:STYLE#Disambiguation.2C_versions_and_translations_pages|alt=Style Guide for disambiguation, version and translation pages.]]</span>'
			.. 'Search for titles <span class="selfreference">[[Special:Search/intitle:"' .. pagename .. '"|containing]]</span> or '
		if current_title:inNamespaces(14) then
			dabItem = dabItem .. '[[Special:Categories/' .. pagename .. '|beginning]]'
		elseif current_title:inNamespaces(0) then
			dabItem = dabItem .. '[[Special:PrefixIndex/' .. current_title.fullText .. '|beginning]]'
		else
			dabItem = dabItem .. '[[Special:PrefixIndex/' .. current_title.fullText .. '|beginning (in ' .. current_title.nsText .. 's)]]'
		end
		dabItem = dabItem .. ' with: "' .. pagename .. '."'
		ul_list:tag('li')
			:addClass('dabitem')
			:wikitext(dabItem)
	end
	
	local show_textinfo = args.textinfotitle or yesno(args.textinfo or args.edition)
	if show_textinfo then
		local edition_title
		if args.textinfotitle then
			edition_title = mw.title.new(args.textinfotitle)
		else
			edition_title = current_title
		end
		ul_list:tag('li')
			:addClass('sisitem')
			:node(construct_sisicon_span({
				image = 'Information_icon.svg',
				link = 'Template:Textinfo',
				alt = 'Documentation for the TextInfo template.',
			}))
			:wikitext('[[' .. edition_title.talkNsText .. ':' .. edition_title.text .. '|information about this edition]].')
	end
	
	local portal = args.portal or args.portals
	if portal then
		ul_list:tag('li')
			:addClass('sisitem')
			:node(construct_sisicon_span({
				image = 'Wikisource-logo.svg',
				link = 'Portal:Portals',
				alt = 'Related Portals.'
			}))
			:wikitext('[[Portal:Portals|related portals]]: ' .. slashed_list_to_links(portal, 'Portal:') .. '.')
	end
	
	local related_author = args.related_author or args.related_authors or args['related-author'] or args['related-authors']
	if related_author then
		ul_list:tag('li')
			:addClass('sisitem')
			:node(construct_sisicon_span({
				image = 'System-users.svg',
				link = 'Wikisource:Authors',
				alt = 'Related Authors.'
			}))
			:wikitext('[[Wikisource:Authors|related authors]]: ' .. slashed_list_to_links(related_author, 'Author:') .. '.')
	end
	
	local related_work = args.related_work or args.related_works or args['related-work'] or args['related-works']
	if related_work then
		ul_list:tag('li')
			:addClass('sisitem')
			:node(construct_sisicon_span({
				image = 'Nuvola apps bookcase.svg',
				link = 'Wikisource:Works',
				alt = 'Related Works.'
			}))
			:wikitext('[[Wikisource:Works|related works]]: ' .. slashed_list_to_links(related_work) .. '.')
	end
	
	local sisters = p._interprojectPart(args)
	if sisters then
		ul_list:node(sisters)
	end
	
	if yesno(args.wikidataswitch) and not item then
		local wdSearch = '<span class="sisicon">[[File:Wikidata-logo.svg|frameless|17px|link=Wikisource:Wikidata|alt=Wikidata.]]</span>'
			.. '[[d:Special:Search/' .. pagename .. '|Search Wikidata]].'
		ul_list:tag('li')
			:addClass('sisitem')
			:wikitext(wdSearch)
	end
	
	if not yesno(args.disambiguation) and not show_textinfo and not portal and not related_author and not related_work and not sisters and not yesno(args.wikidataswitch) then
		return nil
	end
	
	return current_frame:extensionTag('templatestyles', '', {src = 'Plain sister/styles.css'}) .. tostring(ul_list)
end

function p.plain_sister(frame)
	return p._plain_sister(getArgs(frame))
end

return p

-- Debug console testing:
-- =p.interprojectPart(mw.getCurrentFrame():newChild{title='nop',args={wikidata='Q23308118'}})