Module:DescriptionFromDataItem

From OpenStreetMap Wiki
Jump to navigation Jump to search
Module documentation[View] [edit] [history] [Purge]

This module is used for {{KeyDescription}} and {{ValueDescription}} templates.

It operates as a pass-through module -- it takes whatever parameters were specified on the KeyDescription or ValueDescription templates, compares them with the values stored in the data items, modifies parameters as needed, and passes them on to the {{Description}} template. It also adds a few maintenance categories to make it easier to find some issues.

Please help translate it here.

Useful Queries
Number of key and tag descriptions per language

N 8 tests failed.

Name Expected Actual
N test_english Module:DescriptionFromDataItem/testcases:137: categories

Failed to assert that nil equals expected [[Category:Item with no description in language RU|my.dbg.key]][[Category:Item with no description in language JA|my.dbg.key]]

Y test_french
N test_german Module:DescriptionFromDataItem/testcases:177: description

Failed to assert that nil equals expected desc-en&nbsp;<span class=wb-edit-pencil>[[File:Arbcom ru editing.svg|12px|Datenelement (data item) anzeigen/bearbeiten|link=Item:Q42]]</span>

N test_no_dataitem Module:DescriptionFromDataItem/testcases:238: categories

Failed to assert that nil equals expected [[Category:Missing data item in default namespace|theatre:type=amphi]][[Category:Missing data item|theatre:type=amphi]]

N test_no_dataitem_key Module:DescriptionFromDataItem/testcases:248: categories

Failed to assert that nil equals expected [[Category:Missing data item in default namespace|theatre:type]][[Category:Missing data item|theatre:type]]

N test_no_dataitem_value Module:DescriptionFromDataItem/testcases:258: categories

Failed to assert that nil equals expected [[Category:Missing data item in default namespace|theatre:type=amphi]][[Category:Missing data item|theatre:type=amphi]]

N test_polish Module:DescriptionFromDataItem/testcases:192: description

Failed to assert that nil equals expected desc-en&nbsp;<span class=wb-edit-pencil>[[File:Arbcom ru editing.svg|12px|Show/edit corresponding data item.|link=Item:Q42]]</span>

N test_polish_group Module:DescriptionFromDataItem/testcases:207: description

Failed to assert that nil equals expected desc-en&nbsp;<span class=wb-edit-pencil>[[File:Arbcom ru editing.svg|12px|Show/edit corresponding data item.|link=Item:Q42]]</span>

N test_portuguese Module:DescriptionFromDataItem/testcases:162: description

Failed to assert that nil equals expected desc-en&nbsp;<span class=wb-edit-pencil>[[File:Arbcom ru editing.svg|12px|Show/edit corresponding data item.|link=Item:Q42]]</span>

See testcases

The above documentation is transcluded from Module:DescriptionFromDataItem/doc. (edit | history)
Editors can experiment in this module's sandbox (edit | diff) and testcases (edit) pages.
Add categories to the /doc subpage. Subpages of this module.

 local getArgs = require('Module:Arguments').getArgs
 local titleParser = require('Module:OsmPageTitleParser')
 local data = mw.loadData('Module:DescriptionFromDataItem/data')
 local i18n = data.translations
 local ns = mw.title.getCurrentTitle().namespace
 local p = {}

 -- USEFUL DEBUGGING:
 -- =p.dbg{title='Key:bridge:movable'}
 -- =p.dbg{title='Tag:theatre:type=amphi'}
 -- =p.dbg{title='Tag:theatre:type=amphi', key='theatre:type', value='amphi'}
 -- =p.dbg{title='Key:bridge:movable', status='accepted'}
 -- =p.dbg{title='Tag:noexit=yes'}
 -- =p.dbg{qid='Q104'}
 -- =p.dbg{qid='Q888'}
 -- =p.stmt(p.GROUP, 'Q501', 'en')
 -- =p.stmt(p.STATUS, 'Q5846') -- status with ref

 -- =mw.text.jsonEncode(mw.wikibase.getBestStatements('Q104', p.IMAGE), mw.text.JSON_PRETTY)
 -- mw.log(mw.text.jsonEncode(stmt, mw.text.JSON_PRETTY))


 -- ##########################################################################
 -- CONSTANTS
 -- ##########################################################################

 -- "fallback" - if this property is not set on the Tag item, check the corresponding Key item
 -- "qid" - for item values, output item's Q ID
 -- "en" - for item values, output english label
 -- "map" - converts claim's value to the corresponding value in the given map
 p.INSTANCE_OF = { id = 'P2', qid = true }
 p.FORMATTER_URL = { id = 'P8' }
 p.GROUP = { id = 'P25', fallback = true }
 p.RENDER_IMAGE = { id = 'P39', fallback = true }
 p.Q_EXCEPT = { id = 'P27' }
 p.Q_LIMIT = { id = 'P26' }
 p.KEY_ID = { id = 'P16', fallback = true }
 p.KEY_PREFIX_ID = { id = 'P52', fallback = true }
 p.KEY_SUFFIX_ID = { id = 'P53', fallback = true }
 p.TAG_ID = { id = 'P19' }
 p.TAG_KEY = { id = 'P10' }
 p.REL_ID = { id = 'P41' }
 p.REL_TAG = { id = 'P40' }
 p.ROLE_REL = { id = 'P43' }
 p.INCOMPATIBLE_WITH = { id = 'P44', fallback = true, multi = true, strid = true }
 p.IMPLIES = { id = 'P45', multi = true, strid = true }
 p.COMBINATION = { id = 'P46', multi = true, strid = true }
 p.SEE_ALSO = { id = 'P18', multi = true, strid = true }
 p.REQUIRES = { id = 'P22', multi = true, strid = true }

 p.STATUS_REF = { id = 'P11', is_reference = true }
 p.IMG_CAPTION = { id = 'P47', is_qualifier = true }

 p.STATUS = { id = 'P6', en = true, extra = p.STATUS_REF }
 p.IMAGE = { id = 'P28', extra = p.IMG_CAPTION }

 local use_on_values = {
 Q8000 = 'yes',
 Q8001 = 'no',
 }

 local instance_types = {
 Q7 = { type = 'key', templatename = 'Template:KeyDescription' },
 Q22020 = { type = 'key-prefix', templatename = 'Template:KeyPrefixDescription' },
 Q22021 = { type = 'key-suffix', templatename = 'Template:KeySuffixDescription' },
 Q2 = { type = 'value', templatename = 'Template:ValueDescription' },
 Q6 = { type = 'relation', templatename = 'Template:RelationDescription' },
 }

 p.USE_ON_NODES = { id = 'P33', map = use_on_values }
 p.USE_ON_WAYS = { id = 'P34', map = use_on_values }
 p.USE_ON_AREAS = { id = 'P35', map = use_on_values }
 p.USE_ON_RELATIONS = { id = 'P36', map = use_on_values }
 p.USE_ON_CHANGESETS = { id = 'P37', map = use_on_values }

 -- Makes it possible to override by unit tests
 p.trackedLanguages = data.trackedLanguages

 -- ##########################################################################
 -- UTILITIES
 -- ##########################################################################

 local function startswith(self, str)
 return self:sub(1, #str) == str
 end

 local formatKeyVal = function(key, value)
 if value then
 return key .. '=' .. value
 else
 return key
 end
 end

 -- Normalizes yes/no/maybe into "yes", "no", nil
 local function normalizeBoolean(val)
 if val then
 val = string.lower(val)
 if val == 'yes' or val == 'no' then
 return val
 end
 end
 return nil
 end

 local function localize(key, langCode, params)
 local msgTable = i18n[key]
 local msg
 if msgTable then
 msg = msgTable[langCode] or msgTable['en']
 end
 if not msg then
 return '<' .. key .. '>'
 end
 return mw.message.newRawMessage(msg, unpack(params or {})):plain()
 end

 local function getItemValue(self, prop)
 -- Only get the first returned value, so need an extra local var step
 local value = p.getClaimValue(prop, self.langCode, self.entity, self.fallbackEntity)
 return value
 end

 -- Format as an edit link. Target is either a relative url that starts with a slash, or an item ID (e.g. Q104)
 local function editLink(self, target, msgKey)
 local file
 if msgKey == 'desc_edit_mismatch_page' then
 file = 'Red pencil.svg'
 else
 file = 'Arbcom ru editing.svg'
 end
 if not startswith(target, '/') then
 target = 'Item:' .. target
 end
 return ('&nbsp;<span class=wb-edit-pencil>[[File:' .. file .. '|12px|' ..
 localize(msgKey, self.langCode) .. '|link=' .. target .. ']]</span>')
 end

 -- Convert key:... and tag:... into {{key|...}} and {{tag|...}} in a description
 -- Debug: =p.dbgFmtDesc('abc key:xyz aaa tag:ttt:bbb=yyy:123_k, bbb')
 local function formatDescription(description, frame)
	if startswith(description, '<span') then
		-- FIXME: in case description in dataitem and wiki is different,
		-- do not perform expansion. Otherwise we would break the title="..."
		-- for the span element, creating invalid HTML.
		return description
	end
 local function repl(typ, key, value)
 local title = typ == 'key' and 'TagKey' or 'Tag'
 return frame:expandTemplate { title = title, args = {key, value} }
 end
 description = string.gsub(description, '(key):([-:_a-zA-Z0-9]+)', repl)
 description = string.gsub(description, '(tag):([-:_a-zA-Z0-9]+)=([-:_a-zA-Z0-9]+)', repl)
 return description
 end

 -- ##########################################################################
 -- DATA ITEM PARSING
 -- ##########################################################################

 -- p.Q_LIMIT "limited to region qualifier" if qualifier is present, include the statement
 -- only if self.region equals any of the listed regions
 -- p.Q_EXCEPT "excluding region qualifier" if qualifier is present, include the statement
 -- only if self.region does not equal all of the listed regions
 local regionQualifiers = { { prop = p.Q_LIMIT, include = true }, { prop = p.Q_EXCEPT, include = false } }

 -- Test if qualifiers indicate that current statement should be
 -- included or excluded based on the rules table
 -- Returns true/false if it should be included, and true/false if it was based on qualifiers
 local function allowRegion(region, statement)
 if statement.rank ~= 'preferred' and statement.rank ~= 'normal' then
 return false, false
 end
 local qualifiers = statement.qualifiers
 if qualifiers then
 for _, value in pairs(regionQualifiers) do
 local qualifier = qualifiers[value.prop.id]
 if qualifier then
 local include = not value.include
 for _, q in pairs(qualifier) do
 if region == q.datavalue.value.id then
 include = value.include
 end
 end
 -- return after the first found rule, because multiple rules
 -- do not make any sense on the same statement
 return include, true
 end
 end
 end
 return true, false -- by default, the statement should be included
 end

 local function qidToStrid(qid)
 local entity = p.wbGetEntity(qid)
 if not entity then return end

 local tag = p.getClaimValue(p.TAG_ID, 'en', entity)
 local eKey, eValue = titleParser.splitKeyValue(tag)
 if not eKey then
 eKey = p.getClaimValue(p.KEY_ID, 'en', entity)
 if eKey then
 return { eKey }
 end
 eKey = p.getClaimValue(p.KEY_PREFIX_ID, 'en', entity)
 if eKey then
 	return { eKey .. ':' }
 	end
 eKey = p.getClaimValue(p.KEY_SUFFIX_ID, 'en', entity)
 if eKey then
 	return { ':' .. eKey }
 	end
 else
 return { eKey, eValue }
 end
 end

 -- Convert claim value into a string
 -- property object specifies what value to get:
 -- 'qid' - returns data item id
 -- 'strid' - return referenced item
 -- 'map' - use a map to convert qid into a string
 -- 'en' - only english label
 -- default - first try local, then english, then qid
 local function claimToValue(datavalue, prop, langCode)
 local result = false
 if not datavalue then
 	return nil
	elseif datavalue.type == 'wikibase-entityid' then
 local qid = datavalue.value.id
 if prop.map then
 result = prop.map[qid]
 elseif prop.strid then
 result = qidToStrid(qid)
 if not result then
 result = { 'Bad item: ' .. qid }
 end
 elseif not prop.qid then
 if not prop.en then
 result = p.wbGetLabelByLang(qid, langCode)
 end
 if not result then
 result = p.wbGetLabel(qid)
 end
 end
 if not result then
 result = qid
 end
 elseif datavalue.type == 'string' then
 result = datavalue.value
 else
 -- TODO: handle other property types
 result = "Unknown datatype " .. datavalue.type
 end
 return result
 end

 local function getStatements(entity, prop, langCode)
 if prop.multi then
 return entity:getBestStatements(prop.id)
 elseif prop.id == 'P28' and langCode == 'en' then
		-- For multiple images, get the best (the image with the highest rank, e.g. "preferred") in case of English description page.
		return entity:getBestStatements(prop.id)
 else
 return entity:getAllStatements(prop.id)
 end
 end

 -- From a monolingual property, get either the given language or English
 local function getMonoString(snakList, langCode)
 local enVal, val
 if snakList then
 for _, snak in pairs(snakList) do
 local lang = snak.datavalue.value.language
 val = snak.datavalue.value.text
 if langCode == lang then
 return val
 elseif langCode == 'en' then
 enVal = val
 end
 end
 end
 return enVal or val
 end

 -- Debug: =mw.text.jsonEncode(p.getClaimValue(p.GROUP, 'en', mw.wikibase.getEntity('Q501')),0)
 function p.getClaimValue(prop, langCode, entity, fallbackEntity)
 local usedFallback = false
 local region = data.regions[langCode]
 local statements = getStatements(entity, prop, langCode)
 if fallbackEntity and prop.fallback and next(statements) == nil then
 usedFallback = true
 statements = getStatements(fallbackEntity, prop, langCode)
 end

 if prop.multi then
 local result = {}
 for _, stmt in pairs(statements) do
 local val = claimToValue(stmt.mainsnak.datavalue, prop, langCode)
 if val then
 table.insert(result, val)
 end
 end
 return result
 end

 local match
 for _, stmt in pairs(statements) do
 local include, qualified = allowRegion(region, stmt)
 if include then
 match = stmt
 if qualified then
 -- Keep non-qualified statement until we look through all claims,
 -- if we see a qualified one (limited to the current region), we found the best match
 break
 end
 end
 end

 local result
 local extra
 if match then
 -- Get extra value if available (e.g. reference or image caption)
 if prop.extra then
 if prop.extra.is_reference and match.references then
 for _, ref in pairs(match.references) do
 local snak = ref.snaks[prop.extra.id]
 if snak and snak[1] then
 extra = snak[1].datavalue.value
 break
 end
 end
 elseif prop.extra.is_qualifier and match.qualifiers then
 extra = getMonoString(match.qualifiers[prop.extra.id])
 end
 end
 result = claimToValue(match.mainsnak.datavalue, prop, langCode)
 end

 return result, usedFallback, extra
 end

 local function validateKeyValue(self)
 if self.args.type == 'key-prefix' or self.args.type == 'key-suffix' or self.args.type == 'relation' then
 -- Ignore for key-prefixes, key-suffixes and relations
 return
 end
 -- Ensure key and value are set properly in the template params
 local args = self.args
 local tag = getItemValue(self, p.TAG_ID)
 local eKey, eValue = titleParser.splitKeyValue(tag)
 if not eKey then
 eKey = getItemValue(self, p.KEY_ID)
 end

 if not args.key then
 args.key = eKey
 end
 if not args.value then
 args.value = eValue
 end
 if args.key ~= eKey or args.value ~= eValue then
 table.insert(self.categories, 'Mismatched Key or Value')
 end
 end

 -- Get categories string, e.g. "[[category:xx]][[category:yy]]"
 local function getCategories(self)
 if next(self.categories) ~= nil then
 local sortkey = formatKeyVal(self.key, self.value)
 local prefix = '[[Category:'
 local suffix = sortkey and '|' .. sortkey .. ']]' or ']]'
 return prefix .. table.concat(self.categories, suffix .. prefix) .. suffix
 else
 return nil
 end
 end

 local function formatValue(self, value, editLinkRef)
 if not editLinkRef then
 return value
 else
 return value .. editLink(self, editLinkRef, 'desc_edit')
 end
 end

 -- Process a single property, comparing old and new values
 -- add tracking categories as needed
 -- if qid is set, shows a pencil icon next to this value
 local function processValue(self, argname, entityVal, pageVal, qid)
 local args = self.args
 if pageVal == nil then
 pageVal = args[argname]
 end
 if pageVal == '' then
 pageVal = nil
 end
 if entityVal == '' then
 entityVal = nil
 end
 if not pageVal then
 if entityVal then
 -- value is only present in the entity
 if self.langCode == 'en' and ns == 0 then
 	if argname == 'status' or argname == 'description' or argname == 'image' then
 		table.insert(self.categories, 'Pages loading ' .. argname .. ' from data item')
 	end
 		if argname == 'onNode' or argname == 'onWay' or argname == 'onArea' or argname == 'onRelation' then
 			table.insert(self.categories, 'Pages loading applicabilities from data item')
 	end
 end
 args[argname] = formatValue(self, entityVal, qid)
 else
 -- value is not set in template nor in the entity
 args[argname] = nil
 end
 elseif not entityVal then
 -- value has not been copied to the entity yet
 -- if page is in main namespace or DE:/ES:/FR:/IT:/JA:/NL:/RU: then add category "Not copied ..."
 if ns == 0 or ns == 200 or ns == 202 or ns == 204 or ns == 206 or ns == 208 or ns == 210 or ns == 212 then
 table.insert(self.categories, 'Not copied ' .. argname)
 end
 args[argname] = formatValue(self, pageVal, qid)
 elseif entityVal == pageVal or
 (argname ~= 'description' and
 self.language:caseFold(entityVal) == self.language:caseFold(pageVal)) then
 -- value is identical in both entity and the page
 -- comparison is case-insensitive except for the description

 -- For now, do not track this -- there are too many of them.
 -- Once we start cleaning them up, uncomment this tracking category
 -- table.insert(self.categories, 'Redundant ' .. argname)
 args[argname] = formatValue(self, pageVal, qid)
 elseif argname == 'image' and pageVal:gsub(" ", "_") == 'Image:' .. getItemValue(self, p.IMAGE):gsub(" ", "_") then
 do return end -- Doesn't add "Category:Mismatched image" if "|image=" on wiki page is set with "Image:" instead of "File:"
 elseif argname == 'image' and pageVal:gsub(" ", "_") == 'image:' .. getItemValue(self, p.IMAGE):gsub(" ", "_") then
 do return end -- Doesn't add "Category:Mismatched image" if "|image=" on wiki page is set with "image:" instead of "File:"
 elseif argname == 'image' and pageVal:gsub(" ", "_") == 'File:' .. getItemValue(self, p.IMAGE):gsub(" ", "_") then
 args[argname] = formatValue(self, pageVal, qid)
 elseif argname == 'osmcarto-rendering' and pageVal:gsub(" ", "_") == 'File:' .. getItemValue(self, p.RENDER_IMAGE):gsub(" ", "_") then
 	-- Doesn't add "Category:Mismatched osmcarto-rendering" if only difference in filename is " " and "_".
 args[argname] = formatValue(self, pageVal, qid)
 elseif argname == 'statuslink' and entityVal == 'https://wiki.openstreetmap.org/wiki/' .. pageVal:gsub(" ", "_") then
 -- Doesn't add "Category:Mismatched statuslink" just because the type of url notation is different but leads to the same page.
 -- Notation on page: Proposed_features/foo bar
 -- Notation on item: https://wiki.openstreetmap.org/wiki/Proposed_features/foo_bar
 args[argname] = formatValue(self, pageVal, qid)
 else
 -- value in the page and in the entity do not match
 -- Don't apply maintenance categories if page is in namespace "Talk:" "User:" "Template:" "File:" "Help:" etc.
 if ns == 0 or ns > 15 then
 	if self.langCode == 'en' then
 			table.insert(self.categories, 'Mismatched ' .. argname .. ' in default namespace')
 	end
			if argname == 'description' then
				table.insert(self.categories, 'Mismatched description|' .. mw.title.getCurrentTitle().prefixedText)
			else
 		table.insert(self.categories, 'Mismatched ' .. argname)
 	end
 end
 if not qid then
 args[argname] = pageVal
 else
 -- Format when value differs between Wiki and Wikibase, with a pencil
 -- For now, show pageVal with two pencils: a red one to wiki page and gray one to Wikibase
 local editPageLink = editLink(self, self.currentTitle:fullUrl('action=edit'), 'desc_edit_mismatch_page')
 local editItemLink = editLink(self, qid, 'desc_edit_mismatch_item')

 local span = mw.html.create('span')
 span:attr('title', localize('desc_mismatch', self.langCode, { entityVal }))
 :wikitext(pageVal)
 args[argname] = tostring(span) .. editPageLink .. editItemLink

 -- In the future, switch to showing mismatched old value as red, with an edit link
 -- to the wiki page, plus new value with an edit link to Wikibase
 -- :attr('style', 'color:red')
 -- args[argname] = tostring(span) .. editPageLink .. '<br>' .. entityVal .. editItemLink
 end
 end
 end

 local function processEntity(self)
 local args = self.args
 local qid = self.entity:getId()

 validateKeyValue(self)

 processValue(self, 'project')

 -- Compare all known parameters against the data item entity
 processValue(self, 'description',
 self.entity:getDescription(self.langCode),
 args.description, qid) -- add edit links to description

 processValue(self, 'group', getItemValue(self, p.GROUP))

 -- For status we must use english label (special processing inside the template)
 local status, _, statuslink = p.getClaimValue(p.STATUS, self.langCode, self.entity, self.fallbackEntity)
 processValue(self, 'status', status)
 processValue(self, 'statuslink', statuslink)

 local image, _, image_caption = p.getClaimValue(p.IMAGE, self.langCode, self.entity, self.fallbackEntity)
 processValue(self, 'image', image and 'File:' .. image or nil)
 processValue(self, 'image_caption', image_caption)

 local render = getItemValue(self, p.RENDER_IMAGE)
 processValue(self, 'osmcarto-rendering', render and 'File:' .. render or nil)

 -- Handle onRelation, onArea, onWay, onNode, and onChangeset
 if args.type ~= 'relation' then
 	processValue(self, 'onNode', getItemValue(self, p.USE_ON_NODES), normalizeBoolean(args.onNode))
 	processValue(self, 'onWay', getItemValue(self, p.USE_ON_WAYS), normalizeBoolean(args.onWay))
 	processValue(self, 'onArea', getItemValue(self, p.USE_ON_AREAS), normalizeBoolean(args.onArea))
 	processValue(self, 'onRelation', getItemValue(self, p.USE_ON_RELATIONS), normalizeBoolean(args.onRelation))
 	processValue(self, 'onChangeset', getItemValue(self, p.USE_ON_CHANGESETS), normalizeBoolean(args.onChangeset))
 end
 
 processValue(self, 'url_pattern', getItemValue(self, p.FORMATTER_URL), args.url_pattern)

 -- Not yet possible to compare these data item values with the template params, so just use if missing
 args.combination = args.combination or getItemValue(self, p.COMBINATION)
 args.implies = args.implies or getItemValue(self, p.IMPLIES)
 args.seeAlso = args.seeAlso or getItemValue(self, p.SEE_ALSO)
 args.requires = args.requires or getItemValue(self, p.REQUIRES)

 -- Values that are coming only from the data items
 args.incompatibleWith = getItemValue(self, p.INCOMPATIBLE_WITH)
 end

 local function constructor(args)
 local self = {
 categories = {},
 args = args,
 }

 if args.currentTitle then
 self.currentTitle = mw.title.new(args.currentTitle)
 else
 self.currentTitle = mw.title.getCurrentTitle()
 end

 -- sets self.key, self.value, and self.language from the current title
 titleParser.parseTitleToObj(self, self.currentTitle)

 -- if lang parameter is set, overrides the one detected from the title
 if args.lang and mw.language.isSupportedLanguage(args.lang) then
 self.language = mw.getLanguage(args.lang)
 end
 self.langCode = self.language:getCode()

 toSitelink = function(key, value)
 return (value and 'Tag:' or 'Key:') .. formatKeyVal(key, value)
 end

 local entity
 local typeGuess
 if args.qid then
 entity = p.wbGetEntity(args.qid)
 elseif args.key then
 -- template caller gave a key param (with optional value)
 entity = p.wbGetEntity(p.wbGetEntityIdForTitle(toSitelink(args.key, args.value)))
 self.key = args.key
 self.value = args.value
 typeGuess = self.value and 'Q2' or 'Q7'
 elseif args.type then
 -- template caller gave type param, guessing a relation
 entity = p.wbGetEntity(p.wbGetEntityIdForTitle('Relation:' .. args.type))
 args.key = 'type'
 args.value = args.type
 args.rtype = args.type
 args.type = 'relation'
 typeGuess = 'Q6'
 else
 if self.currentTitle then
 -- template caller gave currentTitle param (probably debugging)
 entity = p.wbGetEntity(p.wbGetEntityIdForTitle(self.currentTitle.text))
 else
 entity = p.wbGetEntity()
 end

 -- If there is no associated entity, try to deduce it from the title (e.g. translated pages)
 -- note that we cannot guess relations the same way
 if not entity and self.key then
 entity = p.wbGetEntity(p.wbGetEntityIdForTitle(toSitelink(self.key, self.value)))
 end

 -- No data item exists
 if not entity and self.key then
 typeGuess = self.value and 'Q2' or 'Q7'
 end
 end

 -- Try to get a fallback entity - key for tag, tag for relation, relation for rel role
 self.entity = entity
 if entity then
 local _, fbStmt = next(entity:getBestStatements(p.TAG_KEY.id))
 if not fbStmt then
 _, fbStmt = next(entity:getBestStatements(p.REL_TAG.id))
 if not fbStmt then
 _, fbStmt = next(entity:getBestStatements(p.ROLE_REL.id))
 end
 end
 if fbStmt then
 self.fallbackEntity = p.wbGetEntity(fbStmt.mainsnak.datavalue.value.id)
 end
 else
 if ns == 0 and self.langCode == 'en' and not args.debug then
 if args.status == 'approved' or args.status == 'Approved' then
 	table.insert(self.categories, 'Missing data item for approved tag')
 elseif args.status == 'de facto' or args.status == 'De facto' then
 	table.insert(self.categories, 'Missing data item for de facto tag')
 elseif args.status == 'in use' or args.status == 'In use' then
 	table.insert(self.categories, 'Missing data item for tag in use')
 elseif args.status ~= 'deprecated' then
 	table.insert(self.categories, 'Missing data item for unestablished tag')
 end
 end
 if ns == 0 or ns == 200 or ns == 202 or ns == 204 or ns == 206 or ns == 208 or ns == 210 or ns == 212 then
 table.insert(self.categories, 'Missing data item')
 end
 end

 local instance_of = entity and getItemValue(self, p.INSTANCE_OF)
 local types = instance_types[instance_of or typeGuess]
 if types then
 if not args.templatename then
 args.templatename = types.templatename
 end
 if not args.type then
 args.type = types.type
 end
 if instance_of == 'Q6' then
 -- Relations are tricky - ther remap "temp" to "rtemp" and "value"
 if not args.rtype then
 args.rtype = getItemValue(self, p.REL_ID)
 end
 if not args.value then
 args.value = args.rtype
 end
 end
 end
 -- Template:Description needs these to properly format language bar and template links
 -- templatename = Template:ValueDescription | ...
 -- type = key|value|relation
 if not args.templatename then
 local frame2 = mw.getCurrentFrame():getParent()
 args.templatename = frame2 and frame2:getTitle() or 'Unknown'
 end
 if not args.type then
 if args.templatename and string.find(args.templatename, 'KeyDescription', 1, true) then
 args.type = 'key'
 elseif args.templatename and string.find(args.templatename, 'ValueDescription', 1, true) then
 args.type = 'value'
 elseif args.templatename and string.find(args.templatename, 'RelationDescription', 1, true) then
 args.type = 'relation'
 end
 end

 return self
 end

 -- ##########################################################################
 -- ENTRY POINTS
 -- ##########################################################################

 -- If we found data item for this key/tag, compare template parameters
 -- with what we have in the item, and add some extra formatting/links/...
 -- If the values are not provided, just use the ones from the data item
 function p.main(frame)
	if not mw.wikibase then
		return frame:expandTemplate { title = 'Warning', args = { text = "The OSM wiki is experiencing technical difficulties. Infoboxes will be restored soon." } }
	end
	
 local args = getArgs(frame)

 -- initialize self - parse title, language, and get relevant entities
 local self = constructor(args)

 if self.entity then
 processEntity(self)
 end

 -- if page is in main namespace or DE:/ES:/FR:/IT:/JA:/NL:/RU: then add category if no description is provided in a tracked language
 if ns == 0 or ns == 200 or ns == 202 or ns == 204 or ns == 206 or ns == 208 or ns == 210 or ns == 212 then
 if self.entity then
 -- If this module is included from [[Template:Deprecated]],
 -- omit to check if description is set or not
 if args.status ~= 'deprecated' and args.status ~= 'obsolete' then
 -- If this is an English item, check if description is set for
 -- the tracked languages, and if not, add a tracking category
 if self.langCode == 'en' then
 for _, lng in ipairs(p.trackedLanguages) do
 if not self.entity:getDescription(lng) then
 table.insert(self.categories,
 'Item with no description in language ' .. mw.ustring.upper(lng))
 end
 end
 elseif not self.entity:getDescription('en') then
 table.insert(self.categories, 'Item with no description in language EN')
 end
 end
 end
 end

 -- Create a group category. Use language-prefixed name if category page exists
 if self.args.group and not args.debug then
 local group = self.language:ucfirst(self.args.group)
 local prefix = titleParser.langPrefix(self.langCode)
 local title = mw.title.new('Category:' .. prefix .. group)
 if title and title.exists then
 table.insert(self.categories, title.text)
 else
 table.insert(self.categories, group)
 end
 end

 local categories = getCategories(self)

 local baseTemplate = args.basetemplate or 'Template:Description'
 if args.debuglua then
 -- debug and unit test support
 return {
 template = baseTemplate,
 args = args,
 categories = categories,
 }
 end

 for _, arg in pairs({ 'combination', 'implies', 'seeAlso', 'requires', 'incompatibleWith' }) do
 if type(args[arg]) == 'table' then
 local result = ''
 for _, val in pairs(args[arg]) do
 	local title = 'Tag'
 	if val[1]:sub(-1) == ':' then
 		title = 'Prefix'
 		val[1] = val[1]:sub(1, #val[1] - 1)
 	elseif val[1]:sub(1, 1) == ':' then
 				title = 'Suffix'
 				val[1] = val[1]:sub(2)
 		end
 result = result .. '* ' .. frame:expandTemplate { title = title, args = val } .. '\n'
 end
 if result ~= '' then
 args[arg] = result
 else
 args[arg] = nil
 end
 end
 end

 if args.description then
 args.description = formatDescription(args.description, frame)
 end

 local result = frame:expandTemplate { title = baseTemplate, args = args }
 if categories then
 result = result .. categories
 end
 if args.debugargs then
 result = result ..
 '<br><pre>' ..
 mw.text.nowiki(mw.text.jsonEncode(args, mw.text.JSON_PRETTY)) ..
 '</pre><br>'
 end
 return result
 end


 -- Create a table row to describe a specific value
 -- Usually rendered as key | value | element | comment | rendering | photo
 function p.row(frame)
 local args = getArgs(frame)

 if args[1] and not args.key then
 args.key = args[1]
 end
 if args[2] and not args.value then
 args.value = args[2]
 end

 -- Unlike sidecard, table could be used in different types of pages, and should not rely on auto-guessing
 assert(args.key, 'Missing key=... parameter')
 assert(args.value, 'Missing value=... parameter')

 -- initialize self - parse title, language, and get relevant entities
 local self = constructor(args)

 if self.entity then
 local qid = self.entity:getId()

 validateKeyValue(self)

 -- Compare all known parameters against the data item entity
 processValue(self, 'description',
 self.entity:getDescription(self.langCode),
 args.description, qid) -- add edit links to description

 value, usedFb, ref = p.getClaimValue(p.IMAGE, self.langCode, self.entity, self.fallbackEntity)
 processValue(self, 'photo', value and 'File:' .. value or nil)

 local render = getItemValue(self, p.RENDER_IMAGE)
 processValue(self, 'osmcarto-rendering', render and 'File:' .. render or nil)

 -- Handle onRelation, onArea, onWay, onNode, and onChangeset
 processValue(self, 'onNode', getItemValue(self, p.USE_ON_NODES), normalizeBoolean(args.onNode))
 processValue(self, 'onWay', getItemValue(self, p.USE_ON_WAYS), normalizeBoolean(args.onWay))
 processValue(self, 'onArea', getItemValue(self, p.USE_ON_AREAS), normalizeBoolean(args.onArea))
 processValue(self, 'onRelation', getItemValue(self, p.USE_ON_RELATIONS), normalizeBoolean(args.onRelation))
 processValue(self, 'onChangeset', getItemValue(self, p.USE_ON_CHANGESETS), normalizeBoolean(args.onChangeset))
 end

 local elems = {}
 if 'yes' == args.onNode then
 table.insert(elems, 'iconNode')
 end
 if 'yes' == args.onWay then
 table.insert(elems, 'iconWay')
 end
 if 'yes' == args.onArea then
 table.insert(elems, 'iconArea')
 end
 if 'yes' == args.onRelation then
 table.insert(elems, 'iconRelation')
 end

 args.render = args.render and '[[' .. args.render .. '|100px]]'
 args.photo = args.photo and '[[' .. args.photo .. '|100px]]'

 if args.description2 then
 args.description = args.description .. '<br>' .. args.description2
 end

 -- key | value | element | comment | render | photo
 local resultTbl = {
 args.key,
 args.value,
 elems,
 args.description or '',
 args.render or '',
 args.photo or '',
 }
 local categories = getCategories(self)
 if args.debuglua then
 -- debug and unit test support
 return { args = args, categories = categories, row = resultTbl }
 end

 -- expand templates
 local lang = self.langCode
 for i, v in ipairs(elems) do
 elems[i] = frame:expandTemplate { title = v }
 end
 local result = '|-\n| ' .. table.concat({
 frame:expandTemplate { title = 'TagKey/exists', args = { resultTbl[1], lang = lang } },
 frame:expandTemplate { title = 'TagValue/exists', args = { resultTbl[1], resultTbl[2], lang = lang } },
 table.concat(elems, ''),
 resultTbl[4],
 resultTbl[5],
 resultTbl[6],
 }, '\n| ')
 if categories then
 result = result .. categories
 end
 if args.debugargs then
 result = result ..
 '<br><pre>' ..
 mw.text.nowiki(mw.text.jsonEncode(resultTbl, mw.text.JSON_PRETTY)) ..
 '</pre><br><pre>' ..
 mw.text.nowiki(result) ..
 '</pre><br>'
 end
 return result
 end

 -- ##########################################################################
 -- DEBUGGING AND TESTING SUPPORT
 -- ##########################################################################
 -- From the debug console, use =p.dbg{title='Key:bridge'}
 function p.dbg(args)
 args.currentTitle = args.title or 'Key:bridge:movable'
 args.debuglua = args.debuglua == nil and true or args.debuglua
 local frame = mw.getCurrentFrame():newChild { title = 'Module:DescriptionFromDataItem', args = args }
 return mw.text.jsonEncode(p.main(frame), mw.text.JSON_PRETTY)
 end

 function p.dbgFmtDesc(desc)
 return formatDescription(desc, mw.getCurrentFrame():newChild { title = 'Module:DescriptionFromDataItem' })
 end

 -- From the debug console, use =p.dbgrow{key='bridge', value='movable'}
 function p.dbgrow(args)
 local frame = mw.getCurrentFrame():newChild { title = 'Module:DescriptionFromDataItem', args = args }
 return p.row(frame)
 end

 -- Debug helper for statements
 -- =p.stmt(p.GROUP, 'Q501', 'en')
 function p.stmt(prop, id, lang)
 return mw.text.jsonEncode(
 { p.getClaimValue(prop, lang, mw.wikibase.getEntity(id)) },
 mw.text.JSON_PRETTY)
 end

 -- These methods could be overwritten by unit tests
 function p.wbGetEntity(entity)
 return mw.wikibase.getEntity(entity)
 end

 function p.wbGetEntityIdForTitle(title)
 return mw.wikibase.getEntityIdForTitle(title)
 end

 function p.wbGetLabel(qid)
 return mw.wikibase.getLabel(qid)
 end

 function p.wbGetLabelByLang(qid, langCode)
 return mw.wikibase.getLabelByLang(qid, langCode)
 end

 return p
Retrieved from "https://wiki.openstreetmap.org/w/index.php?title=Module:DescriptionFromDataItem&oldid=2878570"