Module:Navbox
Jump to navigation
Jump to search
Documentation for this module may be created at Module:Navbox/doc
1 local p = {}
2 local navbar = require('Module:Navbar')._navbar
3 local cfg = mw.loadData('Module:Navbox/configuration')
4 local getArgs -- lazily initialized
5 local args
6 local format = string.format
7
8 local function striped(wikitext, border)
9 -- Return wikitext with markers replaced for odd/even striping.
10 -- Child (subgroup) navboxes are flagged with a category that is removed
11 -- by parent navboxes. The result is that the category shows all pages
12 -- where a child navbox is not contained in a parent navbox.
13 local orphanCat = cfg.category.orphan
14 if border == cfg.keyword.border_subgroup and args[cfg.arg.orphan] ~= cfg.keyword.orphan_yes then
15 -- No change; striping occurs in outermost navbox.
16 return wikitext .. orphanCat
17 end
18 local first, second = cfg.class.navbox_odd_part, cfg.class.navbox_even_part
19 if args[cfg.arg.evenodd] then
20 if args[cfg.arg.evenodd] == cfg.keyword.evenodd_swap then
21 first, second = second, first
22 else
23 first = args[cfg.arg.evenodd]
24 second = first
25 end
26 end
27 local changer
28 if first == second then
29 changer = first
30 else
31 local index = 0
32 changer = function (code)
33 if code == '0' then
34 -- Current occurrence is for a group before a nested table.
35 -- Set it to first as a valid although pointless class.
36 -- The next occurrence will be the first row after a title
37 -- in a subgroup and will also be first.
38 index = 0
39 return first
40 end
41 index = index + 1
42 return index % 2 == 1 and first or second
43 end
44 end
45 local regex = orphanCat:gsub('([%[%]])', '%%%1')
46 return (wikitext:gsub(regex, ''):gsub(cfg.marker.regex, changer)) -- () omits gsub count
47 end
48
49 local function processItem(item, nowrapitems)
50 if item:sub(1, 2) == '{|' then
51 -- Applying nowrap to lines in a table does not make sense.
52 -- Add newlines to compensate for trim of x in |parm=x in a template.
53 return '\n' .. item ..'\n'
54 end
55 if nowrapitems == cfg.keyword.nowrapitems_yes then
56 local lines = {}
57 for line in (item .. '\n'):gmatch('([^\n]*)\n') do
58 local prefix, content = line:match('^([*:;#]+)%s*(.*)')
59 if prefix and not content:match(cfg.pattern.nowrap) then
60 line = format(cfg.nowrap_item, prefix, content)
61 end
62 table.insert(lines, line)
63 end
64 item = table.concat(lines, '\n')
65 end
66 if item:match('^[*:;#]') then
67 return '\n' .. item ..'\n'
68 end
69 return item
70 end
71
72 local function has_navbar()
73 return args[cfg.arg.navbar] ~= cfg.keyword.navbar_off
74 and args[cfg.arg.navbar] ~= cfg.keyword.navbar_plain
75 and (
76 args[cfg.arg.name]
77 or mw.getCurrentFrame():getParent():getTitle():gsub(cfg.pattern.sandbox, '')
78 ~= cfg.pattern.navbox
79 )
80 end
81
82 local function renderNavBar(titleCell)
83 if has_navbar() then
84 titleCell:wikitext(navbar{
85 [cfg.navbar.name] = args[cfg.arg.name],
86 [cfg.navbar.mini] = 1,
87 [cfg.navbar.fontstyle] = (args[cfg.arg.basestyle] or '') .. ';' ..
88 (args[cfg.arg.titlestyle] or '') ..
89 ';background:none transparent;border:none;box-shadow:none;padding:0;'
90 })
91 end
92
93 end
94
95 local function renderTitleRow(tbl)
96 if not args[cfg.arg.title] then return end
97
98 local titleRow = tbl:tag('tr')
99
100 local titleCell = titleRow:tag('th'):attr('scope', 'col')
101
102 local titleColspan = 2
103 if args[cfg.arg.imageleft] then titleColspan = titleColspan + 1 end
104 if args[cfg.arg.image] then titleColspan = titleColspan + 1 end
105
106 titleCell
107 :cssText(args[cfg.arg.basestyle])
108 :cssText(args[cfg.arg.titlestyle])
109 :addClass(cfg.class.navbox_title)
110 :attr('colspan', titleColspan)
111
112 renderNavBar(titleCell)
113
114 titleCell
115 :tag('div')
116 -- id for aria-labelledby attribute
117 :attr('id', mw.uri.anchorEncode(args[cfg.arg.title]))
118 :addClass(args[cfg.arg.titleclass])
119 :css('font-size', '114%')
120 :css('margin', '0 4em')
121 :wikitext(processItem(args[cfg.arg.title]))
122 end
123
124 local function getAboveBelowColspan()
125 local ret = 2
126 if args[cfg.arg.imageleft] then ret = ret + 1 end
127 if args[cfg.arg.image] then ret = ret + 1 end
128 return ret
129 end
130
131 local function renderAboveRow(tbl)
132 if not args[cfg.arg.above] then return end
133
134 tbl:tag('tr')
135 :tag('td')
136 :addClass(cfg.class.navbox_abovebelow)
137 :addClass(args[cfg.arg.aboveclass])
138 :cssText(args[cfg.arg.basestyle])
139 :cssText(args[cfg.arg.abovestyle])
140 :attr('colspan', getAboveBelowColspan())
141 :tag('div')
142 -- id for aria-labelledby attribute, if no title
143 :attr('id', args[cfg.arg.title] and nil or mw.uri.anchorEncode(args[cfg.arg.above]))
144 :wikitext(processItem(args[cfg.arg.above], args[cfg.arg.nowrapitems]))
145 end
146
147 local function renderBelowRow(tbl)
148 if not args[cfg.arg.below] then return end
149
150 tbl:tag('tr')
151 :tag('td')
152 :addClass(cfg.class.navbox_abovebelow)
153 :addClass(args[cfg.arg.belowclass])
154 :cssText(args[cfg.arg.basestyle])
155 :cssText(args[cfg.arg.belowstyle])
156 :attr('colspan', getAboveBelowColspan())
157 :tag('div')
158 :wikitext(processItem(args[cfg.arg.below], args[cfg.arg.nowrapitems]))
159 end
160
161 local function renderListRow(tbl, index, listnum, listnums_size)
162 local row = tbl:tag('tr')
163
164 if index == 1 and args[cfg.arg.imageleft] then
165 row
166 :tag('td')
167 :addClass(cfg.class.noviewer)
168 :addClass(cfg.class.navbox_image)
169 :addClass(args[cfg.arg.imageclass])
170 :css('width', '1px') -- Minimize width
171 :css('padding', '0 2px 0 0')
172 :cssText(args[cfg.arg.imageleftstyle])
173 :attr('rowspan', listnums_size)
174 :tag('div')
175 :wikitext(processItem(args[cfg.arg.imageleft]))
176 end
177
178 local group_and_num = format(cfg.arg.group_and_num, listnum)
179 local groupstyle_and_num = format(cfg.arg.groupstyle_and_num, listnum)
180 if args[group_and_num] then
181 local groupCell = row:tag('th')
182
183 -- id for aria-labelledby attribute, if lone group with no title or above
184 if listnum == 1 and not (args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group2]) then
185 groupCell
186 :attr('id', mw.uri.anchorEncode(args[cfg.arg.group1]))
187 end
188
189 groupCell
190 :attr('scope', 'row')
191 :addClass(cfg.class.navbox_group)
192 :addClass(args[cfg.arg.groupclass])
193 :cssText(args[cfg.arg.basestyle])
194 -- If groupwidth not specified, minimize width
195 :css('width', args[cfg.arg.groupwidth] or '1%')
196
197 groupCell
198 :cssText(args[cfg.arg.groupstyle])
199 :cssText(args[groupstyle_and_num])
200 :wikitext(args[group_and_num])
201 end
202
203 local listCell = row:tag('td')
204
205 if args[group_and_num] then
206 listCell
207 :addClass(cfg.class.navbox_list_with_group)
208 else
209 listCell:attr('colspan', 2)
210 end
211
212 if not args[cfg.arg.groupwidth] then
213 listCell:css('width', '100%')
214 end
215
216 local rowstyle -- usually nil so cssText(rowstyle) usually adds nothing
217 if index % 2 == 1 then
218 rowstyle = args[cfg.arg.oddstyle]
219 else
220 rowstyle = args[cfg.arg.evenstyle]
221 end
222
223 local list_and_num = format(cfg.arg.list_and_num, listnum)
224 local listText = args[list_and_num]
225 local oddEven = cfg.marker.oddeven
226 if listText:sub(1, 12) == '</div><table' then
227 -- Assume list text is for a subgroup navbox so no automatic striping for this row.
228 oddEven = listText:find(cfg.pattern.navbox_title) and cfg.marker.restart or cfg.class.navbox_odd_part
229 end
230
231 local liststyle_and_num = format(cfg.arg.liststyle_and_num, listnum)
232 local listclass_and_num = format(cfg.arg.listclass_and_num, listnum)
233 listCell
234 :css('padding', '0')
235 :cssText(args[cfg.arg.liststyle])
236 :cssText(rowstyle)
237 :cssText(args[liststyle_and_num])
238 :addClass(cfg.class.navbox_list)
239 :addClass(cfg.class.navbox_part .. oddEven)
240 :addClass(args[cfg.arg.listclass])
241 :addClass(args[listclass_and_num])
242 :tag('div')
243 :css('padding',
244 (index == 1 and args[cfg.arg.list1padding]) or args[cfg.arg.listpadding] or '0 0.25em'
245 )
246 :wikitext(processItem(listText, args[cfg.arg.nowrapitems]))
247
248 if index == 1 and args[cfg.arg.image] then
249 row
250 :tag('td')
251 :addClass(cfg.class.noviewer)
252 :addClass(cfg.class.navbox_image)
253 :addClass(args[cfg.arg.imageclass])
254 :css('width', '1px') -- Minimize width
255 :css('padding', '0 0 0 2px')
256 :cssText(args[cfg.arg.imagestyle])
257 :attr('rowspan', listnums_size)
258 :tag('div')
259 :wikitext(processItem(args[cfg.arg.image]))
260 end
261 end
262
263 local function has_list_class(htmlclass)
264 local patterns = {
265 '^' .. htmlclass .. '$',
266 '%s' .. htmlclass .. '$',
267 '^' .. htmlclass .. '%s',
268 '%s' .. htmlclass .. '%s'
269 }
270
271 for arg, _ in pairs(args) do
272 if type(arg) == 'string' and mw.ustring.find(arg, cfg.pattern.class) then
273 for _, pattern in ipairs(patterns) do
274 if mw.ustring.find(args[arg] or '', pattern) then
275 return true
276 end
277 end
278 end
279 end
280 return false
281 end
282
283 -- there are a lot of list classes in the wild, so we add their TemplateStyles
284 local function add_list_styles()
285 local frame = mw.getCurrentFrame()
286 local function add_list_templatestyles(htmlclass, templatestyles)
287 if has_list_class(htmlclass) then
288 return frame:extensionTag{
289 name = 'templatestyles', args = { src = templatestyles }
290 }
291 else
292 return ''
293 end
294 end
295
296 local hlist_styles = add_list_templatestyles('hlist', cfg.hlist_templatestyles)
297 local plainlist_styles = add_list_templatestyles('plainlist', cfg.plainlist_templatestyles)
298
299 -- a second workaround for [[phab:T303378]]
300 -- when that issue is fixed, we can actually use has_navbar not to emit the
301 -- tag here if we want
302 if has_navbar() and hlist_styles == '' then
303 hlist_styles = frame:extensionTag{
304 name = 'templatestyles', args = { src = cfg.hlist_templatestyles }
305 }
306 end
307
308 -- hlist -> plainlist is best-effort to preserve old Common.css ordering.
309 -- this ordering is not a guarantee because most navboxes will emit only
310 -- one of these classes [hlist_note]
311 return hlist_styles .. plainlist_styles
312 end
313
314 local function needsHorizontalLists(border)
315 if border == cfg.keyword.border_subgroup or args[cfg.arg.tracking] == cfg.keyword.tracking_no then
316 return false
317 end
318 return not has_list_class(cfg.pattern.hlist) and not has_list_class(cfg.pattern.plainlist)
319 end
320
321 local function hasBackgroundColors()
322 for _, key in ipairs({cfg.arg.titlestyle, cfg.arg.groupstyle,
323 cfg.arg.basestyle, cfg.arg.abovestyle, cfg.arg.belowstyle}) do
324 if tostring(args[key]):find('background', 1, true) then
325 return true
326 end
327 end
328 return false
329 end
330
331 local function hasBorders()
332 for _, key in ipairs({cfg.arg.groupstyle, cfg.arg.basestyle,
333 cfg.arg.abovestyle, cfg.arg.belowstyle}) do
334 if tostring(args[key]):find('border', 1, true) then
335 return true
336 end
337 end
338 return false
339 end
340
341 local function isIllegible()
342 local styleratio = require('Module:Color contrast')._styleratio
343 for key, style in pairs(args) do
344 if tostring(key):match(cfg.pattern.style) then
345 if styleratio{mw.text.unstripNoWiki(style)} < 4.5 then
346 return true
347 end
348 end
349 end
350 return false
351 end
352
353 local function getTrackingCategories(border)
354 local cats = {}
355 if needsHorizontalLists(border) then table.insert(cats, cfg.category.horizontal_lists) end
356 if hasBackgroundColors() then table.insert(cats, cfg.category.background_colors) end
357 if isIllegible() then table.insert(cats, cfg.category.illegible) end
358 if hasBorders() then table.insert(cats, cfg.category.borders) end
359 return cats
360 end
361
362 local function renderTrackingCategories(builder, border)
363 local title = mw.title.getCurrentTitle()
364 if title.namespace ~= 10 then return end -- not in template space
365 local subpage = title.subpageText
366 if subpage == cfg.keyword.subpage_doc or subpage == cfg.keyword.subpage_sandbox
367 or subpage == cfg.keyword.subpage_testcases then return end
368
369 for _, cat in ipairs(getTrackingCategories(border)) do
370 builder:wikitext('[[Category:' .. cat .. ']]')
371 end
372 end
373
374 local function renderMainTable(border, listnums)
375 local tbl = mw.html.create('table')
376 :addClass(cfg.class.nowraplinks)
377 :addClass(args[cfg.arg.bodyclass])
378
379 local state = args[cfg.arg.state]
380 if args[cfg.arg.title] and state ~= cfg.keyword.state_plain and state ~= cfg.keyword.state_off then
381 if state == cfg.keyword.state_collapsed then
382 state = cfg.class.collapsed
383 end
384 tbl
385 :addClass(cfg.class.collapsible)
386 :addClass(state or cfg.class.autocollapse)
387 end
388
389 tbl:css('border-spacing', 0)
390 if border == cfg.keyword.border_subgroup or border == cfg.keyword.border_none then
391 tbl
392 :addClass(cfg.class.navbox_subgroup)
393 :cssText(args[cfg.arg.bodystyle])
394 :cssText(args[cfg.arg.style])
395 else -- regular navbox - bodystyle and style will be applied to the wrapper table
396 tbl
397 :addClass(cfg.class.navbox_inner)
398 :css('background', 'transparent')
399 :css('color', 'inherit')
400 end
401 tbl:cssText(args[cfg.arg.innerstyle])
402
403 renderTitleRow(tbl)
404 renderAboveRow(tbl)
405 local listnums_size = #listnums
406 for i, listnum in ipairs(listnums) do
407 renderListRow(tbl, i, listnum, listnums_size)
408 end
409 renderBelowRow(tbl)
410
411 return tbl
412 end
413
414 local function add_navbox_styles(hiding_templatestyles)
415 local frame = mw.getCurrentFrame()
416 -- This is a lambda so that it doesn't need the frame as a parameter
417 local function add_user_styles(templatestyles)
418 if templatestyles and templatestyles ~= '' then
419 return frame:extensionTag{
420 name = 'templatestyles', args = { src = templatestyles }
421 }
422 end
423 return ''
424 end
425
426 -- get templatestyles. load base from config so that Lua only needs to do
427 -- the work once of parser tag expansion
428 local base_templatestyles = cfg.templatestyles
429 local templatestyles = add_user_styles(args[cfg.arg.templatestyles])
430 local child_templatestyles = add_user_styles(args[cfg.arg.child_templatestyles])
431
432 -- The 'navbox-styles' div exists to wrap the styles to work around T200206
433 -- more elegantly. Instead of combinatorial rules, this ends up being linear
434 -- number of CSS rules.
435 return mw.html.create('div')
436 :addClass(cfg.class.navbox_styles)
437 :wikitext(
438 add_list_styles() .. -- see [hlist_note] applied to 'before base_templatestyles'
439 base_templatestyles ..
440 templatestyles ..
441 child_templatestyles ..
442 table.concat(hiding_templatestyles)
443 )
444 :done()
445 end
446
447 -- work around [[phab:T303378]]
448 -- for each arg: find all the templatestyles strip markers, insert them into a
449 -- table. then remove all templatestyles markers from the arg
450 local function move_hiding_templatestyles(args)
451 local gfind = string.gfind
452 local gsub = string.gsub
453 local templatestyles_markers = {}
454 local strip_marker_pattern = '(\127[^\127]*UNIQ%-%-templatestyles%-%x+%-QINU[^\127]*\127)'
455 for k, arg in pairs(args) do
456 for marker in gfind(arg, strip_marker_pattern) do
457 table.insert(templatestyles_markers, marker)
458 end
459 args[k] = gsub(arg, strip_marker_pattern, '')
460 end
461 return templatestyles_markers
462 end
463
464 function p._navbox(navboxArgs)
465 args = navboxArgs
466 local hiding_templatestyles = move_hiding_templatestyles(args)
467 local listnums = {}
468
469 for k, _ in pairs(args) do
470 if type(k) == 'string' then
471 local listnum = k:match(cfg.pattern.listnum)
472 if listnum then table.insert(listnums, tonumber(listnum)) end
473 end
474 end
475 table.sort(listnums)
476
477 local border = mw.text.trim(args[cfg.arg.border] or args[1] or '')
478 if border == cfg.keyword.border_child then
479 border = cfg.keyword.border_subgroup
480 end
481
482 -- render the main body of the navbox
483 local tbl = renderMainTable(border, listnums)
484
485 local res = mw.html.create()
486 -- render the appropriate wrapper for the navbox, based on the border param
487
488 if border == cfg.keyword.border_none then
489 res:node(add_navbox_styles(hiding_templatestyles))
490 local nav = res:tag('div')
491 :attr('role', 'navigation')
492 :node(tbl)
493 -- aria-labelledby title, otherwise above, otherwise lone group
494 if args[cfg.arg.title] or args[cfg.arg.above] or (args[cfg.arg.group1]
495 and not args[cfg.arg.group2]) then
496 nav:attr(
497 'aria-labelledby',
498 mw.uri.anchorEncode(
499 args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1]
500 )
501 )
502 else
503 nav:attr('aria-label', cfg.aria_label)
504 end
505 elseif border == cfg.keyword.border_subgroup then
506 -- We assume that this navbox is being rendered in a list cell of a
507 -- parent navbox, and is therefore inside a div with padding:0em 0.25em.
508 -- We start with a </div> to avoid the padding being applied, and at the
509 -- end add a <div> to balance out the parent's </div>
510 res
511 :wikitext('</div>')
512 :node(tbl)
513 :wikitext('<div>')
514 else
515 res:node(add_navbox_styles(hiding_templatestyles))
516 local nav = res:tag('div')
517 :attr('role', 'navigation')
518 :addClass(cfg.class.navbox)
519 :addClass(args[cfg.arg.navboxclass])
520 :cssText(args[cfg.arg.bodystyle])
521 :cssText(args[cfg.arg.style])
522 :css('padding', '3px')
523 :node(tbl)
524 -- aria-labelledby title, otherwise above, otherwise lone group
525 if args[cfg.arg.title] or args[cfg.arg.above]
526 or (args[cfg.arg.group1] and not args[cfg.arg.group2]) then
527 nav:attr(
528 'aria-labelledby',
529 mw.uri.anchorEncode(args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1])
530 )
531 else
532 nav:attr('aria-label', cfg.aria_label)
533 end
534 end
535
536 if (args[cfg.arg.nocat] or cfg.keyword.nocat_false):lower() == cfg.keyword.nocat_false then
537 renderTrackingCategories(res, border)
538 end
539 return striped(tostring(res), border)
540 end
541
542 function p.navbox(frame)
543 if not getArgs then
544 getArgs = require('Module:Arguments').getArgs
545 end
546 args = getArgs(frame, {wrappers = {cfg.pattern.navbox}})
547
548 -- Read the arguments in the order they'll be output in, to make references
549 -- number in the right order.
550 local _
551 _ = args[cfg.arg.title]
552 _ = args[cfg.arg.above]
553 -- Limit this to 20 as covering 'most' cases (that's a SWAG) and because
554 -- iterator approach won't work here
555 for i = 1, 20 do
556 _ = args[format(cfg.arg.group_and_num, i)]
557 _ = args[format(cfg.arg.list_and_num, i)]
558 end
559 _ = args[cfg.arg.below]
560
561 return p._navbox(args)
562 end
563
564 return p