Module:Navigation by Wikidata
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
editSee {{Navigation by/Wikidata}}.
Functions
editstyle= | 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
editNavigation list items
editThere 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) andproperty = 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), andparentproperty = 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
editTo 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
editFor 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
editThis 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("d", 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