モジュール:Navbox
このモジュールについての説明文ページを モジュール:Navbox/doc に作成できます
1 --
2 -- This module implements {{Navbox}}
3 --
4
5 local p = {}
6
7 local navbar = require('Module:Navbar')._navbar
8 local getArgs -- lazily initialized
9
10 local args
11 local border
12 local listnums
13 local ODD_EVEN_MARKER = '\127_ODDEVEN_\127'
14 local RESTART_MARKER = '\127_ODDEVEN0_\127'
15 local REGEX_MARKER = '\127_ODDEVEN(%d?)_\127'
16
17 local function striped(wikitext)
18 -- Return wikitext with markers replaced for odd/even striping.
19 -- Child (subgroup) navboxes are flagged with a category that is removed
20 -- by parent navboxes. The result is that the category shows all pages
21 -- where a child navbox is not contained in a parent navbox.
22 local orphanCat = '[[Category:Navbox orphans]]'
23 if border == 'subgroup' and args.orphan ~= 'yes' then
24 -- No change; striping occurs in outermost navbox.
25 return wikitext .. orphanCat
26 end
27 local first, second = 'odd', 'even'
28 if args.evenodd then
29 if args.evenodd == 'swap' then
30 first, second = second, first
31 else
32 first = args.evenodd
33 second = first
34 end
35 end
36 local changer
37 if first == second then
38 changer = first
39 else
40 local index = 0
41 changer = function (code)
42 if code == '0' then
43 -- Current occurrence is for a group before a nested table.
44 -- Set it to first as a valid although pointless class.
45 -- The next occurrence will be the first row after a title
46 -- in a subgroup and will also be first.
47 index = 0
48 return first
49 end
50 index = index + 1
51 return index % 2 == 1 and first or second
52 end
53 end
54 local regex = orphanCat:gsub('([%[%]])', '%%%1')
55 return (wikitext:gsub(regex, ''):gsub(REGEX_MARKER, changer)) -- () omits gsub count
56 end
57
58 local function processItem(item, nowrapitems)
59 if item:sub(1, 2) == '{|' then
60 -- Applying nowrap to lines in a table does not make sense.
61 -- Add newlines to compensate for trim of x in |parm=x in a template.
62 return '\n' .. item ..'\n'
63 end
64 if nowrapitems == 'yes' then
65 local lines = {}
66 for line in (item .. '\n'):gmatch('([^\n]*)\n') do
67 local prefix, content = line:match('^([*:;#]+)%s*(.*)')
68 if prefix and not content:match('^<span class="nowrap">') then
69 line = prefix .. '<span class="nowrap">' .. content .. '</span>'
70 end
71 table.insert(lines, line)
72 end
73 item = table.concat(lines, '\n')
74 end
75 if item:match('^[*:;#]') then
76 return '\n' .. item ..'\n'
77 end
78 return item
79 end
80
81 -- Separate function so that we can evaluate properly whether hlist should
82 -- be added by the module
83 local function has_navbar()
84 return args.navbar ~= 'off' and args.navbar ~= 'plain' and not
85 (not args.name and mw.getCurrentFrame():getParent():getTitle():gsub('/sandbox$', '') == 'Template:Navbox')
86 end
87
88 local function renderNavBar(titleCell)
89
90 if has_navbar() then
91 titleCell:wikitext(navbar{
92 args.name,
93 -- we depend on this being mini = 1 when the navbox module decides
94 -- to add hlist templatestyles. we also depend on navbar outputting
95 -- a copy of the hlist templatestyles.
96 mini = 1,
97 fontstyle = (args.basestyle or '') .. ';' .. (args.titlestyle or '') .. ';background:none transparent;border:none;box-shadow:none; padding:0;'
98 })
99 end
100
101 end
102
103 --
104 -- Title row
105 --
106 local function renderTitleRow(tbl)
107 if not args.title then return end
108
109 local titleRow = tbl:tag('tr')
110
111 if args.titlegroup then
112 titleRow
113 :tag('th')
114 :attr('scope', 'row')
115 :addClass('navbox-group')
116 :addClass(args.titlegroupclass)
117 :cssText(args.basestyle)
118 :cssText(args.groupstyle)
119 :cssText(args.titlegroupstyle)
120 :wikitext(args.titlegroup)
121 end
122
123 local titleCell = titleRow:tag('th'):attr('scope', 'col')
124
125 if args.titlegroup then
126 titleCell
127 :addClass('navbox-title1')
128 end
129
130 local titleColspan = 2
131 if args.imageleft then titleColspan = titleColspan + 1 end
132 if args.image then titleColspan = titleColspan + 1 end
133 if args.titlegroup then titleColspan = titleColspan - 1 end
134
135 titleCell
136 :cssText(args.basestyle)
137 :cssText(args.titlestyle)
138 :addClass('navbox-title')
139 :attr('colspan', titleColspan)
140
141 renderNavBar(titleCell)
142
143 titleCell
144 :tag('div')
145 -- id for aria-labelledby attribute
146 :attr('id', mw.uri.anchorEncode(args.title))
147 :addClass(args.titleclass)
148 :css('font-size', '114%')
149 :css('margin', '0 4em')
150 :wikitext(processItem(args.title))
151 end
152
153 --
154 -- Above/Below rows
155 --
156
157 local function getAboveBelowColspan()
158 local ret = 2
159 if args.imageleft then ret = ret + 1 end
160 if args.image then ret = ret + 1 end
161 return ret
162 end
163
164 local function renderAboveRow(tbl)
165 if not args.above then return end
166
167 tbl:tag('tr')
168 :tag('td')
169 :addClass('navbox-abovebelow')
170 :addClass(args.aboveclass)
171 :cssText(args.basestyle)
172 :cssText(args.abovestyle)
173 :attr('colspan', getAboveBelowColspan())
174 :tag('div')
175 -- id for aria-labelledby attribute, if no title
176 :attr('id', args.title and nil or mw.uri.anchorEncode(args.above))
177 :wikitext(processItem(args.above, args.nowrapitems))
178 end
179
180 local function renderBelowRow(tbl)
181 if not args.below then return end
182
183 tbl:tag('tr')
184 :tag('td')
185 :addClass('navbox-abovebelow')
186 :addClass(args.belowclass)
187 :cssText(args.basestyle)
188 :cssText(args.belowstyle)
189 :attr('colspan', getAboveBelowColspan())
190 :tag('div')
191 :wikitext(processItem(args.below, args.nowrapitems))
192 end
193
194 --
195 -- List rows
196 --
197 local function renderListRow(tbl, index, listnum)
198 local row = tbl:tag('tr')
199
200 if index == 1 and args.imageleft then
201 row
202 :tag('td')
203 :addClass('navbox-image')
204 :addClass(args.imageclass)
205 :css('width', '1px') -- Minimize width
206 :css('padding', '0px 2px 0px 0px')
207 :cssText(args.imageleftstyle)
208 :attr('rowspan', #listnums)
209 :tag('div')
210 :wikitext(processItem(args.imageleft))
211 end
212
213 if args['group' .. listnum] then
214 local groupCell = row:tag('th')
215
216 -- id for aria-labelledby attribute, if lone group with no title or above
217 if listnum == 1 and not (args.title or args.above or args.group2) then
218 groupCell
219 :attr('id', mw.uri.anchorEncode(args.group1))
220 end
221
222 groupCell
223 :attr('scope', 'row')
224 :addClass('navbox-group')
225 :addClass(args.groupclass)
226 :cssText(args.basestyle)
227 :css('width', args.groupwidth or '1%') -- If groupwidth not specified, minimize width
228
229 groupCell
230 :cssText(args.groupstyle)
231 :cssText(args['group' .. listnum .. 'style'])
232 :wikitext(args['group' .. listnum])
233 end
234
235 local listCell = row:tag('td')
236
237 if args['group' .. listnum] then
238 listCell
239 :addClass('navbox-list1')
240 else
241 listCell:attr('colspan', 2)
242 end
243
244 if not args.groupwidth then
245 listCell:css('width', '100%')
246 end
247
248 local rowstyle -- usually nil so cssText(rowstyle) usually adds nothing
249 if index % 2 == 1 then
250 rowstyle = args.oddstyle
251 else
252 rowstyle = args.evenstyle
253 end
254
255 local listText = args['list' .. listnum]
256 local oddEven = ODD_EVEN_MARKER
257 if listText:sub(1, 12) == '</div><table' then
258 -- Assume list text is for a subgroup navbox so no automatic striping for this row.
259 oddEven = listText:find('<th[^>]*"navbox%-title"') and RESTART_MARKER or 'odd'
260 end
261 listCell
262 :css('padding', '0px')
263 :cssText(args.liststyle)
264 :cssText(rowstyle)
265 :cssText(args['list' .. listnum .. 'style'])
266 :addClass('navbox-list')
267 :addClass('navbox-' .. oddEven)
268 :addClass(args.listclass)
269 :addClass(args['list' .. listnum .. 'class'])
270 :tag('div')
271 :css('padding', (index == 1 and args.list1padding) or args.listpadding or '0em 0.25em')
272 :wikitext(processItem(listText, args.nowrapitems))
273
274 if index == 1 and args.image then
275 row
276 :tag('td')
277 :addClass('navbox-image')
278 :addClass(args.imageclass)
279 :css('width', '1px') -- Minimize width
280 :css('padding', '0px 0px 0px 2px')
281 :cssText(args.imagestyle)
282 :attr('rowspan', #listnums)
283 :tag('div')
284 :wikitext(processItem(args.image))
285 end
286 end
287
288
289 --
290 -- Tracking categories
291 --
292
293 local function needsHorizontalLists()
294 if border == 'subgroup' or args.tracking == 'no' then
295 return false
296 end
297 local listClasses = {
298 ['plainlist'] = true, ['hlist'] = true, ['hlist hnum'] = true,
299 ['hlist hwrap'] = true, ['hlist vcard'] = true, ['vcard hlist'] = true,
300 ['hlist vevent'] = true,
301 }
302 return not (listClasses[args.listclass] or listClasses[args.bodyclass])
303 end
304
305 -- there are a lot of list classes in the wild, so we have a function to find
306 -- them and add their TemplateStyles
307 local function addListStyles()
308 local frame = mw.getCurrentFrame()
309 -- TODO?: Should maybe take a table of classes for e.g. hnum, hwrap as above
310 -- I'm going to do the stupid thing first though
311 -- Also not sure hnum and hwrap are going to live in the same TemplateStyles
312 -- as hlist
313 local function _addListStyles(htmlclass, templatestyles)
314 local class_args = { -- rough order of probability of use
315 'bodyclass', 'listclass', 'aboveclass', 'belowclass', 'titleclass',
316 'navboxclass', 'groupclass', 'titlegroupclass', 'imageclass'
317 }
318 local patterns = {
319 '^' .. htmlclass .. '$',
320 '%s' .. htmlclass .. '$',
321 '^' .. htmlclass .. '%s',
322 '%s' .. htmlclass .. '%s'
323 }
324
325 local found = false
326 for _, arg in ipairs(class_args) do
327 for _, pattern in ipairs(patterns) do
328 if mw.ustring.find(args[arg] or '', pattern) then
329 found = true
330 break
331 end
332 end
333 if found then break end
334 end
335 if found then
336 return frame:extensionTag{
337 name = 'templatestyles', args = { src = templatestyles }
338 }
339 else
340 return ''
341 end
342 end
343
344 local hlist_styles = ''
345 -- navbar always has mini = 1, so here (on this wiki) we can assume that
346 -- we don't need to output hlist styles in navbox again.
347 if not has_navbar() then
348 hlist_styles = _addListStyles('hlist', 'Flatlist/styles.css')
349 end
350 local plainlist_styles = _addListStyles('plainlist', 'Plainlist/styles.css')
351
352 return hlist_styles .. plainlist_styles
353 end
354
355 local function hasBackgroundColors()
356 for _, key in ipairs({'titlestyle', 'groupstyle', 'basestyle', 'abovestyle', 'belowstyle'}) do
357 if tostring(args[key]):find('background', 1, true) then
358 return true
359 end
360 end
361 end
362
363 local function hasBorders()
364 for _, key in ipairs({'groupstyle', 'basestyle', 'abovestyle', 'belowstyle'}) do
365 if tostring(args[key]):find('border', 1, true) then
366 return true
367 end
368 end
369 end
370
371 local function isIllegible()
372 -- require('Module:Color contrast') absent on mediawiki.org
373 return false
374 end
375
376 local function getTrackingCategories()
377 local cats = {}
378 if needsHorizontalLists() then table.insert(cats, 'Navigational boxes without horizontal lists') end
379 if hasBackgroundColors() then table.insert(cats, 'Navboxes using background colours') end
380 if isIllegible() then table.insert(cats, 'Potentially illegible navboxes') end
381 if hasBorders() then table.insert(cats, 'Navboxes using borders') end
382 return cats
383 end
384
385 local function renderTrackingCategories(builder)
386 local title = mw.title.getCurrentTitle()
387 if title.namespace ~= 10 then return end -- not in template space
388 local subpage = title.subpageText
389 if subpage == 'doc' or subpage == 'sandbox' or subpage == 'testcases' then return end
390
391 for _, cat in ipairs(getTrackingCategories()) do
392 builder:wikitext('[[Category:' .. cat .. ']]')
393 end
394 end
395
396 --
397 -- Main navbox tables
398 --
399 local function renderMainTable()
400 local tbl = mw.html.create('table')
401 :addClass('nowraplinks')
402 :addClass(args.bodyclass)
403
404 if args.title and (args.state ~= 'plain' and args.state ~= 'off') then
405 if args.state == 'collapsed' then args.state = 'mw-collapsed' end
406 tbl
407 :addClass('mw-collapsible')
408 :addClass(args.state or 'autocollapse')
409 end
410
411 tbl:css('border-spacing', 0)
412 if border == 'subgroup' or border == 'none' then
413 tbl
414 :addClass('navbox-subgroup')
415 :cssText(args.bodystyle)
416 :cssText(args.style)
417 else -- regular navbox - bodystyle and style will be applied to the wrapper table
418 tbl
419 :addClass('navbox-inner')
420 :css('background', 'transparent')
421 :css('color', 'inherit')
422 end
423 tbl:cssText(args.innerstyle)
424
425 renderTitleRow(tbl)
426 renderAboveRow(tbl)
427 for i, listnum in ipairs(listnums) do
428 renderListRow(tbl, i, listnum)
429 end
430 renderBelowRow(tbl)
431
432 return tbl
433 end
434
435 function p._navbox(navboxArgs)
436 args = navboxArgs
437 listnums = {}
438
439 for k, _ in pairs(args) do
440 if type(k) == 'string' then
441 local listnum = k:match('^list(%d+)$')
442 if listnum then table.insert(listnums, tonumber(listnum)) end
443 end
444 end
445 table.sort(listnums)
446
447 border = mw.text.trim(args.border or args[1] or '')
448 if border == 'child' then
449 border = 'subgroup'
450 end
451
452 -- render the main body of the navbox
453 local tbl = renderMainTable()
454
455 -- get templatestyles
456 local frame = mw.getCurrentFrame()
457 local base_templatestyles = frame:extensionTag{
458 name = 'templatestyles', args = { src = 'Module:Navbox/styles.css' }
459 }
460 local templatestyles = ''
461 if args.templatestyles and args.templatestyles ~= '' then
462 templatestyles = frame:extensionTag{
463 name = 'templatestyles', args = { src = args.templatestyles }
464 }
465 end
466
467 local res = mw.html.create()
468 -- 'navbox-styles' exists for two reasons:
469 -- 1. To wrap the styles to work around phab: T200206 more elegantly. Instead
470 -- of combinatorial rules, this ends up being linear number of CSS rules.
471 -- 2. To allow MobileFrontend to rip the styles out with 'nomobile' such that
472 -- they are not dumped into the mobile view.
473 res:tag('div')
474 :addClass('navbox-styles')
475 :addClass('nomobile')
476 :wikitext(base_templatestyles .. templatestyles)
477 :done()
478
479 -- render the appropriate wrapper around the navbox, depending on the border param
480 if border == 'none' then
481 local nav = res:tag('div')
482 :attr('role', 'navigation')
483 :wikitext(addListStyles())
484 :node(tbl)
485 -- aria-labelledby title, otherwise above, otherwise lone group
486 if args.title or args.above or (args.group1 and not args.group2) then
487 nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
488 else
489 nav:attr('aria-label', 'Navbox')
490 end
491 elseif border == 'subgroup' then
492 -- We assume that this navbox is being rendered in a list cell of a
493 -- parent navbox, and is therefore inside a div with padding:0em 0.25em.
494 -- We start with a </div> to avoid the padding being applied, and at the
495 -- end add a <div> to balance out the parent's </div>
496 res
497 :wikitext('</div>')
498 :wikitext(addListStyles())
499 :node(tbl)
500 :wikitext('<div>')
501 else
502 local nav = res:tag('div')
503 :attr('role', 'navigation')
504 :addClass('navbox')
505 :addClass(args.navboxclass)
506 :cssText(args.bodystyle)
507 :cssText(args.style)
508 :css('padding', '3px')
509 :wikitext(addListStyles())
510 :node(tbl)
511 -- aria-labelledby title, otherwise above, otherwise lone group
512 if args.title or args.above or (args.group1 and not args.group2) then
513 nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
514 else
515 nav:attr('aria-label', 'Navbox')
516 end
517 end
518
519 if (args.nocat or 'false'):lower() == 'false' then
520 renderTrackingCategories(res)
521 end
522
523 return striped(tostring(res))
524 end
525
526 function p.navbox(frame)
527 if not getArgs then
528 getArgs = require('Module:Arguments').getArgs
529 end
530 args = getArgs(frame, {wrappers = {'Template:Navbox', 'Template:Navbox subgroup'}})
531 if frame.args.border then
532 -- This allows Template:Navbox_subgroup to use {{#invoke:Navbox|navbox|border=...}}.
533 args.border = frame.args.border
534 end
535
536 -- Read the arguments in the order they'll be output in, to make references number in the right order.
537 local _
538 _ = args.title
539 _ = args.above
540 for i = 1, 20 do
541 _ = args["group" .. tostring(i)]
542 _ = args["list" .. tostring(i)]
543 end
544 _ = args.below
545
546 return p._navbox(args)
547 end
548
549 return p