Module:Navigation by Wikidata/sandbox

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules

no dokucemtation yet.

Code

-- =============================================================================
-- Routines for the automatic generation of simple navigation boxes and complete
-- category description pages with navigation, using Wikidata
-- =============================================================================

local arguments = require "Module:Arguments"
local navbox    = require "Module:Navbox"
local wikidata  = require "Module:Wikidata label"

local p = {}

-- =============================================================================
-- Helper functions for Wikidata access
-- =============================================================================

-- -----------------------------------------------------------------------------
-- Return a single claim for a property, throw error if more than one claim
-- -----------------------------------------------------------------------------

local function getOneClaim(entityId, propertyId)
	local claims = mw.wikibase.getBestStatements(entityId, propertyId)
	if #claims > 0 then
		assert(#claims == 1, entityId .. " has more than one claim for " .. propertyId)
		return claims[1]
	else
		return nil
	end
end

-- -----------------------------------------------------------------------------
-- Return information about a claim, usable for error messages
-- -----------------------------------------------------------------------------

local function claimInfo(claim)
	return (
		"Item " .. string.match(claim.id, "^(.*)%$")
		.. ", Property " .. claim.mainsnak.property
	)
end

-- -----------------------------------------------------------------------------
-- Return the item id for a claim that points to a data item
-- -----------------------------------------------------------------------------

local function getAnyId(claim, datatype, entitytype)
	assert(
		claim.mainsnak.datatype == datatype,
		claimInfo(claim) .. " is not of datatype " .. datatype
	)
	local datavalue = assert(
		claim.mainsnak.datavalue,
		claimInfo(claim) .. " has no datavalue"
	)
	assert(
		datavalue.type == "wikibase-entityid",
		claimInfo(claim) .. "'s value is not of type wikibase-entityid"
	)
	local value = assert(
		datavalue.value,
		claimInfo(claim) .. " has no value"
	)
	assert(
		value["entity-type"] == entitytype,
		claimInfo(claim) .. "'s value's entity-type is not " .. entitytype
	)
	return assert(
		value.id,
		claimInfo(claim) .. "'s value has no id field"
	)
end

-- -----------------------------------------------------------------------------
-- Return the item id for a claim that points to a data item
-- -----------------------------------------------------------------------------

local function getItemId(claim)
	return getAnyId(claim, "wikibase-item", "item")
end

-- -----------------------------------------------------------------------------
-- Return the property id for a claim that points to a property
-- -----------------------------------------------------------------------------

local function getPropertyId(claim)
	return getAnyId(claim, "wikibase-property", "property")
end

-- =============================================================================
-- Helper functions for connecting Wikidata and Commons
-- =============================================================================

-- -----------------------------------------------------------------------------
-- Return a link to the Commons category page for a Wikidata item, using a pattern
-- -----------------------------------------------------------------------------

local function getLink(itemId, lowercase, pattern, redlink)
	-- Find out the Commons category name for the data item
	local claim = assert(
		getOneClaim(itemId, "P373"),
		itemId .. " has no property P373"
	)
	-- Determine the value to use as a pattern replacement
	local value = mw.wikibase.renderSnak(claim.mainsnak)
	if lowercase then
		value = value:gsub("^%u", string.lower)
	end
	-- Substitute it into the pattern
	local target = ":Category:" .. string.gsub(
		pattern,
		"<<[^>]+>>",
		value)
	-- Check if it would be a red link
	if not (redlink or mw.title.new(target).exists) then
		return nil
	end
	-- Return the link in wikitext
	return "[[" .. target .. "|" .. mw.wikibase.getLabel(itemId) .. "]]"
end

-- -----------------------------------------------------------------------------
-- Return the Wikidata item id for the base item
-- -----------------------------------------------------------------------------

local function getBaseItemId(itemId, propertyId, level)
	if level == "children" then
		-- For children, start at exactly this item
		return itemId
	elseif level == "siblings" then
		-- For siblings, start at the parent item
		local parentClaim = getOneClaim(propertyId, "P1696")
		if not parentClaim then
			return nil
		end
		local parentPropertyId = getPropertyId(parentClaim)
		if not parentPropertyId then
			return nil
		end
		local claim = getOneClaim(itemId, parentPropertyId)
		if not claim then
			return nil
		end
		return getItemId(claim)
	else
		error("Invalid level “" .. tostring(level) .. "”")
	end
end

-- =============================================================================
-- Sort functions
-- =============================================================================

local sortfunc = {}

-- -----------------------------------------------------------------------------
-- By label
-- -----------------------------------------------------------------------------

function sortfunc.label(a, b)
	return (mw.wikibase.getLabel(a) or '') < (mw.wikibase.getLabel(b) or '')
end

-- =============================================================================
-- Functions to build navigation blocks
-- =============================================================================

-- -----------------------------------------------------------------------------
-- Determine Wikidata item ID for the navigation title
-- Arguments: title, item, property, level
-- -----------------------------------------------------------------------------

local function getTitleItemId(args)
	if args.title then
		return args.title
	elseif args.item and args.property and args.level then
		return getBaseItemId(args.item, args.property, args.level)
	end
end

-- -----------------------------------------------------------------------------
-- Build navigation title
-- Arguments: title, item, property, level, pattern
-- -----------------------------------------------------------------------------

local function getTitle(args)
	local titleItemId = getTitleItemId(args)
	if not titleItemId then
		return nil
	end
	-- Instead of a red link, return just the label without any link
	return getLink(titleItemId, false, args.pattern, false) or mw.wikibase.getLabel(titleItemId)
end

-- -----------------------------------------------------------------------------
-- Build navigation list
-- Arguments: item, property, level, sort, lowercase, pattern, redlinks, [itemId...]
-- -----------------------------------------------------------------------------

function p._navigationList(args)
	-- Start with the Wikidata item IDs given as array arguments
	local itemIdList = {}
	for i, value in ipairs(args) do
		table.insert(itemIdList, value)
	end
	-- Add items from the Wikidata statements
	if args.item and args.property and args.level then
		-- Find out which Wikidata item to use as the starting point
		local baseItemId = getBaseItemId(args.item, args.property, args.level)
		if not baseItemId then
			return nil
		end
		-- Add the Wikidata item IDs from the Wikidata statements to our list
		for k, claim in pairs(mw.wikibase.getBestStatements(baseItemId, args.property)) do
			table.insert(itemIdList, (getItemId(claim)))
		end
	end
	-- Sort as requested
	if args.sort then
		table.sort(itemIdList, sortfunc[args.sort])
	end
	-- Build wikitext output
	local result = ""
	for i, itemId in ipairs(itemIdList) do
		local x = getLink(itemId, args.lowercase, args.pattern, args.redlinks)
		if x then
			result = result .. "* " .. x  .. "\n"
		end
	end
	return result
end

-- -----------------------------------------------------------------------------
-- Build a complete navigation block with title and item list
-- Arguments: title, item, property, level, sort, lowercase, pattern, redlinks, [ItemId...]
-- -----------------------------------------------------------------------------

function p._navigationBlock(args)
	-- If level is not defined, do both siblings and children
	if args.item and args.property and not args.level then
		local result = ""
		args.level = "siblings"
		result = result .. (p._navigationBlock(args) or "")
		args.level = "children"
		result = result .. (p._navigationBlock(args) or "")
		return result
	end
	-- Check if the list would only have red links
	local redlinks = args.redlinks
	args.redlinks = false
	local existing = p._navigationList(args)
	args.redlinks = redlinks
	if not existing or existing == "" then
		return nil
	end
	-- Now build the real navigation data
	local title = getTitle(args)
	local list = p._navigationList(args)
	if not (title and list) then
		return nil
	end
	return "; " .. title .. "\n" .. list
end

-- -----------------------------------------------------------------------------
-- Build a stand-alone navigation box
-- Arguments: title, item, property, level, sort, lowercase, pattern, redlinks, [ItemId...]
-- -----------------------------------------------------------------------------

function p._navigationBox(args)
	-- Find out flag to display, if any
	local flag
	local titleItemId = getTitleItemId(args)
	if titleItemId then
		local flagClaim = getOneClaim(titleItemId, "P41")
		if flagClaim then
			flag = "[[File:" .. flagClaim.mainsnak.datavalue.value .. "|30px|border]]"
		end
	end
	return navbox._navbox{
		name = args.name,
		title = getTitle(args),
		above = args.above,
		imageleftstyle = "width: 1px;", -- Workaround for a bug in Module:Navbox that breaks formatting on Chrome
		imageleft = flag,
		listclass = "hlist",
		liststyle = "width: auto;", -- Workaround for a bug in Module:Navbox that breaks formatting on Chrome
		list1 = p._navigationList(args),
		below = args.below
	}
end

-- -----------------------------------------------------------------------------
-- Build an embeddable, borderless navigation box
-- Arguments: title, item, property, level, sort, lowercase, pattern, redlinks, [ItemId...]
-- -----------------------------------------------------------------------------

function p._navigationInline(args)
	return navbox._navbox{
		border = "none",
		bodystyle = "background-color:transparent;",
		listclass = "hlist",
		list1 = p._navigationList(args)
	}
end

-- =============================================================================
-- Functions to build complete category description boxes
-- =============================================================================

-- -----------------------------------------------------------------------------
-- Escape magic characters for a regular expression
-- -----------------------------------------------------------------------------

local function regExEscape(pattern)
	return string.gsub(pattern, "([%(%)%.%%%+%-%*%?%[%^%$])", "%%%1")
end

-- -----------------------------------------------------------------------------
-- Helper function to convert a pattern as given in the parameter into a
-- Lua-style regular expression
-- -----------------------------------------------------------------------------

local function makeRegEx(pattern, variable)
	-- Change underlines into spaces
	local regex = string.gsub(pattern, "_", " ")
	-- Escape any magic characters in the pattern
	regex = regExEscape(regex)
	-- Change <<...>> into regex placeholders
	regex = string.gsub(regex, "<<" .. variable .. ">>", "(.*)")
	regex = string.gsub(regex, "<<.*>>", ".*")
	-- Add beginning and end marks
	regex = "^" .. regex .. "$"
	return regex
end

-- -----------------------------------------------------------------------------
-- Determine navigation data for a page and a pattern
-- -----------------------------------------------------------------------------

local function getNavigationBlocks(pagename, pattern, title)
	if not pagename then
		return title, ""
	end
	local frame = mw.getCurrentFrame()
	local blocks = ""
	for variable in string.gmatch(pattern, "<<([^>]+)>>") do
		-- Determine variable content in current page name
		local content = assert(
			string.match(pagename,makeRegEx(pattern, variable)),
			"“" .. pagename .. "” does not match “" .. pattern .. "”"
		)
		-- Find out which wikidata entity matches the variable content
		local CategoryItemId = assert(
			mw.wikibase.getEntityIdForTitle('Category:' .. content),
			"Page “Category:" .. content .. "” not found"
		)
		local claim = assert(
			getOneClaim(CategoryItemId, "P301"),
			CategoryItemId .. " has no property P301"
		)
		local itemId = getItemId(claim)
		-- Split the variable at each ":" to separate modifiers
		local navigationArgs = {
			style = "block",
			item = itemId
		}
		local template = nil
		for part in string.gmatch(variable, "[^:]+") do
			if not template then
				template = part
			elseif part == "redlinks" then
				navigationArgs.redlinks = "yes"
			elseif part == "noredlinks" then
				navigationArgs.redlinks = ""
			elseif part == "nochildren" then
				navigationArgs.level = "siblings"
			end
		end
		-- Find out the navigation pattern for this variable
		navigationArgs.pattern = string.gsub(
			pagename,
			regExEscape(content),
			"<<" .. template .. ">>"
		)
		-- Call the right template
		local block = frame:expandTemplate{
			title = "Navigation by/" .. template,
			args = navigationArgs
		}
		if title ~= "" then
			title = title .. " - "
		end
		local lang = frame:callParserFunction( "int", "lang" ) -- get user's chosen language 
		title = title .. wikidata._getLabel(itemId, lang, "wikipedia")
		blocks = blocks .. block .. "\n"
	end
	return title, blocks
end

-- -----------------------------------------------------------------------------
-- Build a box with description and navigation blocks
-- Arguments: name, description, remarks, pattern, pagename
-- -----------------------------------------------------------------------------

function p._categoryDescription(args)
	local boxargs = {
		name = args.name,
		above = args.description,
		listclass = "hlist",
		below = args.remarks
	}
	local blocks
	boxargs.title, blocks = getNavigationBlocks(
		args.pagename,
		args.pattern or "",
		args.title or ""
	)
	local count = 0
	for start, line in string.gmatch(blocks, "([%*;]) *([^\n]+)") do
		if start == ";" then
			count = count + 1
			boxargs["group" .. tostring(count)] = line
			boxargs["list" .. tostring(count)] = ""
		elseif count > 0 then
			boxargs["list" .. tostring(count)] = (
				boxargs["list" .. tostring(count)]
				.. "* " .. line .. "\n"
			)
		end
	end
	return navbox._navbox(boxargs)
end

-- =============================================================================
-- Function wrappers for usage with #invoke
-- =============================================================================

function p.navigationList(frame)
	return p._navigationList(arguments.getArgs(frame))
end

function p.navigationBlock(frame)
	return p._navigationBlock(arguments.getArgs(frame))
end

function p.navigationBox(frame)
	return p._navigationBox(arguments.getArgs(frame))
end

function p.navigationInline(frame)
	return p._navigationInline(arguments.getArgs(frame))
end

function p.categoryDescription(frame)
	return p._categoryDescription(arguments.getArgs(frame))
end

-- =============================================================================

return p