Для документации этого модуля может быть создана страница Модуль:External links/doc

local data = require( 'Module:External links/data' )
-- Localizable part
-- Please note that labels for websites and catalogs are taken from Wikidata (i.e. Wikidata label)

-- Feel free to correct labels and categories here
local categoryTemplateEmpty = 'Википедия:Шаблон «Внешние ссылки» пуст'
local categoryCustomColor = 'Википедия:Шаблон «Внешние ссылки» с нестандартным цветом'
local templateLink = 'Внешние ссылки'
local wikidataLink = 'Перейти к элементу Викиданных'
local refLabels = {
	Q1406 = 'Windows',
	Q10677 = 'PS1',
	Q10680 = 'PS2',
	Q10683 = 'PS3',
	Q16338 = 'ПК',
	Q135321 = 'FDS',
	Q170323 = 'DS',
	Q170325 = 'PSP',
	Q172742 = 'NES',
	Q182172 = 'GameCube',
	Q183259 = 'SNES',
	Q184839 = 'N64',
	Q188642 = 'GBA',
	Q188808 = 'PS Vita',
	Q203597 = '3DS',
	Q5014725 = 'PS4',
	Q19610114 = 'Switch',
	Q63184502 = 'PS5',
}

-- The language codes that should always be displayed even if they have normal rank and a claim with another language and preferred rank exists
local preferredLanguage = 'Q7737' -- russian

local templateColorName = 'цвет'
-- Some projects have "named" colors, defined by templates
local function colorByTitle( frame, colorTitle )
	local templateName = 'Цвет/' .. colorTitle
	local templateTitle = mw.title.makeTitle( 'Template', templateName )
	if not templateTitle or not templateTitle.exists then
		return false
	end
	return frame:expandTemplate{ title = templateName }
end

-- Non-localizable part (not need to localize )
local moduleNavbox = require( 'Module:Navbox' )
local moduleLanguages -- accessed if necessary

local propertyQualifiers = {
	P553 = {
		P554 = 'url',
	},
	P1343 = {
		P805 = 'iw',
		P248 = 'iw', -- deprecated, fallback for P805
		P953 = 'url',
		P854 = 'url', -- deprecated, fallback for P953
	},
}

local p = {}


-- Helper functions
local function replace( str, pattern, repl )
    pattern = mw.ustring.gsub( pattern, "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1" ) -- escape pattern
    repl = mw.ustring.gsub( repl, "[%%]", "%%%%" ) -- escape replacement
    repl = mw.ustring.gsub( repl, " ", "%%%%20" ) -- escape replacement
    return mw.ustring.gsub( str, pattern, repl )
end


-- Render functions
local function renderList( elements )
	if #elements == 0 then
		return ''
	end

	return '*' .. table.concat( elements , '\n*' ) .. '\n'
end

local function renderLabel( params )
	if type( params ) == 'string' then
		return params
	end

	local qid = params[ 1 ]
	local default = params[ 2 ]

	if #params >= 3 then
		local label = params[ 3 ]
		local link = mw.wikibase.sitelink( qid )
		if link then
			return '[[' .. link .. '|' .. label .. ']]'
		end
		local title = mw.wikibase.label( qid ) or default
		if title ~= label then
			return '<span title="' .. title .. '" style="border-bottom: 1px dotted; cursor: help;">' .. label .. '</span>'
		end
	end

	return mw.wikibase.label( qid ) or default
end

local function renderLink( resourceData, label, formatter, idAsLabel )
	if resourceData.itemId == nil then
		return '<span class="error">' .. label .. ': Не удаётся определить элемент</span>[[Категория:Статьи с ошибкой в шаблоне Внешние ссылки]]'
	end

	local link
	if not formatter then
		link = resourceData.itemId
		idAsLabel = false
	elseif type( formatter ) == 'string' then
		link = replace( formatter, '$1', resourceData.itemId )
	elseif type ( formatter ) == 'function' then
		link = formatter( resourceData.itemId, resourceData.qualifiers )
	end
	
	-- "Label: ID" without link
	if not link or link == '' then
		return renderLabel( label ) .. ':&nbsp;' .. resourceData.itemId
	end

	local resourceLabel = resourceData.itemId
	if not idAsLabel then
		resourceLabel = renderLabel( label )
	end

	local linkFirstChar = mw.ustring.sub( link, 1, 1 )
	if linkFirstChar == ':' then
		return '[[' .. link .. '|' .. resourceLabel .. ']]'
	end

	return '[' .. link .. ' ' .. resourceLabel .. ']'
end

local function renderLangRef( languages )
	local result = ''
	if languages and #languages > 0 then
		if moduleLanguages ~= false then -- not false, but maybe nil
			if mw.title.makeTitle( 'Module', 'Languages' ).exists
					and mw.title.makeTitle( 'Module', 'Languages/data' ).exists
					and mw.title.makeTitle( 'Module', 'Wikidata/Language-codes' ).exists then
				moduleLanguages = require( 'Module:Languages' )
			else
				moduleLanguages = false
			end
		end
		
		if moduleLanguages then
			for langIndex, language in pairs( languages ) do
				result = result .. '&nbsp;' .. moduleLanguages.getWikidataRefHtml( language )
			end
		end
	end

	return result
end

local function renderRefs( refs )
	local result = ''
	if refs and #refs > 0 then
		for _, ref in ipairs( refs ) do
			result = result .. '&nbsp;<span class="ref-info">(' .. ref .. ')</span>'
		end
	end

	return result
end

local function renderLinkWithRef( resourceData, label, formatter, idAsLabel )
	local link = renderLink( resourceData, label, formatter, idAsLabel )
	if link ~= '' then
		link = link .. renderLangRef( resourceData.languages )
		link = link .. renderRefs( resourceData.refs )
	end
	return link
end


-- Data fetching functions
local function getValueFromSnak( snak )
	if not snak.datavalue then 
		return 
	end
	if snak.datavalue.type == 'wikibase-entityid' then
		return snak.datavalue.value.id
	end
	if snak.datavalue.type == 'monolingualtext' then
		return snak.datavalue.value.text
	end
	return snak.datavalue.value
end

local function getQualifierSingleValue( statement, qualifierName )
	if not statement or not statement.qualifiers or not statement.qualifiers[ qualifierName ] then
		return nil
	end

	for qualifierIndex, qualifier in pairs( statement.qualifiers[ qualifierName ] ) do
		if qualifier.datavalue and qualifier.datavalue.type and qualifier.datavalue.value then
			return getValueFromSnak( qualifier )
		end
	end

	return nil
end

local function getQualifierValues( statement )
	if not statement or not statement.qualifiers then
		return {}
	end

	local result = {}

	for qualifierName, qualifiers in pairs( statement.qualifiers ) do
		for _, qualifier in pairs( qualifiers ) do
			if qualifier.datavalue and qualifier.datavalue.type and qualifier.datavalue.value then
				if not result[ qualifierName ] then
					result[ qualifierName ] = {}
				end
				table.insert( result[ qualifierName ], getValueFromSnak( qualifier ) )
			end
		end
	end

	return result
end

local function contains( tableStructure, value )
	if not tableStructure or not value then
		return true
	end
	for index, line in pairs( tableStructure ) do
		if line == value then
			return true
		end
	end
	return false
end

local function filterByRank( resourceDatas )
	-- itemId, languages. rank = rank

	local hasPreffered = false
	for index, resourceData in pairs( resourceDatas ) do
		if resourceData.rank == 'preferred' then
			hasPreffered = true
		end
	end

	if not hasPreffered then
		return resourceDatas
	end

	local result = {}
	for index, resourceData in pairs( resourceDatas ) do
		if resourceData.rank == 'preferred' or contains( resourceData.languages, preferredLanguage ) then
			table.insert( result, resourceData )
		end
	end

	return result
end

local function getLinkData( statement, qualifier, project )
	local rank = statement.rank or 'normal'
	if rank == 'deprecated' or not statement.mainsnak.datavalue then
		return nil
	end

	local itemId
	if qualifier then
		itemId = getQualifierSingleValue( statement, qualifier )
	else
		itemId = statement.mainsnak.datavalue.value
	end

	if itemId and project then
		itemId = mw.wikibase.getSitelink( itemId, project )
	end

	if not itemId then
		return nil
	end

	local qualifiers = getQualifierValues( statement )
	local languages = qualifiers[ 'P407' ]
	if not languages then
		languages = {}
	end

	local refs = {}
	if qualifiers[ 'P400' ] then
		for _, refQid in ipairs( qualifiers[ 'P400' ] ) do
			local refLabel = refLabels
			if refLabels[ refQid ] then
				refLabel = refLabels[ refQid ]
			else
				refLabel = mw.wikibase.label( refQid ) or refQid
			end
			if refLabel ~= refQid then
				if #qualifiers[ 'P400' ] > 3 then
					refLabel = refLabel .. ' +&nbsp;' .. ( #qualifiers[ 'P400' ] - 1 ) .. '&nbsp;платформ'
					table.insert( refs, refLabel )
					break
				end

				table.insert( refs, refLabel )
			end
		end
	end

	return {
		itemId = itemId,
		qualifiers = qualifiers,
		languages = languages,
		refs = refs,
		rank = rank,
	}
end

local function collectLinks( configuration, elementId, separateLabel )
	-- Create rows
	local elements = {}
	local data = {}

	local item = mw.wikibase.getEntity( elementId )
	if item == nil or item.claims == nil then
		return elements
	end

	for _, params in pairs( configuration ) do
		local resourceId = params
		local pid = resourceId[ 2 ]
		local qid
		if string.match( pid, '^P%d+:Q%d+$' ) then
			local parts = mw.text.split( pid, ':', true )
			pid = parts[ 1 ]
			qid = parts[ 2 ]
		end

		local claims = item.claims[ pid ] or {}
		for _, statement in pairs( claims ) do
			local linkData
			if not qid then
				linkData = getLinkData( statement )
			elseif getValueFromSnak( statement.mainsnak ) == qid then
				for qualifierId, qualifierType in pairs( propertyQualifiers[ pid ] ) do
					local project = nil
					if qualifierType == 'iw' then
						project = params.project
					end
					linkData = getLinkData( statement, qualifierId, project )
					if linkData then
						break
					end
				end
			end

			if linkData then
				if not data[ resourceId ] then
					data[ resourceId ] = {}
				end
				table.insert( data[ resourceId ], linkData )
			end
		end
	end

	for resourceId, resourceDatas in pairs( data ) do
		data[ resourceId ] = filterByRank( resourceDatas )
	end

	for _, params in pairs( configuration ) do
		local resourceId = params
		local resourceDatas = data[ resourceId ]
		if resourceDatas ~= nil then
			local links = {}
			for _, resourceData in pairs( resourceDatas ) do
				if separateLabel then
					-- Label: ID1, ID2
					table.insert( links, renderLinkWithRef( resourceData, '', params[ 3 ], true ) )
				else
					-- Label · Label
					local label = params[ 1 ]
					table.insert( elements, renderLinkWithRef( resourceData, label, params[ 3 ] ) )
				end
			end
			if separateLabel and #links then
				local result = renderLabel( params[ 1 ] ) .. ': ' .. table.concat( links, ', ' )
				table.insert( elements, result )
			end
		end
	end

	return elements
end


function p.render( frame )
	local colorArg = ''
	local byTemplate
	local elementId = nil
	if frame ~= nil then
		local parentArgs = frame:getParent().args
		colorArg = parentArgs[ templateColorName ] or parentArgs[ 'color' ] or parentArgs[ 1 ] or ''
		if parentArgs[ 'from' ] and parentArgs[ 'from' ] ~= '' then
			elementId = string.upper( parentArgs[ 'from' ] )
		elseif parentArgs[ 'd' ] and parentArgs[ 'd' ] ~= '' then
			elementId = string.upper( parentArgs[ 'd' ] )
		end
		if colorArg ~= '' then
			local firstChar = mw.ustring.sub( colorArg, 1, 1 )
			if firstChar ~= '#' then
				byTemplate = colorByTitle( frame, colorArg )
				if byTemplate then
					colorArg = byTemplate
				end
			end
		end
	end

	local navboxData = {
		name  = 'External links',
		navboxclass = 'navbox ruwikiArticleExternalLinksTable',
		bodyclass = 'hlist',
	}
	if colorArg and colorArg ~= '' then
		navboxData.groupstyle = 'background: ' .. colorArg .. ';'
	end

	local rowIndex = 1

	for _, groupData in pairs( data ) do
		local isAuthorityControl = groupData.isAuthorityControl
		local groupLabel = groupData.label
		local groupList = groupData.list
		local groupElements = collectLinks( groupList, elementId, isAuthorityControl )
		if #groupElements > 0 then
			navboxData[ 'group' .. rowIndex ] = groupLabel
			navboxData[ 'list' .. rowIndex ] = renderList( groupElements )

			if isAuthorityControl then
				if #groupElements > 5 then
					navboxData[ 'group' .. rowIndex ] = nil
					local templateStyles = frame:extensionTag{
						name = 'templatestyles',
						args = { src = 'Шаблон:Навигационная таблица/styles.css' }
					}
					local collapsibleNavbox = moduleNavbox._navbox( {
						title = groupLabel,
						list1 = navboxData['list' .. rowIndex],
						border = 'subgroup',
						navbar = 'plain',
						state = 'collapsed',
						titleclass = 'ts-navbox-plaintitle',
						bodyclass = 'authoritycontrol',
						titlestyle = navboxData.groupstyle,
						bodystyle = 'text-align: left;',
					} )
					navboxData[ 'list' .. rowIndex ] = templateStyles .. collapsibleNavbox
				end
			end

			rowIndex = rowIndex + 1
		end
	end

	if rowIndex == 1 then
		if mw.title.getCurrentTitle().namespace == 0 then
			return '[[Category:' .. categoryTemplateEmpty .. ']]'
		end
		return ''
	end

	local buttons = frame:expandTemplate{ title = 'tnavbar-view', args = { templateLink } } ..
		'&nbsp;[[File:OOjs UI icon edit-ltr-progressive.svg|14px|' .. wikidataLink .. '|link=d:'
		.. (elementId or mw.wikibase.getEntityIdForCurrentPage()) .. '#identifiers]]'
	if navboxData[ 'group1' ] then
		navboxData[ 'group1' ] = '<div style="padding: 0 35px 0 0; width: 100%;">' ..
			'<div style="float: left;">' .. buttons .. '</div>&nbsp;&nbsp;' ..
			navboxData[ 'group1' ] .. '</div>'

	else
		navboxData[ 'group1' ] = '<div style="padding: 0; width: 100%;">' .. buttons .. '</div>'
	end

	local out = moduleNavbox._navbox( navboxData )
	
	if colorArg and colorArg ~= '' and not byTemplate then
		out = out .. '[[Category:' .. categoryCustomColor .. ']]'
	end
	
	return out
end

return p