Modulo:Collegamenti esterni

Da Semi del Verbo, l'enciclopedia dell'influenza del Vangelo sulla cultura
Versione del 30 ago 2020 alle 22:52 di Johnrdorazio (discussione | contributi) (una versione importata)
(diff) ← Versione meno recente | Versione attuale (diff) | Versione più recente → (diff)

La documentazione per questo modulo può essere creata in Modulo:Collegamenti esterni/man

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

require('Modulo:No globals')

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', 'Software', 'Calcio', 'Sci',
	'Sport', 'Videogiochi', 'Musica', 'Fumetti', 'Cinema'
}
-- 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'
-- 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

-- 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, '* ' .. 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()
	-- se è specificato l'URL di formattazione è una
	-- proprietà di tipo "identificativo esterno" altrimenti di tipo URL
	if 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.extraConf.restrictedData and self.extraConf.cid[i] or self.linkConf.cid,
				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, '* ' .. 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 = mw.title.getCurrentTitle():getContent()
		:gsub('<!%-%-.-%-%->', '')
		:gsub('<[Rr][Ee][Ff]%s.-/>', '') -- facilita la ricerca successiva
		:gsub('<[Rr][Ee][Ff].->.-</[Rr][Ee][Ff]%s*>', '')
	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
			-- 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
				-- 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
					for template in mw.text.gsplit(linkConf.template, ',') do
						template = mw.text.trim(template):gsub(' +', ' +')
						local first_char = template:match('^.')
						first_char = string.format('[%s%s]', first_char:upper(), first_char:lower())
						if pageContent:match('{{%s*' .. template:gsub('^.', first_char) .. '%s*[|}]') then
							valido, duplicates = false, true
							break
						end
					end
				end
			end
			-- se il sito è abilitato, viene aggiunto a quelli da mostrare
			if valido then
				local url = {}
				local extraConf = { cid = {}, titolodis = { count = 0 } }
				-- 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
				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()
	table.sort(orderedGroupNames)
	for _, groupName in ipairs(orderedGroupNames) do
		local wdLinks = {}
		for _, linkConf in ipairs(cfg[groupName]) do
			local wdLink = string.format('<tr><td>[[d:Property:%s|%s (%s)]]</td><td>%s</td><td>%s</td><td>%s</td></tr>',
				linkConf.pid, mWikidata._getLabel({ linkConf.pid }), linkConf.pid, linkConf.url or '', linkConf.cid or '', linkConf.template and ('[[t:'..linkConf.template..'|'..linkConf.template..']]') or '')
			table.insert(wdLinks, wdLink) 
		end
		local group = frame.args[1] == 'modulo' and
			string.format('* [[Modulo:Collegamenti esterni/%s]] (%s)', groupName, #wdLinks) or
			mw.getCurrentFrame():expandTemplate {
				title = 'Cassetto',
				args = {
					titolo = string.format('[[Modulo:Collegamenti esterni/%s|%s]] (%s)', groupName, groupName, #wdLinks),
					testo = '<table class="wikitable sortable plainlinks"><tr><th>Proprietà</th><th>Formato URL</th><th>cid</th><th>Template singolo</th></tr>' .. table.concat(wdLinks, '') .. '</table>'
				}
			}
		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