Module:Navigation by Wikidata

Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules


The module Navigation by Wikidata facilitates the creation of category navigation boxes which are populated from Wikidata. The text displayed in the resulting navigation boxes will be derived from Wikidata labels in the user's language, so they will be fully translated into all languages available in Wikidata. At the same time, output is formatted using the standard {{Navbox}} template, which means that the navigation boxes are nestable and consistent in design.

This module is wrapped by {{Navigation by/Wikidata}}, which is used by the Navigation by/criterion family of metatemplates, which in turn serve as a basis for the Navigation by criterion family of navigation boxes as well as for the navigation part of the {{Category description}} boxes. Do not invoke this module directly, but rather use one of these templates instead.

Usage edit

{{#invoke:Navigation by Wikidata|navigationList}}
{{#invoke:Navigation by Wikidata|navigationBlock}}
{{#invoke:Navigation by Wikidata|navigationInline}}
{{#invoke:Navigation by Wikidata|navigationBox}}

Parameters edit

See {{Navigation by/Wikidata}}.

Functions edit

style= Function Result Usage
list navigationList Bullet list of navigation items Raw data for inclusion in a more complex navigation box
block navigationBlock Both a title line and a bullet list of navigation items Navigation part of {{Category description}} templates
box navigationBox Complete navigation box Standalone navigation boxes
inline navigationInline Borderless navigation box Navigation embedded into, for example, a table

When invoked through {{Navigation by/Wikidata}}, the parameter “style” of that template determines which of the functions of this module is called.

Mode of operation edit

Navigation list items edit

There are three distinct methods to define the items of which the navigation list should consist:

  • Explicit list of Wikidata item IDs as unnamed (numbered) parameters.
Example: Unnamed parameters Q1314, Q1311, Q1312, and Q1313 produce a navigation for spring, summer, autumn, and winter.
Metatemplates using this method are called “static metatemplates”.
  • Definition of a Wikidata item, a property, and the level “children”. The property is expected to contain references to other Wikidata items, which then are used as the items to navigate through.
Example: item = Q30 (United States) and property = P150 (contains administrative territorial entity) produces a navigation for the states of the U.S.A.
  • Definition of a Wikidata item, a property, optionally a parentproperty, and the level "siblings". First, the parentproperty (or, if that's not set, the inverse property (P1696) of the property) is used to find the “parent” of the given item, and then the list is built from the children of that parent as described in the previous method.
Example: item = Q99 (California), property = P150 (contains administrative territorial entity), and parentproperty = P131 first uses the property located in the administrative territorial entity (P131) to find the "parent item" United States of America (Q30) and then produces a navigation for the states of the U.S.A.
This method is not recommended for building actual navigation templates, but it's used by the Category definition boxes.
Metatemplates using these two methods are called “dynamic metatemplates”. They leave the level parameter to their caller, so they can be used both ways.

Link targets edit

To build the link targets, a pattern must be provided where the variable part is a placeholder enclosed in << and >>. For each of the items collected as described above, this placeholder gets replaced by the value of the item's Commons category (P373) property.

Example: The Commons category (P373) of Mississippi (Q1494) is “Mississippi (state)”, so with a pattern of Nature of <<state>> the link target will be Category:Nature of Mississippi (state). Therefore it is essential that the categories are named consistently: a category named “Nature of Mississippi” or “Nature of Mississippi, USA” would not appear in the navigation list.

There are some cases where the name of the item consists of more than word, and the words must be connected with a hyphen. The hyphen parameter must be set to “yes” in these cases.

Example: For a pattern like <<century>> bridges, you would set this parameter so for 20th century (Q6927), the link target becomes Category:20th-century bridges (with the hyphen between “20th” and “century”) even though the Commons category (P373) is 20th century without the hyphen.

There are some names which, when used in some patterns, need to be prefixed with a “the”. Module:Navigation by Wikidata/special rules defines a list of Wikidata items for which this is the case, and the the parameter can be set to “yes” when this rule should be applied.

Example: For a pattern like Nature of <<country>>, you would set this parameter so for the United States of America (Q30), the link target will be Category:Nature of the United States, while for the pattern <<country>> in the 1970s, you'd not set it, because the desired link target is Category:United States in the 1970s without any “the” before “United States”.

It is possible that for the title of the navigation list, completely different rules for building the link target apply. In that case, you can use title:pattern, title:hyphen, and title:the to define the diverging rules to use for the title.

Example: With the pattern Bridges completed in <<year>>, the title would be the matching decade category so the title:pattern is <<decade>> bridges.

Link labels edit

For each item, the label in the user's language is looked up in Wikidata and used as the label. Optionally, the navigation list can be sorted alphabetically by these (translated) labels.

Automatic categorization edit

This module can also automatically add categories on the page it is used. See the autocat group of parameters in the {{Navigation by/Wikidata}} documentation for more information about this.

Code

-- =============================================================================
-- Routines for the automatic generation of navigation boxes using Wikidata
-- =============================================================================

require("strict")

local arguments    = require "Module:Arguments"
local autocat      = require "Module:Autocat"
local makesortkey  = require "Module:MakeSortKey"
local navbox       = require "Module:Navbox"
local wdLabel      = require "Module:Wikidata label"
local wdStatements = require "Module:Wikidata statements"
local specialRules = require "Module:Navigation by Wikidata/special rules"

local p = {}

-- =============================================================================
-- Helper function to compile the necessary info about a Wikidata item
-- =============================================================================

-- -----------------------------------------------------------------------------
-- This function looks for a category page with a name derived from the Wikidata
-- item and the given pattern.
-- The itemId parameter can also contain modifiers like ":catname", ":before=",
-- and ":after=", as used by Module:Navigation by countries and some explicitly
-- defined navigation lists.
-- Returns a table with the following keys:
-- * id = Wikidata item ID
-- * label = label for the item in the user's preferred language
-- * exists = true if the category page exists, false otherwise
-- * asTitle = wikitext to use if this item is the navigation title
-- * asEntry = wikitext to use if this item is an entry in the navigation list
-- This function is also used by Module:Navigation by countries
-- -----------------------------------------------------------------------------

function p._getItemInfo(itemId, index, label, pattern, hyphen, the)
	pattern = pattern or "<<x>>"	-- Safeguard against missing pattern
	local item = {index = index}
	local modifiers = {}
	-- Split into actual item ID and modifiers
	for part in string.gmatch(itemId, "[^:]+") do
		if string.find(part, "=") then
			local key, value = string.match(part, "(.+)=(.*)")
			modifiers[key] = value
		else
			item.id = part
		end
	end
	-- Find out the label in the user's language and fall back to the item ID
	item.label = label or wdLabel._getLabel(item.id, nil, "-", "ucfirst") or item.id
	-- Workaround for https://phabricator.wikimedia.org/T237884: querying the
	-- Commonscat (P373) from Wikidata is extremely slow when the overall
	-- Wikidata object is large. Therefore, the caller of this function can
	-- directly pass the "catname" value to save us from the work of looking it
	-- up. Module:Navigation by countries makes use of this feature.
	item.catname = modifiers.catname
	-- Find out the Commons category name for the given Wikidata item
	if not item.catname then
		item.catname = wdStatements.getOneValue(item.id, "P373")
	end
	if item.catname then
		-- Determine the value to use as a pattern replacement, considering
		-- special rules and parameters
		local value = item.catname
		if specialRules[item.id] and specialRules[item.id].lowercase then
			value = value:gsub("^%u", string.lower)
		end
		if hyphen == "yes" then
			value = value:gsub(" ", "-")
		end
		if the == "yes" and specialRules[item.id] and specialRules[item.id].the then
			value = "the " .. value
		end
		-- Substitute it into the pattern
		item.pagename = pattern:gsub("<<[^>]+>>", value)
		-- Add the "Category:" prefix
		local target = "Category:" .. item.pagename
		-- Build a link to the target category page
		local link = "[[:" .. target .. "|" .. item.label .. "]]"
		-- Check whether it would be a blue link
		item.exists = mw.title.new(target).exists
		-- Check whether it is the current page
		item.current = (target == mw.title.getCurrentTitle().prefixedText)
		-- For title, use a link if category exists and plain label otherwise
		item.asTitle = item.exists and link or item.label
		-- For navigation entry, use the link with "before" and "after" modifiers
		item.asEntry = (modifiers.before or "") .. link .. (modifiers.after or "")
	else
		item.exists = false
		item.current = false
		item.asTitle = item.label
		item.asEntry = item.label
	end
	return item
end

-- =============================================================================
-- Helper function to find out the parent Wikidata item of the given item
-- =============================================================================

local function getParentItem(args, itemId)
	-- Determine the parent property ID
	local parentPropertyId
	if args.parentproperty then
		parentPropertyId = args.parentproperty
	end
	if not parentPropertyId then
		parentPropertyId = wdStatements.getOnePropertyId(args.property, "P1696")
	end
	if not parentPropertyId then
		error("Missing “parentproperty” parameter")
	end
	-- In case there is more than one possible parent, take the first one
	-- which has a commons category page
	local parents = wdStatements.getItemIdList(itemId, parentPropertyId)
	for index, parentId in ipairs(parents) do
		local parentItem = p._getItemInfo(parentId, nil, nil, args["title:pattern"], args["title:hyphen"], args["title:the"])
		if #parents == 1 or parentItem.exists then
			return parentItem
		end
	end
end

-- =============================================================================
-- Helper function to find out the category to automatically add
-- =============================================================================

local function getAutoCat(args, item, baseItem)
	-- Check whether autocat is actually wanted and we're at the current page
	if not (args.autocat == "yes" and item.current) then
		return ""
	end
	-- Determine the criterion for meta categories
	local criterion = args["title:pattern"]:match("<<(.+)>>")
	-- Determine the sortkey
	local sortkey
	if args["autocat:sortkey"] == "index" then
		sortkey = string.format("%03d", item.index)
	elseif args["autocat:sortkey"] and args["autocat:sortkey"]:match("^number%d+$") then
		sortkey = string.format("%0" .. args["autocat:sortkey"]:match("%d+") .."d", item.catname:match("%d+"))
	else
		sortkey = item.catname or ""
	end
	-- Prune unwanted terms from the sortkey
	if args["autocat:prune"] then
		for part in args["autocat:prune"]:gmatch("[^;]+") do
			sortkey = sortkey:gsub("^" .. part .. " *", "")
		end
	end
	-- Put the "prefix1" before the sortkey
	sortkey = (args["autocat:prefix1"] or "") .. sortkey
	-- For dynamic metatemplates, always categorize into the parent category
	local candidates = ""
	if args["autocat:parent"] ~= "no" and baseItem then
		candidates = baseItem.pagename .. ":" .. (args["autocat:prefix2"] or "") .. "\n"
		-- Find great-parents, grand-great-parents etc.
		local parentItem = baseItem
		for generation = 1, 10 do -- limit just in case
			parentItem = getParentItem(args, parentItem.id)
			if not parentItem then
				break
			end
			-- Categorize here only if metacats exist
			if parentItem.exists then
				candidates = candidates .. parentItem.pagename .. " by " .. criterion .. ";-\n"
			end
		end
	end
	candidates = candidates .. (args["autocat:candidates"] or "")
	-- Now let Module:AutoCat do the rest of the work
	return autocat.autoCat(candidates, criterion, sortkey)
end

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

local sortfunc = {}

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

function sortfunc.label(a, b)
	local lang =  mw.getCurrentFrame():callParserFunction("int", "lang")
	local aKey = makesortkey.makeSortKey(a.label, lang)
	local bKey = makesortkey.makeSortKey(b.label, lang)
	return (aKey < bKey)
end

-- =============================================================================
-- Functions to build navigation bars
-- =============================================================================

-- -----------------------------------------------------------------------------
-- Compile navigation data as a table with the keys "title", "list", and "exists"
-- -----------------------------------------------------------------------------

local function getNavigationData(args, needsTitle)
	-- If no special title pattern is given, fall back to the general pattern
	args["title:pattern"] = args["title:pattern"] or args.pattern
	args["title:hyphen"] = args["title:hyphen"] or args.hyphen
	args["title:the"] = args["title:the"] or args.the
	-- Find out which Wikidata item to use as the starting point
	local baseItem
	if args.item and args.property then
		if args.level == "children" then
			-- For children, start at exactly the given item
			baseItem = p._getItemInfo(args.item, nil, nil, args["title:pattern"], args["title:hyphen"], args["title:the"])
		elseif args.level == "siblings" then
			-- For siblings, start at the parent of the given item
			baseItem = getParentItem(args, args.item)
			-- No parent, no navigation bar
			if not baseItem then
				return {title = {}, list = "", exists = false}
			end
		else
			error("Invalid level “" .. tostring(level) .. "”")
		end
	elseif args.level == "siblings" then
		-- No siblings with a static subtemplate
		return {title = {}, list = "", exists = false}
	end
	-- Initialize the table for the result
	local result = {}
	-- Determine which Wikidata item to use as the title
	-- Even if the title is not displayed, we might need it for the autocat feature
	if needsTitle then
		if args.title then
			result.title = p._getItemInfo(args.title, nil, nil, args["title:pattern"], args["title:hyphen"], args["title:the"])
		else
			result.title = baseItem
		end
		if not result.title then
			error("Missing “title” parameter")
		end
	end
	-- Compile the list of navigation items:
	-- Step 1: If the list is explicitly defined through a subtemplate, use that
	--         and skip the rest
	if baseItem and args.name then
		local explicitList = args.name .. "/" .. baseItem.id
		if mw.title.new(explicitList, "Template") and mw.title.new(explicitList, "Template").exists then
			-- First call the explicit list with redlinks="no" to check whether
			-- it contains only red links
			result.list = mw.getCurrentFrame():expandTemplate{
				title = explicitList,
				args = {pattern=args.pattern, hyphen=args.hyphen, the=args.the, redlinks="no"}
			}
			result.exists = result.list and string.find(result.list, "%[%[")
			-- If we actually want red links, rebuild the list with correct
			-- parameters
			if args.redlinks == "yes" then
				result.list = mw.getCurrentFrame():expandTemplate{
					title = explicitList,
					args = {pattern=args.pattern, hyphen=args.hyphen, the=args.the, redlinks=args.redlinks}
				}
			end
			return result
		end
	end
	-- Step 2: Use the Wikidata item IDs given as array arguments
	local itemList = {}
	for index, itemId in ipairs(args) do
		table.insert(itemList, p._getItemInfo(itemId, index, nil, args.pattern, args.hyphen, args.the))
	end
	-- Step 3: Add items from the Wikidata statements
	if baseItem then
		local itemIdList
		itemIdList = wdStatements.getItemIdList(baseItem.id, args.property)
		-- Don't show very long lists to avoid accessing too many Wikidata items
		if #itemIdList < 100 then
			for index, itemId in ipairs(itemIdList) do
				table.insert(itemList, p._getItemInfo(itemId, index, nil, args.pattern, args.hyphen, args.the))
			end
		end
	end
	-- Step 4: Sort as requested
	if args.sort then
		table.sort(itemList, sortfunc[args.sort])
	end
	-- Step 5: Add back and forward arrows at beginning and end if applicable
	if args.arrows == "yes" and #itemList > 0 then
		local back = wdStatements.getOneItemId(itemList[1].id, "P155")
		if back then
			table.insert(itemList, 1, p._getItemInfo(back, nil, "←", args.pattern, args.hyphen, args.the))
		end
		local forward = wdStatements.getOneItemId(itemList[#itemList].id, "P156")
		if forward then
			table.insert(itemList, p._getItemInfo(forward, nil, "→", args.pattern, args.hyphen, args.the))
		end
	end
	-- Step 6: Build wikitext output
	result.list = ""
	result.exists = false
	for index, item in ipairs(itemList) do
		if args.redlinks == "yes" or item.exists then
			result.list = result.list .. "* " .. item.asEntry .. getAutoCat(args, item, baseItem) .. "\n"
			result.exists = result.exists or item.exists
		end
	end
	return result
end

-- -----------------------------------------------------------------------------
-- Build a naked navigation list
-- -----------------------------------------------------------------------------

function p._navigationList(args)
	return getNavigationData(args, false).list
end

-- -----------------------------------------------------------------------------
-- Build a complete navigation block with title and item list
-- -----------------------------------------------------------------------------

function p._navigationBlock(args)
	local navigationData = getNavigationData(args, true)
	if navigationData.exists then
		return "; " .. (navigationData.title.asTitle) .. "\n" .. navigationData.list
	end
end

-- -----------------------------------------------------------------------------
-- Build a stand-alone navigation box
-- -----------------------------------------------------------------------------

function p._navigationBox(args)
	local navigationData = getNavigationData(args, true)
	-- Find out flag to display, if any
	local flag
	local flagFile = wdStatements.getOneValue(navigationData.title.id, "P41")
	if flagFile then
		flag = "[[File:" .. flagFile .. "|30px|border]]"
	end
	return navbox._navbox{
		name = args.name,
		title = navigationData.title.asTitle,
		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 = navigationData.list,
		below = args.below
	}
end

-- -----------------------------------------------------------------------------
-- Build an embeddable, borderless navigation box
-- -----------------------------------------------------------------------------

function p._navigationInline(args)
	local navigationData = getNavigationData(args, false)
	return navbox._navbox{
		border = "none",
		bodystyle = "background-color:transparent;",
		listclass = "hlist",
		list1 = navigationData.list
	}
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

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

return p