This module is designed for implementing templates to display external links in an English Wikipedia article, using the properties defined in that article's Wikidata item. It was based on the Norwegian Wikipedia module no:Modul:External links.

The first test implementation is Template:Sports links (based on no:Mal:Sportslenker). The sports-related link properties and formats are specified in Module:External links/conf/Sports (based on no:Modul:External links/conf/Sport).

Other subject-based implementations at the Norwegian Wikipedia include:

{{#invoke:External links|function_name}}


require('strict')
-- local genitive = require('Module:Genitive')._genitive
local contLang = mw.language.getContentLanguage()

local cmodule = {}
local conf = require 'Module:External links/conf'(contLang:getCode())
local hasdatafromwikidata = false
local hasdatafromlocal = false
local haswikidatalink = true -- we assume it's connected

local p = {}

local function getLabel(entity, use_genitive, pagetitle)
	local label = (pagetitle and pagetitle ~= '') and pagetitle or nil
	if not label and not entity then
		label = mw.title.getCurrentTitle().text
	elseif not label then
		label = mw.wikibase.label(entity.id) or mw.title.getCurrentTitle().text
	end
--	return use_genitive and genitive(label, 'sitt') or label
	return use_genitive and label .. "'s" or label
end

-- @todo cleanup, this is in production, use the console
local function dump(obj)
	return "<pre>" .. mw.dumpObject(obj) .. "</pre>"
end


local function stringFormatter( datavalue )
	if datavalue == nil or datavalue['type'] ~= 'string' then
		return nil
	end
	return datavalue.value
end

local pval = {}
pval.P1793 = { -- format as a regular expression
	types = {
		snaktype = 'value',
		datatype = 'string',
	},
}

pval.P407 = { -- language of work or name
	types = {
		snaktype = 'value',
		datatype = 'wikibase-item',
		datavalue = {
			type = 'wikibase-entityid', 
		}
	},
}

pval.P364 = { -- original language of work 
	types = {
		snaktype = 'value',
		datatype = 'wikibase-item',
		datavalue = {
			type = 'wikibase-entityid', 
		}
	},
}

pval.P218 = { -- ISO 639-1 language 
	types = {
		snaktype = 'value',
		datatype = 'external-id',
		datavalue = {
			type = 'string', 
		}
	},
}

pval.P305 = { -- IETF language tag
	types = {
		snaktype = 'value',
		datatype = 'external-id',
		datavalue = {
			type = 'string', 
		}
	},
}

pval.P582 = { -- end time
	types = {
		snaktype = 'value',
		datatype = 'time',
		datavalue = {
			type = 'string', 
		}
	},
}


-- This is a really makeshift crappy converter, but it'll do some basic
-- conversion from PCRE to Lua-style patterns (note that this only work
-- in very few cases)
local function regexConverter( regex )
	local output = regex
	output = string.gsub(output, "\\d{2}", "%%d%%d")
	output = string.gsub(output, "\\d{3}", "%%d%%d%%d")
	output = string.gsub(output, "\\d{4}", "%%d%%d%%d%%d")
	output = string.gsub(output, "\\d{5}", "%%d%%d%%d%%d%%d")
	output = string.gsub(output, "\\d{6}", "%%d%%d%%d%%d%%d%%d")
	output = string.gsub(output, "\\d{7}", "%%d%%d%%d%%d%%d%%d%%d")
	output = string.gsub(output, "\\d{8}", "%%d%%d%%d%%d%%d%%d%%d%%d")
	output = string.gsub(output, "\\d", "%%d")
	
	return output
end


local function getFormatterUrl( prop, value )
	local head = ""
	local tail = ""
	local entity = mw.wikibase.getEntity(prop)
	-- to avoid deep tests
	if not entity or not entity.claims then
		return head
	end
	-- get the claims for this entity
	local statements = entity.claims['P1630'] -- formatter URL
	-- to avoid deep tests
	if not statements then
		return head
	end
	local formatters = {}
	-- let's go through the claims
	for _, claim in ipairs( statements ) do
		-- to avoid deep tests
		if not claim then
			claim = {}
		end
		local valid = claim['type'] == 'statement'
			and claim['rank'] ~= 'deprecated'
		if valid then
			local mainsnak = claim.mainsnak or {}
			local preferred = claim['rank'] == 'preferred'
			-- get any qualifiers for this claim (we are interested in P1793 for
			-- indication of which claim is correct) 
			local qualifiers = claim.qualifiers or {}
			-- now let's check the qualifier we are interested in
			local qualid = 'P1793' -- format as a regular expression
			-- if the claim has this qualifier
			if qualifiers[qualid] then
				-- it's here, let's check it out!
				local items = {}
				-- traverse all snaks in this qualifier
				for _, qualsnak in ipairs( qualifiers[qualid] ) do
					if qualsnak and pval[qualid] then
						--mw.log("qualsnak = " .. dump(qualsnak))
						-- check if the snak is of the correct snaktype and datatype
						local valid = qualsnak.snaktype == pval[qualid].types.snaktype
							and qualsnak.datatype == pval[qualid].types.datatype
						if valid then
							-- we'll have to convert the regex to Lua-style
							local regex = regexConverter(qualsnak.datavalue.value)
							local test = string.match( value, '^'..regex..'$' )
							if test then
								-- it matched, this is correct and overrides any other.
								if preferred then
									head = mainsnak.datavalue.value
								else
									tail = mainsnak.datavalue.value
								end
							end
						end
					end
				end
			else
				-- we don't have any qualifier, is it preferred?
				if (head == '' and preferred) or (tail == '' and not preferred) then
					-- if we don't have any other, use this one
					if preferred and head == '' then
						head = mainsnak.datavalue.value
					elseif not preferred and tail == '' then
						tail = mainsnak.datavalue.value
					end
				end
			end
		end
	end
	return head ~= '' and head or tail

end



local function getLanguageData(prop, qid)
	local head = {}
	local tail = {}
	-- mw.log("getLanguageData, prop="..dump(prop).." qid="..dump(qid))
	-- get the entity we are checking
	local entity = mw.wikibase.getEntityObject(qid)
	-- to avoid deep tests
	if not entity then
		return nil
	end
	if not entity.claims then
		return {}
	end
	-- get the claims for this entity
	local statements = entity.claims[prop]
	-- to avoid deep tests
	if not statements then
		return {}
	end
	-- mw.log("getLanguageData going through claims="..dump(statements))
	-- let's go through the claims
	for _, claim in ipairs( statements ) do
		-- to avoid deep tests
		if not claim then
			claim = {}
		end
		local valid = claim['type'] == 'statement'
			and claim['rank'] ~= 'deprecated'
		if valid then
			local mainsnak = claim.mainsnak or {}
			local preferred = claim['rank'] == 'preferred'
			-- verify the item is what we expect
			local valid = mainsnak.snaktype == pval[prop].types.snaktype
				and mainsnak.datatype == pval[prop].types.datatype
				and mainsnak.datavalue.type == pval[prop].types.datavalue.type
			if valid then
				-- mw.log("getLanguageData claim is valid="..dump(claim))
				-- if this is the correct P-value, dive into it and get P218 (ISO 639-1)
				if mainsnak.property == 'P364' then -- original language of work
					if preferred then
						head[#head+1] = table.concat(getLanguageData('P218', 'Q'..mainsnak.datavalue.value['numeric-id']), conf:a('mod-filter-separator'))
					else
						tail[#tail+1] = table.concat(getLanguageData('P218', 'Q'..mainsnak.datavalue.value['numeric-id']), conf:a('mod-filter-separator'))
					end
				elseif mainsnak.property == 'P218' or mainsnak.property == 'P305' then -- ISO 639-1 code or IETF language tag
					if preferred then
						head[#head+1] = stringFormatter(mainsnak.datavalue)
					else
						tail[#tail+1] = stringFormatter(mainsnak.datavalue)
					end
				end
			end
		end
	end
	-- mw.log("getLanguageData returning head="..dump(head).." tail="..dump(tail))
	return #head>0 and head or tail
end

local langqvalorder = {'P407','P364'}
local otherqvalorder = {'P582'}

local function getValuesFromWikidata(props)
	local head = {}
	local tail = {}
	-- mw.log("getValuesFromWikidata, props="..dump(props))
	-- get the entity we are checking
	local entity = mw.wikibase.getEntityObject()
	-- to avoid deep tests
	if not entity then
		--mw.log("getValuesFromWikidata no entity")
		return nil
	end
	if not entity.claims or not props or not props.prop or props.prop == '' then
		--mw.log("getValuesFromWikidata no claims or no props")
		return {}
	end
	-- get the claims for this entity
	local statements = entity.claims[props.prop]
	-- to avoid deep tests
	if not statements then
		return {}
	end
	-- let's go through the claims
	for _, claim in ipairs( statements ) do
		-- to avoid deep tests
		if not claim then
			claim = {}
		end
		local valid = claim['type'] == 'statement'
			and claim['rank'] ~= 'deprecated'
		if valid then
			-- mw.log("getValuesFromWikidata valid claim="..dump(claim))
			local mainsnak = claim.mainsnak or {}
			local preferred = claim['rank'] == 'preferred'
			-- get the content of the claim (the identifier)
			local langcode = props.langcode
			local checklangcode = nil
			if props.langcode and props.langcode ~= '' then
				checklangcode = string.find(langcode, "([pP]%d+)")
			end
			if checklangcode and checklangcode ~= "" then
				-- this is a P-value for language-code, so we'll check qualifiers for languagedata
				-- first get any qualifiers
				local qualifiers = claim.qualifiers or {}
				for _, qualid in ipairs( langqvalorder ) do
					-- if the claim has this qualifier
					if qualifiers[qualid] then
						-- it's here, let's check it out!
						local items = {}
						-- traverse all snaks in this qualifier
						for _, qualsnak in ipairs( qualifiers[qualid] ) do
							if qualsnak and pval[qualid] then
								-- mw.log("qualsnak = " .. dump(qualsnak))
								-- check if the snak is of the correct snaktype and datatype
								local valid = qualsnak.snaktype == pval[qualid].types.snaktype
									and qualsnak.datatype == pval[qualid].types.datatype
								if valid then
									-- now get the actual data
									langcode = table.concat(getLanguageData('P305', 'Q'..qualsnak.datavalue.value['numeric-id']), '')
								end
							end
						end
					end
					-- mw.log("langcode is now="..dump(langcode))
				end
				if string.find(langcode, "([pP]%d+)") then
					-- we still don't have any langcode, so we default to "en"
					langcode = nil
				end
			end
			local stillvalid = true
			-- we should check a couple of other qualifiers as well
			-- first get any qualifiers
			local qualifiers = claim.qualifiers or {}
			for _, qualid in ipairs( otherqvalorder ) do
				-- if the claim has this qualifier
				if qualifiers[qualid] then
					-- it's here, let's check it out!
					local items = {}
					-- traverse all snaks in this qualifier
					for _, qualsnak in ipairs( qualifiers[qualid] ) do
						if qualsnak and pval[qualid] then
							-- mw.log("qualsnak = " .. dump(qualsnak))
							-- check if the snak is of the correct snaktype and datatype
							local valid = qualsnak.snaktype == pval[qualid].types.snaktype
								and qualsnak.datatype == pval[qualid].types.datatype
							if not valid then
								-- sorry, this is not correct
								mw.log("qualsnak = INCORRECT")
								stillvalid = false
							end
						end
					end
				end
				-- mw.log("langcode is now="..dump(langcode))
			end
			if stillvalid then
				if preferred then
					head[#head+1] = { value=stringFormatter(mainsnak.datavalue) }
					if langcode and langcode ~= '' then
						head[#head]['langcode'] = langcode
					end
				else
					tail[#tail+1] = { value=stringFormatter(mainsnak.datavalue) }
					if langcode and langcode ~= '' then
						tail[#tail]['langcode'] = langcode
					end
				end
			end
		end
	end
	-- mw.log("getValuesFromWikidata returning head="..dump(head).." tail="..dump(tail))
	return #head>0 and head or tail
end

local function findMainLinksOnWikidata(props, pagetitle, short_links)
	local output = {}
	local pid = nil
	-- get the entity we are checking
	local entity = mw.wikibase.getEntityObject()
	-- to avoid deep tests
	if not entity then
		return nil
	end
	local values = getValuesFromWikidata(props)
	for _, value in ipairs( values ) do
		local verified_value = nil
		if props.regex then
			-- we have a local defined regex, so this will have to pass first
			-- maybe we'll have to convert the regex to Lua-style
			local regex = regexConverter(props.regex)
			local test = string.match( value.value, '^'..regex..'$' )
			--mw.log("testing with "..regex.. " and test="..dump(test).." and value="..id)
			if test then
				-- it matched, this is correct and overrides any other.
				verified_value = value.value
			end
		else
			verified_value = value.value
		end
		if verified_value then
			local url = ''
			output[#output+1] = {}
			output[#output].langcode = value.langcode
			output[#output].category = {}
			if props.url_f then
				-- we have a local defined url-formatter function, use it as first priority
				url = props.url_f(verified_value)
				if props.track and not string.find(props.langcode, "([pP]%d+)") then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-local-wd'), props.prop):plain()
				elseif props.track then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-wd-wd'), props.prop):plain()
				end
			elseif props.url then
				-- we have a local defined url-formatter string, use it as second priority
				url = mw.message.newRawMessage(props.url, verified_value):plain()
				if props.track and not string.find(props.langcode, "([pP]%d+)") then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-local-wd'), props.prop):plain()
				elseif props.track then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-wd-wd'), props.prop):plain()
				end
			else
				-- get the formatvalue from the property, if it exists
				local formatterUrl = getFormatterUrl(props.prop, verified_value)
				if formatterUrl ~= '' then
					url = mw.message.newRawMessage(formatterUrl, verified_value):plain()
					if props.track then 
						output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-wd-wd'), props.prop):plain()
					end
				end
			end
			if url ~= '' then
				local this_wiki = mw.getContentLanguage()
				local this_wiki_code = this_wiki:getCode()
				local langlink = (value.langcode and value.langcode ~= '' and value.langcode ~= this_wiki_code) and mw.message.newRawMessage(conf:g('msg-langcode'), value.langcode, mw.language.fetchLanguageName(value.langcode, this_wiki_code)) or ""
				if short_links and props.short then
					output[#output].text =
						mw.message.newRawMessage(props.short,
							getLabel(entity, props.genitive, pagetitle),
							url,
							langlink,
							verified_value,
							mw.uri.encode(verified_value, 'PATH'))
						:plain()
				else
					output[#output].text =
						mw.message.newRawMessage(props.message,
							getLabel(entity, props.genitive, pagetitle),
							url,
							langlink,
							verified_value,
							mw.uri.encode(verified_value, 'PATH'))
						:plain()
				end
			end
		end
	end
	--mw.log("findMainLinksOnWikidata returning="..dump(output))
	return output
end

local function getSitelinksFromWikidata(props, entity)
	local output = {}
	--mw.log("getSitelinksFromWikidata, props="..dump(props))
	-- to avoid deep tests
	if not entity then
		entity = mw.wikibase.getEntityObject()
		if not entity then
			--mw.log("getSitelinksFromWikidata no entity")
			return nil
		end
	end
	local requested_sitelink = string.match(props.prop, "SL(%l+)")
	local sitelinks = entity:getSitelink(requested_sitelink)
	if sitelinks and sitelinks ~= '' then
		output[#output+1] = { value = sitelinks }
	end
	--mw.log("getSitelinksFromWikidata returning output="..dump(output))
	return output
end


local function findSiteLinksOnWikidata(props, pagetitle, short_links)
	local output = {}
	local pid = nil
	-- get the entity we are checking
	local entity = mw.wikibase.getEntityObject()
	-- to avoid deep tests
	if not entity then
		return nil
	end
	local values = getSitelinksFromWikidata(props)
	for _, value in ipairs( values ) do
		local verified_value = nil
		if props.regex then
			-- we have a local defined regex, so this will have to pass first
			-- maybe we'll have to convert the regex to Lua-style
			local regex = regexConverter(props.regex)
			local test = string.match( value.value, '^'..regex..'$' )
			--mw.log("testing with "..regex.. " and test="..dump(test).." and value="..id)
			if test then
				-- it matched, this is correct and overrides any other.
				verified_value = value.value
			end
		else
			verified_value = value.value
		end
		if verified_value then
			--mw.log("it's verified..")
			local url = ''
			output[#output+1] = {}
			output[#output].langcode = value.langcode
			output[#output].category = {}
			if props.url_f then
				-- we have a local defined url-formatter function, use it as first priority
				url = props.url_f(verified_value)
				if props.track and not string.find(props.langcode, "(SL%l+)") then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-local-wd'), props.prop):plain()
				elseif props.track then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-wd-wd'), props.prop):plain()
				end
			elseif props.url then
				-- we have a local defined url-formatter string, use it as second priority
				url = mw.message.newRawMessage(props.url, verified_value):plain()
				if props.track and not string.find(props.langcode, "(SL%l+)") then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-local-wd'), props.prop):plain()
				elseif props.track then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-wd-wd'), props.prop):plain()
				end
			else
				url = verified_value:gsub(' ','_')
				if props.track then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-wd-wd'), props.prop):plain()
				end
			end
			if url ~= '' then
				local this_wiki = mw.getContentLanguage()
				local this_wiki_code = this_wiki:getCode()
				local langlink = (value.langcode and value.langcode ~= '' and value.langcode ~= this_wiki_code) and mw.message.newRawMessage(conf:g('msg-langcode'), value.langcode, mw.language.fetchLanguageName(value.langcode, this_wiki_code)) or ""
				if short_links and props.short then
					output[#output].text =
						mw.message.newRawMessage(props.short,
							getLabel(entity, props.genitive, pagetitle),
							url,
							langlink,
							verified_value,
							mw.uri.encode(verified_value, 'PATH'))
						:plain()
				else
					output[#output].text =
						mw.message.newRawMessage(props.message,
							getLabel(entity, props.genitive, pagetitle),
							url,
							langlink,
							verified_value,
							mw.uri.encode(verified_value, 'PATH'))
						:plain()
				end
			end
		end
	end
	--mw.log("findSiteLinksOnWikidata returning="..dump(output))
	return output
end


local function findMainLinksLocal(props, pagetitle, short_links, local_value)
	local output = {}
	-- to avoid deep tests
	if not props.prop then
		return nil
	end
	if not local_value or local_value == '' then
		-- bail out if no value is present
		return output
	end
	-- get the formatvalue from the property
	local verified_value = local_value
	if props.regex and props.regex ~= '' then
		-- let's verify the id
		-- maybe we'll have to convert the regex to Lua-style
		local regex = regexConverter(props.regex)
		local test = string.match( local_value, '^'..regex..'$' )
		if test then
			-- it matched, this is correct
			verified_value = local_value
		else
			verified_value = nil
		end
		
	end
	if not verified_value then
		return output
	end
	local wikidata_property = string.find(props.prop, "([pP]%d+)")
	local wikidata_values = {}
	if wikidata_property then
		-- get any wikidata values to see if they are equal to local values
		wikidata_values = getValuesFromWikidata(props)
	end
	if wikidata_property or (props.url and props.url ~= '') or (props.url_f) then
		output[#output+1] = {}
		output[#output].langcode = string.find(props.langcode, "([pP]%d+)") and "" or props.langcode
		--mw.log("findMainLinksLocal - props="..dump(props).." langcode="..output[#output].langcode)
		output[#output].category = {}
		local url = ''
		if props.track and wikidata_property and wikidata_values and #wikidata_values then
			local local_value_in_wikidata = false
			for _,value in ipairs( wikidata_values ) do
				if value.value == verified_value then
					local_value_in_wikidata = true
				end
			end
			output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), (local_value_in_wikidata and 'track-cat-local-wd-equal' or 'track-cat-local-wd-unequal')), props.prop):plain()
		end
		if wikidata_property and wikidata_values and #wikidata_values then
			hasdatafromwikidata = true -- signal up the chain this article has a wikidata claim
		end
		if props.url_f then
			-- we have a local defined url-formatter function, use it as first priority
			url = props.url_f(verified_value)
			if props.track then 
				output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-local-local'), props.prop):plain()
			end
		elseif props.url then
			-- we have a local defined url-formatter string, use it as second priority
			url = mw.message.newRawMessage(props.url, verified_value):plain()
			if props.track then 
				output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-local-local'), props.prop):plain()
			end
		elseif wikidata_property then
			-- get the formatvalue from the property, if it exists
			local formatterUrl = getFormatterUrl(props.prop, verified_value)
			if formatterUrl ~= '' then
				url = mw.message.newRawMessage(formatterUrl, verified_value):plain()
				if props.track then 
					output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-wd-local'), props.prop):plain()
				end
			end
		else
			-- no other choice, bail out
			return {}
		end
		local this_wiki = mw.getContentLanguage()
		local this_wiki_code = this_wiki:getCode()
		local langlink = (output[#output].langcode and output[#output].langcode ~= '' and output[#output].langcode ~= this_wiki_code) and mw.message.newRawMessage(conf:g('msg-langcode'), props.langcode, mw.language.fetchLanguageName(props.langcode, this_wiki_code)) or ""
		if short_links and props.short then
			output[#output].text =
				mw.message.newRawMessage(props.short,
					getLabel(nil, props.genitive, pagetitle),
					url,
					langlink,
					verified_value,
					mw.uri.encode(verified_value, 'PATH'))
				:plain()
		else
			output[#output].text =
				mw.message.newRawMessage(props.message,
					getLabel(nil, props.genitive, pagetitle),
					url,
					langlink,
					verified_value,
					mw.uri.encode(verified_value, 'PATH'))
				:plain()
		end
	end
	--mw.log("findMainLinksLocal returning="..dump(output))
	return output
end

local function findSiteLinksLocal(props, pagetitle, short_links, local_value)
	local output = {}
	-- to avoid deep tests
	if not props.prop then
		return nil
	end
	if not local_value or local_value == '' then
		-- bail out if no value is present
		return output
	end
	-- get the formatvalue from the property
	local verified_value = local_value
	if props.regex and props.regex ~= '' then
		-- let's verify the id
		-- maybe we'll have to convert the regex to Lua-style
		local regex = regexConverter(props.regex)
		local test = string.match( local_value, '^'..regex..'$' )
		if test then
			-- it matched, this is correct
			verified_value = local_value
		else
			verified_value = nil
		end
		
	end
	if not verified_value then
		return output
	end
	local wikidata_property = string.find(props.prop, "(SL.+)")
	local wikidata_values = {}
	if wikidata_property then
		-- get any wikidata values to see if they are equal to local values
		wikidata_values = getSitelinksFromWikidata(props)
	end
	if wikidata_property or (props.url and props.url ~= '') or (props.url_f) then
		output[#output+1] = {}
		output[#output].langcode = string.find(props.langcode, "(SL.+)") and "" or props.langcode
		--mw.log("findSiteLinksLocal - props="..dump(props).." langcode="..output[#output].langcode .." wikidata_values="..dump(wikidata_values))
		output[#output].category = {}
		local url = ''
		if props.track and wikidata_property and wikidata_values and #wikidata_values then
			local local_value_in_wikidata = false
			for _,value in ipairs( wikidata_values ) do
				if value.value == verified_value then
					local_value_in_wikidata = true
				end
			end
			output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), (local_value_in_wikidata and 'track-cat-local-wd-equal' or 'track-cat-local-wd-unequal')), props.prop):plain()
		end
		if wikidata_property and wikidata_values and #wikidata_values then
			hasdatafromwikidata = true -- signal up the chain this article has a wikidata claim
		end
		if props.url_f then
			-- we have a local defined url-formatter function, use it as first priority
			url = props.url_f(verified_value)
			if props.track then 
				output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-local-local'), props.prop):plain()
			end
		elseif props.url then
			-- we have a local defined url-formatter string, use it as second priority
			url = mw.message.newRawMessage(props.url, verified_value):plain()
			if props.track then 
				output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-local-local'), props.prop):plain()
			end
		elseif wikidata_property then
			url = verified_value:gsub(' ','_')
			if props.track then 
				output[#output].category[#output[#output].category+1] = mw.message.newRawMessage(cmodule:getMessage(contLang:getCode(), 'track-cat-wd-local'), props.prop):plain()
			end
		else
			-- no other choice, bail out
			return {}
		end
		local this_wiki = mw.getContentLanguage()
		local this_wiki_code = this_wiki:getCode()
		local langlink = (output[#output].langcode and output[#output].langcode ~= '' and output[#output].langcode ~= this_wiki_code) and mw.message.newRawMessage(conf:g('msg-langcode'), props.langcode, mw.language.fetchLanguageName(props.langcode, this_wiki_code)) or ""
		if short_links and props.short then
			output[#output].text =
				mw.message.newRawMessage(props.short,
					getLabel(nil, props.genitive, pagetitle),
					url,
					langlink,
					verified_value,
					mw.uri.encode(verified_value, 'PATH'))
				:plain()
		else
			output[#output].text =
				mw.message.newRawMessage(props.message,
					getLabel(nil, props.genitive, pagetitle),
					url,
					langlink,
					verified_value,
					mw.uri.encode(verified_value, 'PATH'))
				:plain()
		end
	end
	--mw.log("findSiteLinksLocal returning="..dump(output))
	return output
end


local function addLinkback(str, property)
	local id = mw.wikibase.getEntityObject()
	if not id then
		return str
	end
	if type(id) == 'table' then
		id = id.id
	end
	
	local class = ''
	local url = ''
	if property then
		class = 'wd_' .. string.lower(property)
		url = mw.uri.fullUrl('d:' .. id .. '#' .. property)
		url.fragment = property
	else
		url = mw.uri.fullUrl('d:' .. id )
	end
	
	local title = conf:g('wikidata-linkback-edit')
	local icon = '[%s [[File:Blue pencil.svg|%s|10px|text-top|link=]] ]'
	url = tostring(url)
	local v = mw.html.create('span')
		:addClass(class)
		:wikitext(str)
		:tag('span')
			:addClass('noprint plainlinks wikidata-linkback')
			:css('padding-left', '.3em')
			:wikitext(icon:format(url, title))
		:allDone()
	return tostring(v)
end


local function getArgument(frame, argument)
	local args = frame.args
	if args[1] == nil then
		local pFrame = frame:getParent();
		args = pFrame.args;
		for k,v in pairs( frame.args ) do
			args[k] = v;
		end
	end
	if args[argument] then
		return args[argument]
	end
	return nil
end


local function removeEntry(conf_claims, identifier, property)
	for i, props in ipairs(conf_claims) do
		if props[identifier] == property then
			table.remove(conf_claims, i)
		end
	end
	return conf_claims
end

function p.getLinks(frame)
	local configured_conf = getArgument(frame, conf:a('arg-conf'))
	if configured_conf then
		cmodule = require ('Module:External_links/conf/'..configured_conf)
	else
		error(mw.message.newRawMessage(conf:g('missing-conf'), configured_conf):plain())
	end
	local output = {}
	local category = {}
	local conf_claims = cmodule:getConfiguredClaims(contLang:getCode())
	local limits = cmodule:getLimits()
	assert(limits, mw.message.newRawMessage(conf:g('missing-limits'), configured_conf):plain())
	local links_shown = getArgument(frame, conf:a('arg-maxlink'))
	local pagetitle = getArgument(frame, conf:a('arg-title'))
	-- get a list of tracked properties from the article itself
	local requested_tracking = getArgument(frame, conf:a('arg-track'))
	if requested_tracking and requested_tracking ~= '' then
		-- the properties should be written as P1234, P2345 and other 
		-- version corresponding to the applicable property-identifiers in the config
		for track_prop in string.gmatch(requested_tracking,"([^ ,;:]+)") do
			-- get the requested properties and be able to access them
			-- like req_prop['P345'] to verify if it was requested
			local remove_track = string.match(track_prop, "^\-(.*)")
			for i,claim in ipairs ( conf_claims )  do
				if remove_track == claim.prop or remove_track == conf:a('mod-filter-all') then
					-- if a property starts with "-", then we'll simply remove that 
					-- property from the conf_claims
					conf_claims[i]['track'] = false
				elseif track_prop == claim.prop or track_prop == conf:a('mod-filter-all') then
					conf_claims[i]['track'] = true
				end
			end
		end
	end
	-- get a list of "approved" properties from the article itself
	local requested_properties = getArgument(frame, conf:a('arg-properties'))
	--mw.log("requested_properties="..dump(requested_properties))
	-- assume all properties are allowed
	local req_prop = {}
	local no_req_prop = false  -- we'll allow properties to be filtered for now 
	if requested_properties and requested_properties ~= '' then
		-- the properties should be written as P1234, P2345 and other 
		-- version corresponding to the applicable property-identifiers in the config
		for i in string.gmatch(requested_properties,"([^ ,;:]+)") do
			-- get the requested properties and be able to access them
			-- like req_prop['P345'] to verify if it was requested
			if i == conf:a('mod-filter-all') then
				-- this is a special modifier, saying we should ignore 
				-- all previous and future positive filters and remove the
				-- filter (with exception of negative filters)
				req_prop = {}
				no_req_prop = true
			end
			local remove_prop = string.match(i, "^\-(.*)")
			if remove_prop then
				-- if a property starts with "-", then we'll simply remove that 
				-- property from the conf_claims
				conf_claims = removeEntry(conf_claims, 'prop', remove_prop)
			elseif not no_req_prop then -- only if we are allowing properties to be filtered 
				req_prop[i] = 1
				-- cheat to make #req_prop indicate populated table
				req_prop[1] = 1
			end
		end
	end
	local requested_langs = getArgument(frame, conf:a('arg-languages'))
	--mw.log("requested_langs="..dump(requested_langs))
	-- assume all languages are allowed
	local req_lang = {}
	local no_req_lang = false  -- we'll allow languages to be filtered for now
	if requested_langs and requested_langs ~= '' then
		-- the languages should be written as langcodes as used in the conf_claims
		for i in string.gmatch(requested_langs,"([^ ,;:]+)") do
			-- get the requested languages and be able to access them
			if i == conf:a('mod-filter-all') then
				-- this is a special modifier, saying we should ignore 
				-- all previous and future positive filters and remove the
				-- filter (with exception of negative filters)
				req_lang = {}
				no_req_lang = true
			end
			-- like req_lang['en'] to verify if it was requested
			local remove_lang = string.match(i, "^\-(.*)")
			if remove_lang then
				-- if a language starts with "-", then we'll simply remove that 
				-- language from the conf_claims
				conf_claims = removeEntry(conf_claims, 'langcode', remove_lang)
			elseif not no_req_lang then -- only if we are allowing languages to be filtered 
				req_lang[i] = 1
				-- cheat to make #req_lang indicate populated table
				req_lang[1] = 1
			end
		end
	end
	local short_links = getArgument(frame, conf:a('arg-short'))
	if short_links and short_links ~= '' then
		short_links = true
	else
		short_links = false
	end
	local showinline = getArgument(frame, conf:a('arg-inline'))
	if showinline and showinline ~= '' then
		showinline = true
	else
		showinline = false
	end
	if not links_shown or links_shown == '' then
		links_shown = limits['links-shown'] and limits['links-shown'] or 10
	else
		links_shown = tonumber(links_shown)
	end
	local somedataonwikidata = not short_links
	--mw.log("conf_claims="..dump(conf_claims))
	--mw.log("req_prop="..dump(req_prop))
	--mw.log("req_lang="..dump(req_lang))
	--mw.log("short_links="..dump(short_links))
	for _, props in ipairs(conf_claims) do
		-- if we're called with a list of approved properties or languages, check if this one is "approved"
		if (#req_prop==0 or req_prop[props.prop]) and (#req_lang==0 or req_lang[props.langcode] or string.find(props.langcode, "([pP]%d+)")) then
			--mw.log("checking claim="..dump(props))
			local links = {}
			local checkedonwikidata = false
			-- get the any local overriding value from the call
			local wikivalue = getArgument(frame, props.prop)
			--mw.log("wikivalue="..dump(wikivalue))
			if (not wikivalue or wikivalue == "") and string.find(props.prop, "([pP]%d+)") then
				-- the property is a Pnnn type, and therefore on Wikidata
				links = findMainLinksOnWikidata(props, pagetitle, short_links)
				if links == nil then
					-- a nil-value indicated no wikidata-link
					haswikidatalink = false
					links = {}
				else
					checkedonwikidata = true
				end
			elseif (not wikivalue or wikivalue == "") and string.find(props.prop, "(SL%l+)") then
				-- this is a sitelink-type (SLspecieswiki)
				--mw.log("finding sitelinks..")
				links = findSiteLinksOnWikidata(props, pagetitle, short_links)
				if links == nil then
					-- a nil-value indicated no wikidata-link
					haswikidatalink = false
					links = {}
				else
					checkedonwikidata = true
				end
			elseif (wikivalue and wikivalue ~= "") and string.find(props.prop, "(SL%l+)") then
				-- this is a sitelink-type (SLspecieswiki)
				links = findSiteLinksLocal(props, pagetitle, short_links, wikivalue)
			elseif wikivalue and wikivalue ~= '' then
				-- the property is of another annotation, and therefore a local construct
				links = findMainLinksLocal(props, pagetitle, short_links, wikivalue)
			end
			--mw.log("links="..dump(links))
			for _,v in ipairs(links) do
				-- we'll have to check langcodes again as they may have come from wikidata
				if (#req_lang==0 or req_lang[v.langcode]) then
					if checkedonwikidata and not hasdatafromwikidata then
						-- add a general tracking category for articles with data from wikidata
						hasdatafromwikidata = true
						category[#category+1] = cmodule:getMessage(contLang:getCode(), 'with-data-cat')
					elseif not checkedonwikidata and not hasdatafromlocal then
						-- add a general tracking category for articles with data from template-calls in local articles
						hasdatafromlocal = true
						category[#category+1] = cmodule:getMessage(contLang:getCode(), 'with-local-cat')
					end
					if short_links and props.short and v.text and v.text ~= '' then
						-- if short links were requested, and a short definition exists for this property, let's use it
						if #output==0 then
							output[#output+1] = v.text
						else
							output[#output] = output[#output] .. cmodule:getMessage(contLang:getCode(),'short-list-separator') .. v.text
						end
						somedataonwikidata = true
					elseif not short_links and not showinline and v.text and v.text ~= '' then
						-- only if short links were not requested
						output[#output+1] = (#output>=1 and conf:g('msg-ul-prepend') or '')			-- if this is the first link, we won't output a list-element (msg-ul-prepend) 
							.. (checkedonwikidata and addLinkback(v.text, props.prop) or v.text)	-- if the link comes from wikidata, also output a linkback.
					elseif not short_links and showinline and v.text and v.text ~= '' then
						-- only if short links were not requested
						output[#output+1] = v.text
					end
					if props.track and v.category and #v.category then
						-- add category if tracking is on for this property and a category exists in the link-result.
						for _,cats in ipairs( v.category ) do
							category[#category+1] = cats
						end
					end
					if links_shown>0 then
						links_shown = links_shown - 1
					else
						break
					end
				end
			end
			if links_shown==0 then
				break
			end
		end
	end
	local outtext = "" 
	if short_links and #output>0 then
		-- if these are short links, output the whole thing with linkback to wikidata
		--mw.log("somedataonwikidata="..dump(somedataonwikidata).." and output="..dump(output).." and #output="..dump(#output))
		outtext = (somedataonwikidata 
			and addLinkback(table.concat(output,cmodule:getMessage(contLang:getCode(),'short-list-separator')), nil)
			or table.concat(output,cmodule:getMessage(contLang:getCode(),'short-list-separator')))
	elseif not short_links and not showinline and #output>0 then
		outtext = table.concat(output,"\n")
	elseif not short_links and showinline and #output>0 then
		outtext = table.concat(output,conf:g('msg-inline-separator'))
	end
	if not hasdatafromwikidata then
		category[#category+1] = cmodule:getMessage(contLang:getCode(), 'no-data-cat')
		if not hasdatafromlocal and not short_links then
			outtext = cmodule:getMessage(contLang:getCode(), 'no-data-text')
		end
	end
	if not haswikidatalink then
		category[#category+1] = cmodule:getMessage(contLang:getCode(), 'no-wikilink-cat')
		if not hasdatafromlocal and not short_links then
			outtext = cmodule:getMessage(contLang:getCode(), 'no-wikilink')
		end
	end
	local nocategory = getArgument(frame, conf:a('arg-no-categories'))
	category = #category>0 and "\n" .. table.concat(category,"\n") or ""
	--mw.log("nocategory="..dump(nocategory).." and outtext="..dump(outtext).." and category="..dump(category))
	outtext = outtext .. (nocategory and '' or category)
	return outtext
end

function p.getLanguageCode(frame)
	local prop = getArgument(frame, conf:a('arg-properties'))
	local output = getLanguageData(prop)
	return table.concat(output, conf:a('mod-filter-separator'))
end

return p