Modulo:Collegamenti esterni

Modulo che implementa il template {{Collegamenti esterni}}.

Ha una sottopagina di configurazione per ciascun gruppo di collegamenti esterni:

Tipo di proprietà

Quasi tutti i collegamenti si basano su proprietà di tipo "identificativo esterno", quindi per ciascuno bisognerà inserire, nel relativo gruppo della sottopagina di configurazione, almeno l'ID della proprietà Wikidata ("pid") e l'URL di formattazione ("url"). Per i rari casi di utilizzo di proprietà di tipo "URL", al posto dell'URL di formattazione va specificato il titolo del collegamento ("titolo").

Ordine collegamenti

All'interno di ciascun gruppo i collegamenti sono visualizzati nell'ordine in cui sono stati inseriti nella pagina di configurazione (ovviamente se è presente la relativa proprietà Wikidata). Per cambiare l'ordine è quindi sufficiente cambiare l'ordine dei collegamenti nelle sottopagine di configurazione.

Disambiguare i collegamenti relativi allo stesso sito web

Quando più proprietà Wikidata si riferiscono allo stesso sito web (esempio per le diverse schede di un calciatore, allenatore, dirigente, ...) e possono essere usate contemporaneamente in uno stesso elemento Wikidata, ci sono tre modi per disambiguare i collegamenti generati:

  1. differenziarli con "tipo", esempio nel primo tipo = 'calciatore' e nel secondo tipo = 'allenatore'. La disambiguazione apparirà però sempre, anche quando presente una sola proprietà nell'elemento.
  2. differenziarli con "titolo", esempio nel primo titolo = 'Scheda giocatore di $1' e nel secondo titolo = 'Scheda allenatore di $1'. La disambiguazione apparirà però sempre, anche quando presente una sola proprietà nell'elemento, inoltre la frase apparirà in corsivo anche se non fosse effettivamente il titolo della pagina.
  3. differenziarli con "sitodis", esempio nel primo sitodis = 'giocatore' e nel secondo sitodis = 'allenatore'. In questo modo, rispetto ai due metodi precedenti: (1) la disambiguazione apparirà automaticamente solo quando è presente più di una proprietà che utilizza lo stesso sito web e (2) si potranno omettere titolo e sito, lasciandoli al loro valore predefinito, ossia il titolo della pagina e il dominio dell'URL.
Controllo duplicati

Verifica automatica di proprietà duplicate: P345.


--[[
* Modulo che implementa il template Collegamenti esterni.
]]--

require('strict')

local getArgs = require('Modulo:Arguments').getArgs
local mWikidata = require('Modulo:Wikidata')
local mCitazione = require('Modulo:Citazione')
local mEditAtWikidata = require('Modulo:Modifica su Wikidata')
-- Permette di definire l'ordine di visualizzazione dei vari gruppi
local orderedGroupNames = {
	'Ufficiali', 'Enciclopedie', 'Biografie', 'Testi',
	'Letteratura', 'Politica', 'Religione', 'Architettura',
	'Astronomia', 'Biologia', 'Editoria', 'Geografia',
	'Linguistica', 'MAB', 'Matematica', 'Software', 'Calcio', 'Sci',
	'Sport', 'Videogiochi', 'Musica', 'Fumetti', 'Cinema', 'Gastronomia'
}
-- Soglie di attenzione sul numero elevato di collegamenti, per categorie di controllo
local MOLTI_LINK = 12
local MOLTI_LINK_2 = 15
-- Categorie di servizio
local catLetta = ' letta da Wikidata'
local catMultiSenzaQual = ' multipla letta da Wikidata senza qualificatore'
local catMoltiLink = 'Voci con template Collegamenti esterni e molti collegamenti'
local catMoltiLink2 = 'Voci con template Collegamenti esterni e molti collegamenti (soglia maggiore)'
local catEmpty = 'Voci con template Collegamenti esterni senza dati da Wikidata'
local catUnknownQual = 'Voci con template Collegamenti esterni e qualificatori sconosciuti'
local catExcessiveLoad = 'Voci con template Collegamenti esterni e molte entità Wikidata caricate'
local catDuplicates = 'Voci con template Collegamenti esterni e doppioni disattivati'
-- Avvisi per i link omessi
local tmpFound = 'Link visibile solo nell\'anteprima della pagina. È già presente il template $1'
local tmpSection = 'Questo link potrebbe essere visibile solo nell\'anteprima della pagina se in altre sezioni c\'è già uno dei seguenti template: $1'
-- Tabella dei qualificatori riconosciuti
-- Qui vanno elencati solo quelli generali, validi per tutte le dichiarazioni
-- Ciascuno ha la sua configurazione che può essere lasciata vuota o compilata come di seguito:
-- "par" è il parametro del modulo Citazione da compilare col qualificatore
-- "fallback" è una sottotabella coi qualificatori di ripiego in ordine di priorità
-- "restricted" è true se il qualificatore impone una citazione diversa per url a stesso sito
local knownQualifiers = {
	-- item o stringa dell'autore
	P50 = { par = 'autore', fallback = { 'P2093' }, restricted = true },
	P478 = { par = 'volume', restricted = true },
	P304 = { par = 'pagina', restricted = true },
	P577 = { par = 'data', restricted = true },
	-- titolo o "indicato come" o "riferito come" o pseudonimo
	P1476 = { par = 'titolo', fallback = { 'P1810', 'P1932', 'P742', 'P554' } },
	-- url archiviato, solo per le proprietà di tipo URL
	P1065 = { par = 'archivio' }, P2960 = { par = 'dataarch' }, P582 = { par = 'urlmorto' },
	P407 = {}, P813 = {}
}
-- I qualificatori di ripiego sono automaticamente aggiunti con configurazione vuota
for _, t in pairs(knownQualifiers) do
	if t.fallback then
		for _, v in ipairs(t.fallback) do knownQualifiers[v] = {} end
	end
end

-- =============================================================================
--                            Funzioni di utilità
-- =============================================================================

-- Restituisce la configurazione delle sottopagine.
--
-- @return {table}
local function readConfig()
	local ret = {}
	for _, groupName in ipairs(orderedGroupNames) do
		ret[groupName] = mw.loadData('Modulo:Collegamenti esterni/' .. groupName)
	end
	return ret
end

-- Restituisce il titolo della pagina corrente rimuovendo eventuale testo tra parentesi.
-- Se l'etichetta dell'elemento Wikidata contiene delle parentesi,
-- non le rimuove perché significa che fanno parte del titolo.
-- Con il parametro "from" (elemento Wikidata arbitrario) usa sempre l'etichetta.
--
-- @param {string} from
-- @return {string}
local function getCurrentTitle(from)
	local ret
	local label = mWikidata._getLabel({ from })
	if from then
		ret = label
	else
		ret = mw.title.getCurrentTitle().text
		if not (label and string.find(label, ' %(')) then
			ret = mw.text.split(ret, ' %(')[1]
		end
	end
	return ret
end

-- Restituisce il dominio dell'URL specificato.
--
-- @param {string} url
-- @return {string}
local function getDomain(url)
	return mw.uri.new(url).host:gsub('^www.', '')
end

-- Restituisce true se l'elemento collegato alla pagina o quello specificato in from
-- ha tra i valori (o loro sottoclassi) della proprietà indicata uno degli elementi specificati.
-- Restituisce come secondo valore una tabella con gli ID delle entità caricate ai fini della ricerca.
-- @param {string} prop - codice della proprietà 'Pxxx'
-- @param {table} [entityIds] - array dei valori (strighe 'Qxxx') che può avere
-- @param {string} from
-- @return {boolean}
-- @return {table}
local function checkEntity(prop, entityIds, from)
	local args = { from = from, recursion = 8 }
	for _, entityId in ipairs(entityIds) do
		table.insert(args, entityId)
	end
	return mWikidata._propertyHasEntity(prop, args)
end

-- Converte un parametro ricevuto come 'Xxx, yyy' in { 'Xxx'=true, 'Yyy'=true }
local function ParametroElenco(param)
	local chiavi = {}
	local valori = param and mw.text.split(param, ',')
	if valori then
		for _, str in ipairs(valori) do
			chiavi[mw.getContentLanguage():ucfirst(mw.text.trim(str))] = true
		end
	end
	return chiavi
end

-- =============================================================================
--                            Classe ExtLink
-- =============================================================================

-- La classe ExtLink rappresenta un singolo collegamento esterno.
-- Al suo interno ha un riferimento alla propria configurazione (linkConf)
-- e nel caso la proprietà Wikidata abbia più valori li raccoglie tutti.

local ExtLink = {}

-- Costruttore della classe ExtLink.
--
-- @param {table} [url] - uno o più URL, quanti sono i valori della proprietà Wikidata
-- @param {table} linkConf - la configurazione fissa per questo collegamento esterno
-- @param {table} extraConf - altri elementi di configurazione ricavati dall'item
-- @param {string} from - entityId se diverso da quello collegato alla pagina corrente
-- @return {table} un nuovo oggetto ExtLink
function ExtLink:new(url, linkConf, extraConf, from)
	local self = {}
	setmetatable(self, { __index = ExtLink })

	self.url = url
	self.linkConf = linkConf
	self.extraConf = extraConf
	self.from = from
	self.title = getCurrentTitle()
	self.title = self.from and mWikidata._getLabel({ self.from }) or self.title
	self.extraConf.medium = self.linkConf.medium or 'web'

	return self
end

-- Restituisce il parametro titolo per il modulo Citazione.
--
-- @param {number} i - il numero dell'URL a cui assegnare il titolo.
-- @return {string}
function ExtLink:_getTitolo(i)
	local titolo = self.extraConf.titolo[i] or self.title
	if self.linkConf.titolo then
		titolo = self.linkConf.titolo:gsub('$1', titolo)
	end
	if #self.url > 1 and self.extraConf.titolodis[i] then
		return string.format('%s (%s)', titolo, self.extraConf.titolodis[i])
	else
		return titolo
	end
end

-- Restituisce il parametro altrilink per il modulo Citazione.
-- Nel caso di valori multipli, genera quelli successivi al primo.
--
-- @return {table}
function ExtLink:_getAltriLink()
	if self.extraConf.restrictedData then return end
	local tbl = {}
	local titolo, specifica
	for i = 2, #self.url do
		titolo = self.extraConf.titolo[i] or self.title
		if self.extraConf.titolodis[i] then
			specifica = self.extraConf.titolodis[i]
		else
			local ripetuto = false -- controllo se stesso titolo già usato
			for j = 1, i - 1 do
				if titolo == (self.extraConf.titolo[j] or self.title) then
					ripetuto = true
					break
				end
			end
			if ripetuto then
				specifica = 'altra versione'
			else
				specifica = nil -- se titoli diversi, va bene anche senza specificazione
			end
		end
		if specifica then
			titolo = string.format('%s (%s)', titolo, specifica)
		end
		table.insert(tbl, { self.url[i], titolo })
	end
	return tbl
end

-- Restituisce il parametro cid per il modulo Citazione.
--
-- @param {number} i - il numero dell'URL a cui assegnare il cid.
-- @return {string}
function ExtLink:_getCid(i)
	if #self.url > 1 and self.extraConf.restrictedData then
		return self.extraConf.cid[i]
	else
		return self.linkConf.cid
	end
end

function ExtLink:_formatPreviewMsg()
	local ret = ''
	if self.extraConf.previewMsg then
		ret = mw.html.create('abbr')
			:css('color', 'red')
			:css('font-weight', 'bold')
			:attr('title', self.extraConf.previewMsg)
			:wikitext('!')
		ret = tostring(ret) .. ' '
	end
	return ret
end

-- Formatta il collegamento esterno quando la proprietà è di tipo URL.
--
-- @return {string}
function ExtLink:_formatPropertyURL()
	local formattedLinks = {}
	local currTitle = getCurrentTitle(self.from)
	local claims = mWikidata._getClaims(self.linkConf.pid, { from = self.from }) or {}
	for idx, claim in ipairs(claims) do
		local langs = mWikidata._formatQualifiers(claim, 'P407', { formatting = 'raw' }, true)
		langs = (#langs == 1 and langs[1] == 'Q652') and {} or langs
		for i, lang in ipairs(langs) do
			langs[i] = mw.wikibase.getLabel(lang) or lang
		end
		local formattedLink = mCitazione.cita_da_modulo(
			self.extraConf.medium,
			{
				url = mWikidata._formatStatement(claim),
				titolo = self.linkConf.titolo:gsub('$1', currTitle),
				lingua = table.concat(langs, ','),
				cid = self.linkConf.cid,
				urlarchivio = self.extraConf.archivio[idx],
				dataarchivio = self.extraConf.dataarch[idx],
				urlmorto = self.extraConf.urlmorto[idx] and 'sì' or (self.extraConf.archivio[idx] and 'no')
			})
		table.insert(formattedLinks, '* ' .. self:_formatPreviewMsg() .. formattedLink ..
					 mEditAtWikidata._showMessage({ pid = self.linkConf.pid, qid = self.from }))
	end
	return table.concat(formattedLinks, '\n')
end

-- Formatta il collegamento esterno come elemento di un elenco puntato.
--
-- @return {string}
function ExtLink:getListItem()
	-- restituisce il link se è già stato formattato
	if self.extraConf.formattedLink then
		return '* ' .. self:_formatPreviewMsg() .. self.extraConf.formattedLink
	-- se invece non è specificato l'URL di formattazione presume sia una
	-- proprietà di tipo URL e non di tipo "identificativo esterno"
	elseif not self.linkConf.url then
		return self:_formatPropertyURL()
	end
	local formattedLinks = {}
	for i = 1, self.extraConf.restrictedData and #self.url or 1 do
		local formattedLink = mCitazione.cita_da_modulo(
			self.extraConf.medium,
			{
				url = self.url[i],
				titolo = self:_getTitolo(i),
				altrilink = self:_getAltriLink(),
				sito = self.linkConf.opera and '' or self.extraConf.sito,
				opera = self.linkConf.opera,
				editore = self.linkConf.editore,
				lingua = self.linkConf.lingua,
				cid = self:_getCid(i),
				autore = self.linkConf.autore or self.extraConf.autore[i],
				volume = self.extraConf.volume[i],
				p = self.extraConf.pagina[i],
				data = self.linkConf.data or self.extraConf.data[i],
				tipo = self.linkConf.tipo or self.sitodis
			})
		table.insert(formattedLinks, '* ' .. self:_formatPreviewMsg() .. formattedLink ..
					 mEditAtWikidata._showMessage({ pid = self.linkConf.pid, qid = self.from }))
	end
	return table.concat(formattedLinks, '\n')
end

-- =============================================================================
--                            Classe LinksManager
-- =============================================================================

-- La classe LinksManager è la classe principale del modulo.
-- Al suo interno ha un riferimento a tutti collegamenti esterni (extLinks)
-- presenti in un dato elemento Wikidata e li può restituire tutti formattati 
-- nel modo più appropriato.

local LinksManager = {}

-- Costruttore della classe LinksManager.
--
-- @param {table} args
-- @return {table} un nuovo oggetto LinksManager
function LinksManager:new(args)
	local self = {}
	setmetatable(self, { __index = LinksManager })

	self.numExtLinks = 0
	self.categories = {}
	self.catColon = ''
	self.from = args.from
	self.escludi = ParametroElenco(args.escludi)
	self.soloprop = ParametroElenco(args.prop)
	self.sologruppi = ParametroElenco(args.gruppo)
	self.solomedium = ParametroElenco(args.medium)
	-- la pagina dei test utilizza uno stub del modulo Wikidata
	if mw.title.getCurrentTitle().prefixedText ==
	   'Discussioni modulo:Collegamenti esterni/test' then
		self:_setStubWikidata(args.stubwd)
	end
	self.extLinks = self:_getExtLinks()

	return self
end

-- Formatta e carica una o più categorie di servizio.
function LinksManager:_addCategory(...)
	for _, category in pairs({ ... }) do
		if category then
			category = string.format('[[%sCategoria:%s]]', self.catColon, category)
			table.insert(self.categories, category)
		end
	end
end

-- Permette di specificare uno stub del modulo Wikidata
-- in modo da ottenere i valori delle proprietà in modo deterministico,
-- non dipendenti da modifiche utente a un elemento Wikidata.
--
-- @param {table} stubwd
function LinksManager:_setStubWikidata(stubwd)
	mEditAtWikidata = { _showMessage = function(frame) return '' end }
	mWikidata = stubwd
	self.catColon = ':'
	self.debug = true
end

-- Controlla se un elemento fa parte delle esclusioni richieste dall'utente
function LinksManager:_Escluso(elemento, inclusi)
	return self.escludi[elemento] or (next(inclusi) and inclusi[elemento] ~= true)
end

-- Ottiene tutti i collegamenti esterni (configurati) presenti in un dato elemento Wikidata
-- suddivisi per gruppo.
--
-- @return {table}
function LinksManager:_getExtLinks()
	local ret, groupSites = {}, {}
	local cfg = readConfig()
	local lang = mw.language.getContentLanguage()
	local pageContent, isPreview, isSection
	local loadedEntities = setmetatable({}, {
		__newindex = function(t1, key, t2)
			if not t2 then return end
			for k, v in pairs(t2) do rawset(t1, k, v) end
		end })
	local duplicates = false -- per categoria temporanea
	for _, groupName in ipairs(orderedGroupNames) do -- per ogni gruppo tematico
		groupSites[groupName] = {}
		ret[groupName] = {}
		-- controlla se è un gruppo escluso manualmente dall'utente
		if self:_Escluso(groupName, self.sologruppi) then
			cfg[groupName] = {}
		end
		for _, linkConf in ipairs(cfg[groupName]) do -- per ogni sito configurato
			local claims, valido, extraConf
			-- se il sito non è escluso manualmente dall'utente, avviene la lettura da Wikidata
			if not (self:_Escluso(linkConf.pid, self.soloprop) or
					self:_Escluso(lang:ucfirst(linkConf.medium or 'web'), self.solomedium)) then
				claims = mWikidata._getClaims(linkConf.pid, { from = self.from })
			end
			-- controlla se ci sono valori
			if claims and #claims > 0 then
				extraConf = { cid = {}, titolodis = { count = 0 } }
				-- controlla se è un sito da escludere per soggetto non pertinente
				if type(linkConf.vincolo) == 'table' then
					local vincolo, tipo = {}, 'neg'
					for i, v in ipairs(linkConf.vincolo) do
						if i % 2 ~= 0 then
							local ms, k = v:match('^(%-?)(.+)$')
							if tipo == 'neg' and ms == '' then
								vincolo, tipo = {}, 'pos'
							end
							if not (tipo == 'pos' and ms == '-') then
								vincolo[k] = linkConf.vincolo[i + 1]
							end
						end
					end
					for k, v in pairs(vincolo) do
						valido, loadedEntities[k] = checkEntity(k, v, self.from)
						if tipo == 'neg' then
							valido = not valido
							if not valido then break end
						elseif valido then
							break
						end
					end
				else
					valido = true
				end
				-- controlla che il link non sia generato da altri template in voce
				if valido and linkConf.template then
					local templateList = linkConf.template
						:gsub('%s*,%s*', ','):gsub(',+', ','):gsub('^,', ''):gsub(',$', '')
					if templateList ~= '' then
						if not pageContent then
							pageContent = mw.title.getCurrentTitle():getContent()
								:gsub('<!%-%-.-%-%->', '')
								:gsub('<[Rr][Ee][Ff]%s.-/>', '') -- facilita la ricerca successiva
								:gsub('<[Rr][Ee][Ff].->.-</[Rr][Ee][Ff]%s*>', '')
							isPreview = mw.getCurrentFrame():preprocess('{{REVISIONID}}') == ''
						end
						for template in mw.text.gsplit(templateList, ',') do
							template = template:gsub(' +', ' +')
							local firstChar = template:match('^.')
							firstChar = string.format('[%s%s]', firstChar:upper(), firstChar:lower())
							if pageContent:match('{{%s*' .. template:gsub('^.', firstChar) .. '%s*[|}]') then
								valido, duplicates = isPreview, true
								if isPreview then
									extraConf.previewMsg = mw.message.newRawMessage(tmpFound, template):plain()
								end
								break
							end
						end
						if isPreview and not extraConf.previewMsg then
							if isSection == nil then
								isSection = pageContent:match('^=+[^=\n]+=+ *\n') and true or false
							end
							if isSection then
								templateList = templateList:gsub(',', ', ')
								extraConf.previewMsg = mw.message.newRawMessage(tmpSection, templateList):plain()
							end
						end
					end
				end
			end
			-- verifica se deve generare il link con un sottomodulo
			-- che può fare a sua volta altri controlli
			if valido and linkConf.sottomodulo then
				local success, metamodule = pcall(require, 'Modulo:' .. linkConf.sottomodulo)
				if success and type(metamodule._main) == 'function' then
					extraConf.formattedLink = metamodule._main({
						from = self.from,
						['mostra errori'] = 'no'
					})
				end
				if extraConf.formattedLink then
					claims = {}
				else
					valido = false
				end
			end
			-- se il sito è abilitato, viene aggiunto a quelli da mostrare
			if valido then
				local url = {}
				-- per ogni dichiarazione
				for i, claim in ipairs(claims) do
					for k, t in pairs(knownQualifiers) do
						if t.par then
							extraConf[t.par] = extraConf[t.par] or { count = 0 }
							local properties = { k, unpack(t.fallback or {}) }
							-- ricava i qualificatori generali e ne tiene il conto
							for _, v in ipairs(properties) do
								extraConf[t.par][i] = mWikidata._formatQualifiers(claim, v) or
									t.par == 'urlmorto' and
									mWikidata._formatQualifiers(claim, v, { snaktype = 'somevalue' }) or nil
								if extraConf[t.par][i] then
									extraConf[t.par].count = extraConf[t.par].count + 1
									if t.restricted then
										extraConf.restrictedData = true
									end
									break
								end
							end
						end
					end
					if claim.qualifiers then
						knownQualifiers.multi = {}
						-- cerca un disambiguante per il titolo di ogni url
						for qualifierId in mw.text.gsplit(linkConf.multi or '', ',') do
							if qualifierId == '' then break end
							qualifierId = mw.text.trim(qualifierId):upper()
							if extraConf.titolodis[i] == nil and claim.qualifiers[qualifierId] then
								local args = { nq = '1', formatting = 'raw' }
								local formattedQualifier = mWikidata._formatQualifiers(claim, qualifierId, args)
								if formattedQualifier then
									extraConf.titolodis[i] = mw.wikibase.getLabel(formattedQualifier)
									if extraConf.titolodis[i] then
										extraConf.titolodis.count = extraConf.titolodis.count + 1
									end
								end
							end
							knownQualifiers.multi[qualifierId] = true
						end
						-- categoria di servizio in presenza di qualificatori non riconosciuti
						for qualifierId in pairs(claim.qualifiers) do
							if not (knownQualifiers[qualifierId] or knownQualifiers.multi[qualifierId]) then
								self:_addCategory(catUnknownQual)
								break
							end
						end
					end
					-- crea l'url
					claim = mWikidata._formatStatement(claim)
					if linkConf.cid then
						extraConf.cid[i] = linkConf.cid .. ' ' .. (extraConf.data[i] or claim)
					end
					if linkConf.url then
						-- se proprietà di tipo id, il valore viene sostituito a "$1"
						claim = mw.message.newRawMessage(linkConf.url, claim):plain()
					end
					table.insert(url, (claim:gsub(' ', '%%20')))
				end
				-- nome sito, di default il dominio estratto dall'url
				extraConf.sito = linkConf.sito or getDomain(linkConf.url)
				-- creazione dell'oggetto collegamento esterno, con l'url (o gli url) e gli altri dati raccolti
				table.insert(ret[groupName], ExtLink:new(url, linkConf, extraConf, self.from))
				-- categoria per proprietà letta; se multipla e indistinguibile, usa categoria di avviso
				local tail = #url > 1 and linkConf.url and
					extraConf.titolodis.count ~= #url and
					extraConf.titolo.count < #url - 1 and
					catMultiSenzaQual or catLetta
				self:_addCategory(linkConf.pid .. tail)
				-- per verificare se un sito è ripetuto nel gruppo e necessita di disambiguazione
				groupSites[groupName][extraConf.sito] = (groupSites[groupName][extraConf.sito] or 0) + 1
				-- conteggio complessivo dei collegamenti
				self.numExtLinks = self.numExtLinks + 1
			end
		end
	end
	-- verifica se un sito è ripetuto nel gruppo
	for _, groupName in ipairs(orderedGroupNames) do
		for _, extLink in ipairs(ret[groupName]) do
			-- necessaria la disambiguazione (più URL con lo stesso dominio nel gruppo),
			-- se configurata tramite "sitodis" nella configurazione.
			if groupSites[groupName][extLink.extraConf.sito] > 1 then
				extLink.sitodis = extLink.linkConf.sitodis
			end
		end
	end
	-- categorie di servizio su numero di link/entità caricate e doppioni omessi
	local catnumero = self.numExtLinks == 0 and catEmpty or
		self.numExtLinks > MOLTI_LINK_2 and catMoltiLink2 or
		self.numExtLinks > MOLTI_LINK and catMoltiLink
	self:_addCategory(catnumero,
		#loadedEntities > 100 and catExcessiveLoad,
		duplicates and catDuplicates)
	return ret
end

-- Formatta i collegamenti esterni come elenco puntato.
--
-- @param {table} [groupNames]
-- @return {string}
function LinksManager:_formatList(groupNames)
	local formattedLinks = {}
	for _, groupName in ipairs(groupNames) do
		for _, extLink in ipairs(self.extLinks[groupName]) do
			table.insert(formattedLinks, extLink:getListItem())
		end
	end
	return table.concat(formattedLinks, '\n')
end

-- Restituisce tutti i collegamenti esterni formattandoli come elenco puntato
--
-- @return {string}
function LinksManager:getList()
	local categories, links
	-- categorie di servizio
	categories = (mw.title.getCurrentTitle().namespace == 0 or self.debug) and
				 table.concat(self.categories) or ''
	-- collegamenti
	links = self:_formatList(orderedGroupNames)
	return links .. categories
end

-- =============================================================================
--                            Funzioni esportate
-- =============================================================================

local p = {}

-- Funzioni di utilità per il manuale, danno la soglia di attenzione sul n° di link.
function p.threshold(frame)
	return MOLTI_LINK
end
function p.threshold2(frame)
	return MOLTI_LINK_2
end

-- Funzione di utilità per il manuale, restituisce un elenco
-- delle proprietà supportate, divise per gruppo.
function p.properties(frame)
	local res = {}
	local cfg = readConfig()
	local formatString = '<tr><td>[[d:P:%s|%s (%s)]]</td><td>%s</td><td>%s</td><td>%s</td></tr>'
	table.sort(orderedGroupNames)
	for _, groupName in ipairs(orderedGroupNames) do
		local wdLinks = {}
		for _, linkConf in ipairs(cfg[groupName]) do
			local label = mWikidata._getLabel({ linkConf.pid })
			assert(label, string.format(
				'Impossibile trovare l\'etichetta della proprietà %s del gruppo %s',
				linkConf.pid, groupName))
			local templates = {}
			for template in string.gmatch(linkConf.template or '', '[^,]+') do
				table.insert(templates, '[[t:' .. template .. '|' .. template .. ']]')
			end
			local wdLink = string.format(formatString,
				linkConf.pid,
				label,
				linkConf.pid,
				linkConf.url or '',
				linkConf.cid or '',
				table.concat(templates, ', '))
			table.insert(wdLinks, wdLink)
		end

		local group
		if frame.args[1] == 'modulo' then
			group = string.format('* [[Modulo:Collegamenti esterni/%s]] (%s)',
				groupName, #wdLinks)
		else
			local wikitable = mw.html.create('table')
			local title = string.format('[[Modulo:Collegamenti esterni/%s|%s]] (%s)',
				groupName, groupName, #wdLinks)
			wikitable
				:addClass('wikitable sortable plainlinks')
				:tag('tr')
					:tag('th'):wikitext('Proprietà')
					:tag('th'):wikitext('Formato URL')
					:tag('th'):wikitext('cid')
					:tag('th'):wikitext('Template equivalenti')
				:allDone()
				:wikitext(table.concat(wdLinks))
			group = mw.getCurrentFrame():expandTemplate{
				title = 'Cassetto',
				args = { titolo = title, testo = tostring(wikitable) }
			}
		end
		table.insert(res, group)
	end
	return table.concat(res, '\n')
end

-- Funzione di utilità per il manuale, verifica l'assenza di proprietà duplicate.
function p.checkdup(frame)
	local ids, vin, res = {}, {}, {}
	local cfg = readConfig()
	for _, groupName in ipairs(orderedGroupNames) do
		for _, linkConf in ipairs(cfg[groupName]) do
			local duplicate = ids[linkConf.pid]
			if vin[linkConf.pid] == nil then
				vin[linkConf.pid] = { pos = {}, neg = {} }
			end
			if vin[linkConf.pid] ~= false and linkConf.vincolo then
				duplicate, vin[linkConf.pid].cur = false, {}
				local tipo = 'neg'
				for i, v in ipairs(linkConf.vincolo) do
					if i % 2 ~= 0 then
						local ms, p = v:match('^(%-?)(.+)$')
						if tipo == 'neg' and ms == '' then
							vin[linkConf.pid].cur, tipo = {}, 'pos'
						end
						if not (tipo == 'pos' and ms == '-') then
							for _, q in ipairs(linkConf.vincolo[i + 1]) do
								vin[linkConf.pid].cur[p .. q] = true
							end
						end
					end
				end
				if tipo == 'pos' then
					for k in pairs(vin[linkConf.pid].cur) do
						for _, t in ipairs(vin[linkConf.pid].pos) do
							if t[k] then duplicate = true break end
						end
						for _, t in ipairs(vin[linkConf.pid].neg) do
							if not t[k] then duplicate = true break end
						end
					end
				else
					if #vin[linkConf.pid].neg == 1 then
						duplicate = true
					else
						for _, t in ipairs(vin[linkConf.pid].pos) do
							for k in pairs(t) do
								if not vin[linkConf.pid].cur[k] then
									duplicate = true
									break
								end
							end
						end
					end
				end
				table.insert(vin[linkConf.pid][tipo], vin[linkConf.pid].cur)
			end
			if duplicate then
				table.insert(res, linkConf.pid)
			else
				ids[linkConf.pid] = true
				if not linkConf.vincolo then vin[linkConf.pid] = false end
			end
		end
	end
	return #res == 0 and 'nessun duplicato' or
		string.format('<span class="error">%s</span>', table.concat(res, ', ')) 
end

-- Funzione per l'utilizzo da un altro modulo.
function p._main(args)
	return LinksManager:new(args):getList()
end

-- Funzione per il template {{Collegamenti esterni}}.
function p.main(frame)
	return p._main(getArgs(frame, { parentOnly = true }))
end

return p