Module:DescriptionFromDataItem

From OpenStreetMap Wiki
Jump to navigation Jump to search
[Edit] [Purge] Documentation

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

☑Y All tests passed.

Name Expected Actual
☑Y test_english
☑Y test_french
☑Y test_german
☑Y test_no_dataitem
☑Y test_no_dataitem_key
☑Y test_no_dataitem_value
☑Y test_polish
☑Y test_polish_group
☑Y test_portuguese

See testcases

  1 local getArgs = require('Module:Arguments').getArgs
  2 local titleParser = require('Module:OsmPageTitleParser')
  3 local data = mw.loadData('Module:DescriptionFromDataItem/data')
  4 local i18n = data.translations
  5 local p = {}
  6 
  7 -- USEFUL DEBUGGING:
  8 --   =p.dbg{title='Key:bridge:movable'}
  9 --   =p.dbg{title='Tag:theatre:type=amphi'}
 10 --   =p.dbg{title='Tag:theatre:type=amphi', key='theatre:type', value='amphi'}
 11 --   =p.dbg{title='Key:bridge:movable', status='accepted'}
 12 --   =p.dbg{title='Tag:noexit=yes'}
 13 --   =p.dbg{qid='Q104'}
 14 --   =p.dbg{qid='Q888'}
 15 --   =p.stmt(p.GROUP, 'Q501', 'en')
 16 --   =p.stmt(p.STATUS, 'Q5846')  -- status with ref
 17 
 18 --   =mw.text.jsonEncode(mw.wikibase.getBestStatements('Q104', p.IMAGE), mw.text.JSON_PRETTY)
 19 --   mw.log(mw.text.jsonEncode(stmt, mw.text.JSON_PRETTY))
 20 
 21 
 22 -- ##########################################################################
 23 --                             CONSTANTS
 24 -- ##########################################################################
 25 
 26 -- "fallback" - if this property is not set on the Tag item, check the corresponding Key item
 27 -- "qid" - for item values, output item's Q ID
 28 -- "en" - for item values, output english label
 29 -- "map" - converts claim's value to the corresponding value in the given map
 30 p.INSTANCE_OF = { id = 'P2', qid = true }
 31 p.GROUP = { id = 'P25', fallback = true }
 32 p.RENDER_IMAGE = { id = 'P39', fallback = true }
 33 p.Q_EXCEPT = { id = 'P27' }
 34 p.Q_LIMIT = { id = 'P26' }
 35 p.KEY_ID = { id = 'P16', fallback = true }
 36 p.TAG_ID = { id = 'P19' }
 37 p.TAG_KEY = { id = 'P10' }
 38 p.REL_ID = { id = 'P41' }
 39 p.REL_TAG = { id = 'P40' }
 40 p.ROLE_REL = { id = 'P43' }
 41 p.INCOMPATIBLE_WITH = { id = 'P44', fallback = true, multi = true, strid = true }
 42 p.IMPLIES = { id = 'P45', multi = true, strid = true }
 43 p.COMBINATION = { id = 'P46', multi = true, strid = true }
 44 p.SEE_ALSO = { id = 'P18', multi = true, strid = true }
 45 p.REQUIRES = { id = 'P22', multi = true, strid = true }
 46 
 47 p.STATUS_REF = { id = 'P11', is_reference = true }
 48 p.IMG_CAPTION = { id = 'P47', is_qualifier = true }
 49 
 50 p.STATUS = { id = 'P6', en = true, extra = p.STATUS_REF }
 51 p.IMAGE = { id = 'P28', extra = p.IMG_CAPTION }
 52 
 53 local use_on_values = {
 54     Q8000 = 'yes',
 55     Q8001 = 'no',
 56 }
 57 
 58 local instance_types = {
 59     Q7 = { type = 'key', templatename = 'Template:KeyDescription' },
 60     Q2 = { type = 'value', templatename = 'Template:ValueDescription' },
 61     Q6 = { type = 'relation', templatename = 'Template:RelationDescription' },
 62 }
 63 
 64 p.USE_ON_NODES = { id = 'P33', fallback = true, map = use_on_values }
 65 p.USE_ON_WAYS = { id = 'P34', fallback = true, map = use_on_values }
 66 p.USE_ON_AREAS = { id = 'P35', fallback = true, map = use_on_values }
 67 p.USE_ON_RELATIONS = { id = 'P36', fallback = true, map = use_on_values }
 68 
 69 -- Makes it possible to override by unit tests
 70 p.trackedLanguages = data.trackedLanguages
 71 
 72 -- ##########################################################################
 73 --                                   UTILITIES
 74 -- ##########################################################################
 75 
 76 local function startswith(self, str)
 77     return self:sub(1, #str) == str
 78 end
 79 
 80 local formatKeyVal = function(key, value)
 81     if value then
 82         return key .. '=' .. value
 83     else
 84         return key
 85     end
 86 end
 87 
 88 -- Normalizes yes/no/maybe into "yes", "no", nil
 89 local function normalizeBoolean(val)
 90     if val then
 91         val = string.lower(val)
 92         if val == 'yes' or val == 'no' then
 93             return val
 94         end
 95     end
 96     return nil
 97 end
 98 
 99 local function localize(key, langCode, params)
100     local msgTable = i18n[key]
101     local msg
102     if msgTable then
103         msg = msgTable[langCode] or msgTable['en']
104     end
105     if not msg then
106         return '<' .. key .. '>'
107     end
108     return mw.message.newRawMessage(msg, unpack(params or {})):plain()
109 end
110 
111 local function getItemValue(self, prop)
112     -- Only get the first returned value, so need an extra local var step
113     local value = p.getClaimValue(prop, self.langCode, self.entity, self.fallbackEntity)
114     return value
115 end
116 
117 -- Format as an edit link. Target is either a relative url that starts with a slash, or an item ID (e.g. Q104)
118 local function editLink(self, target, msgKey)
119     local file
120     if msgKey == 'desc_edit_mismatch_page' then
121         file = 'Red pencil.svg'
122     else
123         file = 'Arbcom ru editing.svg'
124     end
125     if not startswith(target, '/') then
126         target = 'Item:' .. target
127     end
128     return ('&nbsp;<span class=wb-edit-pencil>[[File:' .. file .. '|12px|' ..
129             localize(msgKey, self.langCode) .. '|link=' .. target .. ']]</span>')
130 end
131 
132 -- Convert  key:... and tag:...  into {{key|...}} and {{tag|...}} in a description
133 -- Debug: =p.dbgFmtDesc('abc key:xyz aaa tag:ttt:bbb=yyy:123_k, bbb')
134 local function formatDescription(description, frame)
135 	if startswith(description, '<span') then
136 		-- FIXME: in case description in dataitem and wiki is different,
137 		-- do not perform expansion. Otherwise we would break the title="..."
138 		-- for the span element, creating invalid HTML.
139 		return description
140 	end
141     local function repl(typ, key, value)
142         local title = typ == 'key' and 'TagKey' or 'Tag'
143         return frame:expandTemplate { title = title, args = {key, value} }
144     end
145     description = string.gsub(description, '(key):([-:_a-zA-Z0-9]+)', repl)
146     description = string.gsub(description, '(tag):([-:_a-zA-Z0-9]+)=([-:_a-zA-Z0-9]+)', repl)
147     return description
148 end
149 
150 -- ##########################################################################
151 --                              DATA ITEM PARSING
152 -- ##########################################################################
153 
154 -- p.Q_LIMIT  "limited to region qualifier"  if qualifier is present, include the statement
155 --      only if self.region equals any of the listed regions
156 -- p.Q_EXCEPT "excluding region qualifier"   if qualifier is present, include the statement
157 --      only if self.region does not equal all of the listed regions
158 local regionQualifiers = { { prop = p.Q_LIMIT, include = true }, { prop = p.Q_EXCEPT, include = false } }
159 
160 -- Test if qualifiers indicate that current statement should be
161 -- included or excluded based on the rules table
162 -- Returns true/false if it should be included, and true/false if it was based on qualifiers
163 local function allowRegion(region, statement)
164     if statement.rank ~= 'preferred' and statement.rank ~= 'normal' then
165         return false, false
166     end
167     local qualifiers = statement.qualifiers
168     if qualifiers then
169         for _, value in pairs(regionQualifiers) do
170             local qualifier = qualifiers[value.prop.id]
171             if qualifier then
172                 local include = not value.include
173                 for _, q in pairs(qualifier) do
174                     if region == q.datavalue.value.id then
175                         include = value.include
176                     end
177                 end
178                 -- return after the first found rule, because multiple rules
179                 -- do not make any sense on the same statement
180                 return include, true
181             end
182         end
183     end
184     return true, false -- by default, the statement should be included
185 end
186 
187 local function qidToStrid(qid)
188     local entity = p.wbGetEntity(qid)
189     if not entity then return end
190 
191     local tag = p.getClaimValue(p.TAG_ID, 'en', entity)
192     local eKey, eValue = titleParser.splitKeyValue(tag)
193     if not eKey then
194         eKey = p.getClaimValue(p.KEY_ID, 'en', entity)
195         if eKey then
196             return { eKey }
197         end
198     else
199         return { eKey, eValue }
200     end
201 end
202 
203 -- Convert claim value into a string
204 -- property object specifies what value to get:
205 --  'qid'    - returns data item id
206 --  'strid'  - return referenced item
207 --  'map'    - use a map to convert qid into a string
208 --  'en'     - only english label
209 --   default - first try local, then english, then qid
210 local function claimToValue(datavalue, prop, langCode)
211     local result = false
212     if not datavalue then
213     	return nil
214 	elseif datavalue.type == 'wikibase-entityid' then
215         local qid = datavalue.value.id
216         if prop.map then
217             result = prop.map[qid]
218         elseif prop.strid then
219             result = qidToStrid(qid)
220             if not result then
221                 result = { 'Bad item: ' .. qid }
222             end
223         elseif not prop.qid then
224             if not prop.en then
225                 result = p.wbGetLabelByLang(qid, langCode)
226             end
227             if not result then
228                 result = p.wbGetLabel(qid)
229             end
230         end
231         if not result then
232             result = qid
233         end
234     elseif datavalue.type == 'string' then
235         result = datavalue.value
236     else
237         -- TODO:  handle other property types
238         result = "Unknown datatype " .. datavalue.type
239     end
240     return result
241 end
242 
243 local function getStatements(entity, prop)
244     if prop.multi then
245         return entity:getBestStatements(prop.id)
246     else
247         return entity:getAllStatements(prop.id)
248     end
249 end
250 
251 -- From a monolingual property, get either the given language or English
252 local function getMonoString(snakList, langCode)
253     local enVal, val
254     if snakList then
255         for _, snak in pairs(snakList) do
256             local lang = snak.datavalue.value.language
257             val = snak.datavalue.value.text
258             if langCode == lang then
259                 return val
260             elseif langCode == 'en' then
261                 enVal = val
262             end
263         end
264     end
265     return enVal or val
266 end
267 
268 -- Debug:  =mw.text.jsonEncode(p.getClaimValue(p.GROUP, 'en', mw.wikibase.getEntity('Q501')),0)
269 function p.getClaimValue(prop, langCode, entity, fallbackEntity)
270     local usedFallback = false
271     local region = data.regions[langCode]
272     local statements = getStatements(entity, prop)
273     if fallbackEntity and prop.fallback and next(statements) == nil then
274         usedFallback = true
275         statements = getStatements(fallbackEntity, prop)
276     end
277 
278     if prop.multi then
279         local result = {}
280         for _, stmt in pairs(statements) do
281             local val = claimToValue(stmt.mainsnak.datavalue, prop, langCode)
282             if val then
283                 table.insert(result, val)
284             end
285         end
286         return result
287     end
288 
289     local match
290     for _, stmt in pairs(statements) do
291         local include, qualified = allowRegion(region, stmt)
292         if include then
293             match = stmt
294             if qualified then
295                 -- Keep non-qualified statement until we look through all claims,
296                 -- if we see a qualified one (limited to the current region), we found the best match
297                 break
298             end
299         end
300     end
301 
302     local result
303     local extra
304     if match then
305         -- Get extra value if available (e.g. reference or image caption)
306         if prop.extra then
307             if prop.extra.is_reference and match.references then
308                 for _, ref in pairs(match.references) do
309                     local snak = ref.snaks[prop.extra.id]
310                     if snak and snak[1] then
311                         extra = snak[1].datavalue.value
312                         break
313                     end
314                 end
315             elseif prop.extra.is_qualifier and match.qualifiers then
316                 extra = getMonoString(match.qualifiers[prop.extra.id])
317             end
318         end
319         result = claimToValue(match.mainsnak.datavalue, prop, langCode)
320     end
321 
322     return result, usedFallback, extra
323 end
324 
325 local function validateKeyValue(self)
326     if self.args.type == 'relation' then
327         -- Ignore for relations
328         return
329     end
330     -- Ensure key and value are set properly in the template params
331     local args = self.args
332     local tag = getItemValue(self, p.TAG_ID)
333     local eKey, eValue = titleParser.splitKeyValue(tag)
334     if not eKey then
335         eKey = getItemValue(self, p.KEY_ID)
336     end
337 
338     if not args.key then
339         args.key = eKey
340     end
341     if not args.value then
342         args.value = eValue
343     end
344     if args.key ~= eKey or args.value ~= eValue then
345         table.insert(self.categories, 'Mismatched Key or Value')
346     end
347 end
348 
349 -- Get categories string, e.g. "[[category:xx]][[category:yy]]"
350 local function getCategories(self)
351     if next(self.categories) ~= nil then
352         local sortkey = formatKeyVal(self.key, self.value)
353         local prefix = '[[Category:'
354         local suffix = sortkey and '|' .. sortkey .. ']]' or ']]'
355         return prefix .. table.concat(self.categories, suffix .. prefix) .. suffix
356     else
357         return nil
358     end
359 end
360 
361 local function formatValue(self, value, editLinkRef)
362     if not editLinkRef then
363         return value
364     else
365         return value .. editLink(self, editLinkRef, 'desc_edit')
366     end
367 end
368 
369 -- Process a single property, comparing old and new values
370 -- add tracking categories as needed
371 -- if qid is set, shows a pencil icon next to this value
372 local function processValue(self, argname, entityVal, pageVal, qid)
373     local args = self.args
374     if pageVal == nil then
375         pageVal = args[argname]
376     end
377     if pageVal == '' then
378         pageVal = nil
379     end
380     if entityVal == '' then
381         entityVal = nil
382     end
383     if not pageVal then
384         if entityVal then
385             -- value is only present in the entity
386             args[argname] = formatValue(self, entityVal, qid)
387         else
388             -- value is not set in template nor in the entity
389             args[argname] = nil
390         end
391     elseif not entityVal then
392         -- value has not been copied to the entity yet
393         table.insert(self.categories, 'Not copied ' .. argname)
394         args[argname] = formatValue(self, pageVal, qid)
395     elseif entityVal == pageVal or
396             (argname ~= 'description' and
397                     self.language:caseFold(entityVal) == self.language:caseFold(pageVal)) then
398         -- value is identical in both entity and the page
399         -- comparison is case-insensitive except for the description
400 
401         -- For now, do not track this -- there are too many of them.
402         -- Once we start cleaning them up, uncomment this tracking category
403         -- table.insert(self.categories, 'Redundant ' .. argname)
404         args[argname] = formatValue(self, pageVal, qid)
405     else
406         -- value in the page and in the entity do not match
407         table.insert(self.categories, 'Mismatched ' .. argname)
408         if not qid then
409             args[argname] = pageVal
410         else
411             -- Format when value differs between Wiki and Wikibase, with a pencil
412             -- For now, show pageVal with two red pencils: a red one to wiki page and gray one to Wikibase
413             local editPageLink = editLink(self, self.currentTitle:fullUrl('action=edit'), 'desc_edit_mismatch_page')
414             local editItemLink = editLink(self, qid, 'desc_edit_mismatch_item')
415 
416             local span = mw.html.create('span')
417             span:attr('title', localize('desc_mismatch', self.langCode, { entityVal }))
418                 :wikitext(pageVal)
419             args[argname] = tostring(span) .. editPageLink .. editItemLink
420 
421             -- In the future, switch to showing mismatched old value as red, with an edit link
422             -- to the wiki page, plus new value with an edit link to Wikibase
423             -- :attr('style', 'color:red')
424             -- args[argname] = tostring(span) .. editPageLink .. '<br>' .. entityVal .. editItemLink
425         end
426     end
427 end
428 
429 local function processEntity(self)
430     local args = self.args
431     local qid = self.entity:getId()
432 
433     validateKeyValue(self)
434 
435     -- Compare all known parameters against the data item entity
436     processValue(self, 'description',
437             self.entity:getDescription(self.langCode) or self.entity:getDescription(),
438             args.description, qid) -- add edit links to description
439 
440     processValue(self, 'group', getItemValue(self, p.GROUP))
441 
442     -- For status we must use english label (special processing inside the template)
443     local status, _, statuslink = p.getClaimValue(p.STATUS, self.langCode, self.entity, self.fallbackEntity)
444     processValue(self, 'status', status)
445     processValue(self, 'statuslink', statuslink)
446 
447     local image, _, image_caption = p.getClaimValue(p.IMAGE, self.langCode, self.entity, self.fallbackEntity)
448     processValue(self, 'image', image and 'File:' .. image or nil)
449     processValue(self, 'image_caption', image_caption)
450 
451     local render = getItemValue(self, p.RENDER_IMAGE)
452     processValue(self, 'osmcarto-rendering', render and 'File:' .. render or nil)
453 
454     -- Handle onRelation, onArray, onWay, and onNode
455     processValue(self, 'onNode', getItemValue(self, p.USE_ON_NODES), normalizeBoolean(args.onNode))
456     processValue(self, 'onWay', getItemValue(self, p.USE_ON_WAYS), normalizeBoolean(args.onWay))
457     processValue(self, 'onArea', getItemValue(self, p.USE_ON_AREAS), normalizeBoolean(args.onArea))
458     processValue(self, 'onRelation', getItemValue(self, p.USE_ON_RELATIONS), normalizeBoolean(args.onRelation))
459 
460     -- Not yet possible to compare these data item values with the template params, so just use if missing
461     args.combination = args.combination or getItemValue(self, p.COMBINATION)
462     args.implies = args.implies or getItemValue(self, p.IMPLIES)
463     args.seeAlso = args.seeAlso or getItemValue(self, p.SEE_ALSO)
464     args.requires = args.requires or getItemValue(self, p.REQUIRES)
465 
466     -- Values that are coming only from the data items
467     args.incompatibleWith = getItemValue(self, p.INCOMPATIBLE_WITH)
468 end
469 
470 local function constructor(args)
471     local self = {
472         categories = {},
473         args = args,
474     }
475 
476     if args.currentTitle then
477         self.currentTitle = mw.title.new(args.currentTitle)
478     else
479         self.currentTitle = mw.title.getCurrentTitle()
480     end
481 
482     -- sets self.key, self.value, and self.language from the current title
483     titleParser.parseTitleToObj(self, self.currentTitle)
484 
485     -- if lang parameter is set, overrides the one detected from the title
486     if args.lang and mw.language.isSupportedLanguage(args.lang) then
487         self.language = mw.getLanguage(args.lang)
488     end
489     self.langCode = self.language:getCode()
490 
491     toSitelink = function(key, value)
492         return (value and 'Tag:' or 'Key:') .. formatKeyVal(key, value)
493     end
494 
495     local entity
496     local typeGuess
497     if args.qid then
498         entity = p.wbGetEntity(args.qid)
499     elseif args.key then
500         -- template caller gave a key param (with optional value)
501         entity = p.wbGetEntity(p.wbGetEntityIdForTitle(toSitelink(args.key, args.value)))
502         self.key = args.key
503         self.value = args.value
504         typeGuess = self.value and 'Q2' or 'Q7'
505     elseif args.type then
506         -- template caller gave type param, guessing a relation
507         entity = p.wbGetEntity(p.wbGetEntityIdForTitle('Relation:' .. args.type))
508         args.key = 'type'
509         args.value = args.type
510         args.rtype = args.type
511         args.type = 'relation'
512         typeGuess = 'Q6'
513     else
514         if self.currentTitle then
515             -- template caller gave currentTitle param (probably debugging)
516             entity = p.wbGetEntity(p.wbGetEntityIdForTitle(self.currentTitle.text))
517         else
518             entity = p.wbGetEntity()
519         end
520 
521         -- If there is no associated entity, try to deduce it from the title (e.g. translated pages)
522         -- note that we cannot guess relations the same way
523         if not entity and self.key then
524             entity = p.wbGetEntity(p.wbGetEntityIdForTitle(toSitelink(self.key, self.value)))
525         end
526 
527         -- No data item exists
528         if not entity and self.key then
529             typeGuess = self.value and 'Q2' or 'Q7'
530         end
531     end
532 
533     -- Try to get a fallback entity - key for tag, tag for relation, relation for rel role
534     self.entity = entity
535     if entity then
536         local _, fbStmt = next(entity:getBestStatements(p.TAG_KEY.id))
537         if not fbStmt then
538             _, fbStmt = next(entity:getBestStatements(p.REL_TAG.id))
539             if not fbStmt then
540                 _, fbStmt = next(entity:getBestStatements(p.ROLE_REL.id))
541             end
542         end
543         if fbStmt then
544             self.fallbackEntity = p.wbGetEntity(fbStmt.mainsnak.datavalue.value.id)
545         end
546     else
547         table.insert(self.categories, 'Missing data item')
548     end
549 
550     local instance_of = entity and getItemValue(self, p.INSTANCE_OF)
551     local types = instance_types[instance_of or typeGuess]
552     if types then
553         if not args.templatename then
554             args.templatename = types.templatename
555         end
556         if not args.type then
557             args.type = types.type
558         end
559         if instance_of == 'Q6' then
560             -- Relations are tricky - ther remap "temp" to "rtemp" and "value"
561             if not args.rtype then
562                 args.rtype = getItemValue(self, p.REL_ID)
563             end
564             if not args.value then
565                 args.value = args.rtype
566             end
567         end
568     end
569     -- Template:Description needs these to properly format language bar and template links
570     --   templatename = Template:ValueDescription | ...
571     --   type = key|value|relation
572     if not args.templatename then
573         local frame2 = mw.getCurrentFrame():getParent()
574         args.templatename = frame2 and frame2:getTitle() or 'Unknown'
575     end
576     if not args.type then
577         if args.templatename and string.find(args.templatename, 'KeyDescription', 1, true) then
578             args.type = 'key'
579         elseif args.templatename and string.find(args.templatename, 'ValueDescription', 1, true) then
580             args.type = 'value'
581         elseif args.templatename and string.find(args.templatename, 'RelationDescription', 1, true) then
582             args.type = 'relation'
583         end
584     end
585     if args.onClosedWay then
586         table.insert(self.categories, 'Obsolete description template parameters')
587     end
588 
589     return self
590 end
591 
592 -- ##########################################################################
593 --                                 ENTRY POINTS
594 -- ##########################################################################
595 
596 -- If we found data item for this key/tag, compare template parameters
597 -- with what we have in the item, and add some extra formatting/links/...
598 -- If the values are not provided, just use the ones from the data item
599 function p.main(frame)
600 	if not mw.wikibase then
601 		return frame:expandTemplate { title = 'Warning', args = { text = "The OSM wiki is experiencing technical difficulties. Infoboxes will be restored soon." } }
602 	end
603 	
604     local args = getArgs(frame)
605 
606     -- initialize self - parse title, language, and get relevant entities
607     local self = constructor(args)
608 
609     if self.entity then
610         processEntity(self)
611     end
612 
613     if self.entity then
614         -- If this module is included from [[Template:Deprecated]],
615         -- omit to check if description is set or not
616         if args.status ~= 'deprecated' and args.status ~= 'obsolete' then
617             -- If this is an English item, check if description is set for
618             -- the tracked languages, and if not, add a tracking category
619             if self.langCode == 'en' then
620                 for _, lng in ipairs(p.trackedLanguages) do
621                     if not self.entity:getDescription(lng) then
622                         table.insert(self.categories,
623                                 'Item with no description in language ' .. mw.ustring.upper(lng))
624                     end
625                 end
626             elseif not self.entity:getDescription('en') then
627                 table.insert(self.categories, 'Item with no description in language EN')
628             end
629         end
630     end
631 
632     -- Create a group category. Use language-prefixed name if category page exists
633     if self.args.group then
634         local group = self.language:ucfirst(self.args.group)
635         local prefix = titleParser.langPrefix(self.langCode)
636         local title = mw.title.new('Category:' .. prefix .. group)
637         if title and title.exists then
638             table.insert(self.categories, title.text)
639         else
640             table.insert(self.categories, group)
641         end
642     end
643 
644     local categories = getCategories(self)
645 
646     local baseTemplate = args.basetemplate or 'Template:Description'
647     if args.debuglua then
648         -- debug and unit test support
649         return {
650             template = baseTemplate,
651             args = args,
652             categories = categories,
653         }
654     end
655 
656     for _, arg in pairs({ 'combination', 'implies', 'seeAlso', 'requires', 'incompatibleWith' }) do
657         if type(args[arg]) == 'table' then
658             local result = ''
659             for _, val in pairs(args[arg]) do
660                 result = result .. '* ' .. frame:expandTemplate { title = 'Tag', args = val } .. '\n'
661             end
662             if result ~= '' then
663                 args[arg] = result
664             else
665                 args[arg] = nil
666             end
667         end
668     end
669 
670     if args.description then
671         args.description = formatDescription(args.description, frame)
672     end
673 
674     local result = frame:expandTemplate { title = baseTemplate, args = args }
675     if categories then
676         result = result .. categories
677     end
678     if args.debugargs then
679         result = result ..
680                 '<br><pre>' ..
681                 mw.text.nowiki(mw.text.jsonEncode(args, mw.text.JSON_PRETTY)) ..
682                 '</pre><br>'
683     end
684     return result
685 end
686 
687 
688 -- Create a table row to describe a specific value
689 -- Usually rendered as key | value | element | comment | rendering | photo
690 function p.row(frame)
691     local args = getArgs(frame)
692 
693     if args[1] and not args.key then
694         args.key = args[1]
695     end
696     if args[2] and not args.value then
697         args.value = args[2]
698     end
699 
700     -- Unlike sidecard, table could be used in different types of pages, and should not rely on auto-guessing
701     assert(args.key, 'Missing key=... parameter')
702     assert(args.value, 'Missing value=... parameter')
703 
704     -- initialize self - parse title, language, and get relevant entities
705     local self = constructor(args)
706 
707     if self.entity then
708         local qid = self.entity:getId()
709 
710         validateKeyValue(self)
711 
712         -- Compare all known parameters against the data item entity
713         processValue(self, 'description',
714                 self.entity:getDescription(self.langCode) or self.entity:getDescription(),
715                 args.description, qid) -- add edit links to description
716 
717         value, usedFb, ref = p.getClaimValue(p.IMAGE, self.langCode, self.entity, self.fallbackEntity)
718         processValue(self, 'photo', value and 'File:' .. value or nil)
719 
720         local render = getItemValue(self, p.RENDER_IMAGE)
721         processValue(self, 'osmcarto-rendering', render and 'File:' .. render or nil)
722 
723         -- Handle onRelation, onArray, onWay, and onNode
724         processValue(self, 'onNode', getItemValue(self, p.USE_ON_NODES), normalizeBoolean(args.onNode))
725         processValue(self, 'onWay', getItemValue(self, p.USE_ON_WAYS), normalizeBoolean(args.onWay))
726         processValue(self, 'onArea', getItemValue(self, p.USE_ON_AREAS), normalizeBoolean(args.onArea))
727         processValue(self, 'onRelation', getItemValue(self, p.USE_ON_RELATIONS), normalizeBoolean(args.onRelation))
728     end
729 
730     local elems = {}
731     if 'yes' == args.onNode then
732         table.insert(elems, 'iconNode')
733     end
734     if 'yes' == args.onWay then
735         table.insert(elems, 'iconWay')
736     end
737     if 'yes' == args.onArea then
738         table.insert(elems, 'iconArea')
739     end
740     if 'yes' == args.onRelation then
741         table.insert(elems, 'iconRelation')
742     end
743 
744     args.render = args.render and '[[' .. args.render .. '|100px]]'
745     args.photo = args.photo and '[[' .. args.photo .. '|100px]]'
746 
747     if args.description2 then
748         args.description = args.description .. '<br>' .. args.description2
749     end
750 
751     -- key | value | element | comment | render | photo
752     local resultTbl = {
753         args.key,
754         args.value,
755         elems,
756         args.description or '',
757         args.render or '',
758         args.photo or '',
759     }
760     local categories = getCategories(self)
761     if args.debuglua then
762         -- debug and unit test support
763         return { args = args, categories = categories, row = resultTbl }
764     end
765 
766     -- expand templates
767     local lang = self.langCode
768     for i, v in ipairs(elems) do
769         elems[i] = frame:expandTemplate { title = v }
770     end
771     local result = '|-\n| ' .. table.concat({
772         frame:expandTemplate { title = 'TagKey/exists', args = { resultTbl[1], lang = lang } },
773         frame:expandTemplate { title = 'TagValue/exists', args = { resultTbl[1], resultTbl[2], lang = lang } },
774         table.concat(elems, ''),
775         resultTbl[4],
776         resultTbl[5],
777         resultTbl[6],
778     }, '\n| ')
779     if categories then
780         result = result .. categories
781     end
782     if args.debugargs then
783         result = result ..
784                 '<br><pre>' ..
785                 mw.text.nowiki(mw.text.jsonEncode(resultTbl, mw.text.JSON_PRETTY)) ..
786                 '</pre><br><pre>' ..
787                 mw.text.nowiki(result) ..
788                 '</pre><br>'
789     end
790     return result
791 end
792 
793 -- ##########################################################################
794 --                        DEBUGGING AND TESTING SUPPORT
795 -- ##########################################################################
796 -- From the debug console, use   =p.dbg{title='Key:bridge'}
797 function p.dbg(args)
798     args.currentTitle = args.title or 'Key:bridge:movable'
799     args.debuglua = args.debuglua == nil and true or args.debuglua
800     local frame = mw.getCurrentFrame():newChild { title = 'Module:DescriptionFromDataItem', args = args }
801     return mw.text.jsonEncode(p.main(frame), mw.text.JSON_PRETTY)
802 end
803 
804 function p.dbgFmtDesc(desc)
805     return formatDescription(desc, mw.getCurrentFrame():newChild { title = 'Module:DescriptionFromDataItem' })
806 end
807 
808 -- From the debug console, use   =p.dbgrow{key='bridge', value='movable'}
809 function p.dbgrow(args)
810     local frame = mw.getCurrentFrame():newChild { title = 'Module:DescriptionFromDataItem', args = args }
811     return p.row(frame)
812 end
813 
814 -- Debug helper for statements
815 -- =p.stmt(p.GROUP, 'Q501', 'en')
816 function p.stmt(prop, id, lang)
817     return mw.text.jsonEncode(
818             { p.getClaimValue(prop, lang, mw.wikibase.getEntity(id)) },
819             mw.text.JSON_PRETTY)
820 end
821 
822 -- These methods could be overwritten by unit tests
823 function p.wbGetEntity(entity)
824     return mw.wikibase.getEntity(entity)
825 end
826 
827 function p.wbGetEntityIdForTitle(title)
828     return mw.wikibase.getEntityIdForTitle(title)
829 end
830 
831 function p.wbGetLabel(qid)
832     return mw.wikibase.getLabel(qid)
833 end
834 
835 function p.wbGetLabelByLang(qid, langCode)
836     return mw.wikibase.getLabelByLang(qid, langCode)
837 end
838 
839 return p