Module:DescriptionFromDataItem
Jump to navigation
Jump to search
This documentation is transcluded from Module:DescriptionFromDataItem/doc. (Edit | history)
Note to editors: Please don't categorize this template by editing it directly. Instead, place the category in its documentation page, in its "includeonly" section.
Note to editors: Please don't categorize this template by editing it directly. Instead, place the category in its documentation page, in its "includeonly" section.
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 |
All tests passed.
Name | Expected | Actual | |
---|---|---|---|
![]() |
test_english | ||
![]() |
test_french | ||
![]() |
test_german | ||
![]() |
test_no_dataitem | ||
![]() |
test_no_dataitem_key | ||
![]() |
test_no_dataitem_value | ||
![]() |
test_polish | ||
![]() |
test_polish_group | ||
![]() |
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 (' <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