Module:NavboxBuilder

From TestWiki

Documentation for this module may be created at Module:NavboxBuilder/doc

-- <nowiki>
local NBB = {}

-- Constants
local EXPANDED, COLLAPSED = 1, 2

-- Easy access to all arguments
local arguments = {}

-- Invokable - Navbox creation
function NBB.create(frame)
    if frame then arguments = getArguments(frame) end
    if type(NBB.hlist) == 'boolean' then NBB.hlist = NBB.hlist and 'hlist' or '' end
    if type(NBB.vlist) == 'boolean' then NBB.vlist = NBB.vlist and 'vlist' or '' end

    local main = prepMain()
    local sections = prepSections(main)
    
    local navbox = ''
    if #sections.list > 0 then navbox = makeNavbox(main, sections) end
    
    return navbox
end
-- Invokable - Return parameter documentation
function NBB.documentation(frame)
    local docs
    if not pcall(function () -- Try specified lang code
        local lang = mw.ustring.lower(mw.text.trim(tostring(frame.args[1])))
        docs = require('Dev:NavboxBuilder/doc/' .. lang)
    end) then pcall(function () -- Then wiki lang
        local lang = mw.getContentLanguage():getCode()
        docs = require('Dev:NavboxBuilder/doc/' .. lang)
    end) end
    if not docs then -- Then English
        docs = require('Dev:NavboxBuilder/doc/en') -- Then default
    end
    for k,v in pairs(NBB.params) do
        v = mw.ustring.gsub(v, '#', tostring(NBB.n), 1)
        v = mw.ustring.gsub(v, '#', tostring(NBB.m), 1)
        docs = mw.ustring.gsub(docs, '%%'..k..'%%', v)
    end
    docs = mw.ustring.gsub(docs, '%%n%%', NBB.n)
    docs = mw.ustring.gsub(docs, '%%m%%', NBB.m)
    
    return docs
end

-- Customizing default parameters
function NBB.changeParameters(params)
    if type(params) == 'table' then
        for k,v in pairs(params) do
            if NBB.params[k] then
                if k:sub(1, 6) == 'value_' then v = mw.ustring.lower(mw.text.trim(tostring(v) or '')) end
                NBB.params[k] = v
            end
        end
    end
    return NBB
end

-- Default parameter names
NBB.params = {
    -- Settings
    links = 'Links',
    state = 'State',
    
    -- Fields
    title = 'Title',
    above = 'Above',
    below = 'Below',
    limage = 'Left image',
    rimage = 'Right image',
    
    -- Sections
    header_n = 'Header #',
    layout_n = 'Layout #',
    state_n = 'State #',
    header_state = 'Header state',
    
    -- Table layout
    limage_n = 'Left image #',
    rimage_n = 'Right image #',
    
    -- Horizontal layout
    perrow_n = 'Per row #',
    span_n = 'Span #',
    
    -- Groups
    group_n = 'Group #',
    group_n_m = 'Group #.#',
    list_n = 'List #',
    list_n_m = 'List #.#',
    
    -- CSS
    navbox_class = 'Navbox class',
    navbox_style = 'Navbox style',
    title_class = 'Title class',
    title_style = 'Title style',
    base_class = 'Base class',
    base_style = 'Base style',
    above_class = 'Above class',
    above_style = 'Above style',
    below_class = 'Below class',
    below_style = 'Below style',
    image_class = 'Image class',
    image_style = 'Image style',
    limage_class = 'Left image class',
    limage_style = 'Left image style',
    rimage_class = 'Right image class',
    rimage_style = 'Right image style',
    header_class = 'Header class',
    header_style = 'Header style',
    header_n_class = 'Header # class',
    header_n_style = 'Header # style',
    limage_n_class = 'Left image # class',
    limage_n_style = 'Left image # style',
    rimage_n_class = 'Right image # class',
    rimage_n_style = 'Right image # style',
    group_class = 'Group class',
    group_style = 'Group style',
    subgroup_class = 'Subgroup class',
    subgroup_style = 'Subgroup style',
    group_n_class = 'Group # class',
    group_n_style = 'Group # style',
    group_n_m_class = 'Group #.# class',
    group_n_m_style = 'Group #.# style',
    list_class = 'List class',
    list_style = 'List style',
    list_n_class = 'List # class',
    list_n_style = 'List # style',
    list_n_m_class = 'List #.# class',
    list_n_m_style = 'List #.# style',
    
    -- Values
    value_expanded = 'expanded',
    value_collapsed = 'collapsed',
    value_table_layout = 'table',
    value_horizontal_layout = 'horizontal',
}

-- How n's and m's are displayed in documentations
NBB.n = '<span class="accent">n</span>'
NBB.m = '<span class="accent">m</span>'

-- Class names for horizontal and vertical lists
NBB.hlist = 'hlist'
NBB.vlist = 'vlist'

-- Cleanup of arguments from frame and its parent
function getArguments(frame, useFrame, useParent)
    local args = {
        keys = {},
        frame = {},
        parent = {},
    }
    if frame and frame.args then
        local keys = {}
        if useFrame or useFrame == nil then
            for k,v in pairs(frame.args) do
                if type(k) == 'string' then k = mw.text.trim(k) end
                if type(v) == 'string' then v = mw.text.trim(v) end
                keys[k] = true
                args.frame[k] = v
            end
        end
        if frame.getParent and (useParent or useParent == nil) then
            local parent = frame:getParent()
            for k,v in pairs(parent.args) do
                if type(k) == 'string' then k = mw.text.trim(k) end
                if type(v) == 'string' then v = mw.text.trim(v) end
                keys[k] = true
                args.parent[k] = v
            end
        end
        for k,v in pairs(keys) do table.insert(args.keys, k) end
    end
    return args
end

-- Returns value of a customized parameter
function getValue(key, n, m)
    local param = NBB.params[key]
    n, m = tonumber(n), tonumber(m)
    if n then param = mw.ustring.gsub(param, '#', tostring(n), 1) end
    if m then param = mw.ustring.gsub(param, '#', tostring(m), 1) end
    
    local val = arguments.parent[param] or arguments.frame[param]
    if val and val ~= '' then return val, arguments.frame[param], arguments.parent[param] end
    return nil
end

-- Checks the input for customized values
function checkValue(val, check)
    val = mw.ustring.lower(mw.text.trim(tostring(val) or ''))
    if type(check) == 'table' then
        for k,v in pairs(check) do
            if NBB.params['value_' .. k] then
                local test = NBB.params['value_' .. k]
                if val == test then return v end
            end
        end
    else
        check = tostring(check)
        if NBB.params['value_' .. check] then
            local test = NBB.params['value_' .. check]
            if val == test then return true end
        end
    end
    return nil
end

-- Returns a list of values for parameters with variables
function getList(...)
    local lists = {}
    local patterns = {}
    for i,v in ipairs(arg) do
        if v then
            lists[i] = {}
            patterns[i] = '^'..mw.ustring.gsub(NBB.params[v], '#', '([0-9]+)')..'$'
        end
    end
    for i,k in ipairs(arguments.keys) do
        local v = arguments.parent[k] or arguments.frame[k]
        if v and v ~= '' then
            for i,pattern in ipairs(patterns) do
                local n, m = mw.ustring.match(k, pattern)
                if m then
                    if not lists[i][tonumber(n)] then lists[i][tonumber(n)] = {} end
                    lists[i][tonumber(n)][tonumber(m)] = v
                    break
                elseif n then
                    lists[i][tonumber(n)] = v
                    break
                end
            end
        end
    end
    return unpack(lists)
end

-- Determine which section the row belongs to
function getSectionNo(test, list)
    for i=2,#list do
        if test < list[i] then return list[i-1] end
    end
    return list[#list] or nil
end

-- Reads parameters and prepares an object with settings for the navbox
function prepMain()
    local main = {}
    
    main.title = getValue('title')
    if main.title then
        main.links = getValue('links')
        main.state = checkValue(getValue('state'), {['expanded'] = EXPANDED, ['collapsed'] = COLLAPSED})
    end
    
    main.above = getValue('above')
    main.below = getValue('below')
    main.limage = getValue('limage')
    main.rimage = getValue('rimage')
    return main
end

-- Reads parameters and prepares objects with necessary settings for each section
function prepSections(main)
    local sections = {[0] = { rows = {} }}
    local headers, layouts, lists, sublists = getList('header_n', 'layout_n', 'list_n', 'list_n_m')
    
    for k,v in pairs(headers) do
        sections[k] = { header = v, rows = {} }
    end
    for k,v in pairs(layouts) do
        if not sections[k] then
            sections[k] = { layout = v, rows = {} }
        end
    end
    local numbers = getKeys(sections, true)
    
    for k,v in pairs(lists) do
        local sec = getSectionNo(k, numbers)
        sections[sec].rows[k] = { list = v, group = getValue('group_n', k) }
    end
    for k,list in pairs(sublists) do
        local sec = getSectionNo(k, numbers)
        local obj = { rows = {}, group = getValue('group_n', k) }
        for l,v in pairs(list) do
            obj.rows[l] = { list = v, group = getValue('group_n_m', k, l) }
        end
        sections[sec].rows[k] = obj
    end
    for _,v in ipairs(numbers) do
        local rows = getKeys(sections[v].rows)
        if #rows > 0 then
            sections[v].state = checkValue(getValue('state_n', v), {['expanded'] = EXPANDED, ['collapsed'] = COLLAPSED})
                                or checkValue(getValue('header_state'), {['expanded'] = EXPANDED, ['collapsed'] = COLLAPSED})
            sections[v].layout = getValue('layout_n', v) or 'table'
        else
            sections[v] = nil
        end
    end
    sections.list = getKeys(sections, true)
    return sections
end

-- Returns a list of keys in a table
function getKeys(tab, sorted)
    local keys = {}
    if not tab then return keys end
    for k,v in pairs(tab) do
        table.insert(keys, k)
    end
    if sorted then table.sort(keys) end
    return keys
end

-- Applies styles and classes from parameters to the element
function applyCSS(elem, ...)
    if not elem then return nil end
    local classes, styles = {}, {}
    for i,v in ipairs(arg) do
        local c, cP, cF, s, sP, sF
        if type(v) == 'table' then
            local key = v[1]
            v[1] = key .. '_class'
            c, cF, cP = getValue(unpack(v))
            v[1] = key .. '_style'
            s, sF, sP = getValue(unpack(v))
        else
            v = tostring(v)
            c, cF, cP = getValue(v .. '_class')
            s, sF, sP = getValue(v .. '_style')
        end
        if c then
            table.insert(classes, cF)
            table.insert(classes, cP)
        end
        if s then
            table.insert(styles, sF)
            table.insert(styles, sP)
        end
    end
    if #classes > 0 then
        classes = mw.ustring.gsub(table.concat(classes, ' '), '  +', ' ')
        elem:addClass(classes)
        end
    if #styles > 0 then
        styles = mw.ustring.gsub(table.concat(styles, ';'), ';;+', ';')
        styles = mw.ustring.gsub(styles, ';$', '')
        elem:cssText(styles)
    end
    return elem
end

-- Makes an element collapsible
function collapsible(elem, header, state)
    if state then
        elem:addClass('mw-collapsible')
        if state == COLLAPSED then elem:addClass('mw-collapsed') end
        if header then header:addClass('mw-collapsible-toggle') end
        return true
    end
    return false
end

-- Creates a navbox with prepared settings
function makeNavbox(main, sections)
    -- Structure
    local box, title, wrapper, tab, row, links, above, below, limage, rimage, content
    
    box = mw.html.create('div')
    if main.title then
        title = box:tag('div')
        if main.links then links = title:tag('div'):wikitext(main.links .. ' ') end
        title:tag('span'):addClass('navbox-title-text'):wikitext(main.title)
    end
    
    wrapper = box:tag('div')
    tab = wrapper:tag('table')
    
    if main.above then
        above = tab:tag('tr'):tag('td'):newline():wikitext(main.above)
        above:done():newline()
    end
    row = tab:tag('tr')
    if main.limage then
        limage = row:tag('td'):newline():wikitext(main.limage)
        limage:done():newline()
    end
    
    content = row:tag('td')
    for i,v in ipairs(sections.list) do content:node(makeSection(sections[v], v)) end
    if #content.nodes == 0 then return nil end
    
    if main.rimage then
        rimage = row:tag('td'):newline():wikitext(main.rimage)
        rimage:done():newline()
    end
    if main.below then
        below = tab:tag('tr'):tag('td'):newline():wikitext(main.below)
        below:done():newline()
    end
    
    -- Appearance
    box:addClass('navbox')
    applyCSS(box, 'navbox')
    wrapper:addClass('navbox-table-wrapper')
    tab:addClass('navbox-table')
    content:addClass('navbox-content')
    
    if title then
        title:addClass('navbox-title')
        applyCSS(title, 'title')
        
        if collapsible(box, title, main.state) then wrapper:addClass('mw-collapsible-content') end
    end
    if links then links:addClass('navbox-template-links') end
    if above then
        above:addClass('navbox-above navbox-base navbox-padding')
        applyCSS(above, 'base', 'above')
    end
    if below then
        below:addClass('navbox-below navbox-base navbox-padding')
        applyCSS(below, 'base', 'below')
    end
    if limage then
        limage:addClass('navbox-image')
        applyCSS(limage, 'image', 'limage')
    end
    if rimage then
        rimage:addClass('navbox-image')
        applyCSS(rimage, 'image', 'rimage')
    end
    
    if limage or rimage then
        local cols = 1 + (limage and 1 or 0) + (rimage and 1 or 0)
        if cols > 1 then
            if above then above:attr('colspan', cols) end
            if below then below:attr('colspan', cols) end
        end
    end
    
    return box
end

-- Creates a single section
function makeSection(section, no)
    -- Structure
    local sec, header, wrapper
    
    sec = mw.html.create('div')
    
    if section.header then header = sec:tag('div'):wikitext(section.header) end
    wrapper = sec:tag('div')
    
    -- Appearance
    sec:addClass('navbox-section')
    wrapper:addClass('navbox-section-wrapper')
    if header then
        header:addClass('navbox-header navbox-base')
        if collapsible(sec, header, section.state) then wrapper:addClass('mw-collapsible-content') end
        applyCSS(header, 'base', 'header', {'header_n', no})
    end
    
    -- Layout
    local layout
    for k,v in pairs(NBB.formats) do
        if checkValue(section.layout, k .. '_layout') then
            layout = k
            break
        else
            if k == section.layout then
                layout = k
                break
            end
        end
    end
    layout = layout or 'table'
    section.layout = layout
    
    local res = NBB.formats[layout](section, no, wrapper)
    
    if res == nil then return nil end
    
    wrapper:node(res)
    wrapper:addClass('navbox-' .. layout .. '-layout')
    
    return sec
end

NBB.formats = {}

NBB.formats.table = function(section, no)
    local lists, deep, count, keys = {}, false, 0, getKeys(section.rows, true)
    if #keys == 0 then return nil end
    
    -- Structure
    local tab, limage, rimage
    
    section.limage = getValue('limage_n', no)
    section.rimage = getValue('rimage_n', no)
    
    tab = mw.html.create('table')
    for i1,v1 in ipairs(keys) do
        count = count + 1
        local row1 = section.rows[v1]
        
        -- Structure
        local tr, group, list
        
        tr = tab:tag('tr'):addClass('navbox-'..(count % 2 == 0 and 'even' or 'odd'))
        if i1 == 1 and section.limage then
            limage = tr:tag('td'):newline():wikitext(section.limage)
            limage:done():newline()
        end
        if row1.group then group = tr:tag('th'):wikitext(row1.group) end
        
        local keys = getKeys(row1.rows, true)
        if #keys > 0 then
            for i2,v2 in ipairs(keys) do
                local row2 = row1.rows[v2]
                
                -- Structure
                local tr, group, list = tr
                
                if i2 > 1 then
                    count = count + 1
                    tr = tab:tag('tr'):addClass('navbox-'..(count % 2 == 0 and 'even' or 'odd'))
                end
                if row2.group then
                    group = tr:tag('th'):wikitext(row2.group)
                    deep = true
                end
                
                list = tr:tag('td'):newline():wikitext(row2.list)
                list:done():newline()
                
                -- Appearance
                list:addClass('navbox-list navbox-padding'):addClass(NBB.hlist or '')
                applyCSS(list, 'list', {'list_n_m', v1, v2})
                if not row2.group then list:attr('colspan', 2) end
                if not row1.group then table.insert(lists, list) end
                
                if group then
                    group:addClass('navbox-subgroup navbox-base navbox-padding')
                    applyCSS(group, 'base', 'subgroup', {'group_n_m', v1, v2})
                end
            end
        else
            -- Structure
            list = tr:tag('td'):newline():wikitext(row1.list)
            list:done():newline()
            table.insert(lists, list)
            
            -- Appearance
            list:addClass('navbox-list navbox-padding'):addClass(NBB.hlist or '')
            if not row1.group then list:attr('colspan', 2):addClass('navbox-nogroup') end
            applyCSS(list, 'list', {'list_n', v1})
        end
        if i1 == 1 and section.rimage then
            rimage = tr:tag('td'):newline():wikitext(section.rimage)
            rimage:done():newline()
        end
        
        -- Appearance
        if group then
            group:addClass('navbox-group navbox-base navbox-padding')
            if #keys > 1 then group:attr('rowspan', #keys) end
            applyCSS(group, 'base', 'group', {'group_n', v1})
        end
    end
    
    -- Appearance
    tab:addClass('navbox-table')
    
    for i,v in ipairs(lists) do
        local span = v:getAttr('colspan') or 1
        if deep then span = span + 1 end
        if span == 1 then span = nil end
        v:attr('colspan', span)
    end
    
    if limage then
        limage:addClass('navbox-image')
        applyCSS(limage, 'image', {'limage_n', no})
        if count > 1 then limage:attr('rowspan', count) end
    end
    if rimage then
        rimage:addClass('navbox-image')
        applyCSS(rimage, 'image', {'rimage_n', no})
        if count > 1 then rimage:attr('rowspan', count) end
    end
    
    return tab
end
NBB.formats.horizontal = function(section, no, wrapper)
    local lists, deep, count, keys = {}, false, 0, getKeys(section.rows, true)
    if #keys == 0 then return nil end
    
    -- Structure
    local res, wrap
    
    wrap = tonumber(getValue('perrow_n', no))
    if wrap then
        wrap = math.max(1, wrap)
        -- if wrap > 0 then
        --     wrapper:addClass('navbox-static'):css('--wrap', wrap)
        -- end
    end
    
    
    res = mw.html.create('')
    for i1,v1 in ipairs(keys) do
        local row1 = section.rows[v1]
        
        -- Structure
        local keys = getKeys(row1.rows, true)
        if #keys > 0 then
            for i2,v2 in ipairs(keys) do
                count = count + 1
                local row2 = row1.rows[v2]
                
                -- Structure
                local col, group, list
                
                col = res:tag('div'):addClass('navbox-'..(count % 2 == 0 and 'even' or 'odd'))
                if row2.group then group = col:tag('div'):wikitext(row2.group) end
            
                if wrap then -- delete in case of css vars
                    col:css('flex-basis', 100/wrap .. '%')
                end
                    
                list = col:tag('div'):newline():wikitext(row2.list)
                list:done():newline()
                table.insert(lists, list)
                
                -- Appearance
                col:addClass('navbox-col')
                list:addClass('navbox-list navbox-padding'):addClass(NBB.vlist or '')
                applyCSS(list, 'list', {'list_n', v1})
                
                if group then
                    group:addClass('navbox-group navbox-base navbox-padding')
                    applyCSS(group, 'base', 'group', {'group_n', v1})
                end
            end
        else
            count = count + 1
            -- Structure
            local col, group, list
            
            col = res:tag('div'):addClass('navbox-'..(count % 2 == 0 and 'even' or 'odd'))
            if row1.group then group = col:tag('div'):wikitext(row1.group) end
            
            if wrap then
                local span = math.max(0, math.min(wrap, tonumber(getValue('span_n', v1)) or 1))
                col:css('flex-basis', span/wrap*100 .. '%') -- delete in case of css vars
                -- if span ~= 1 then
                --     col:css('--span', span)
                -- end
            else
                col._span = tonumber(getValue('span_n', v1))
            end
            
            list = col:tag('div'):newline():wikitext(row1.list)
            list:done():newline()
            table.insert(lists, list)
            
            -- Appearance
            col:addClass('navbox-col')
            list:addClass('navbox-list navbox-padding'):addClass(NBB.vlist or '')
            applyCSS(list, 'list', {'list_n', v1})
            
            if group then
                group:addClass('navbox-group navbox-base navbox-padding')
                applyCSS(group, 'base', 'group', {'group_n', v1})
            end
        end
    end
    if not wrap then
        wrap = #lists
        for i,v in ipairs(lists) do
            local col = v.parent
            col:css('flex-basis', math.max(0, math.min(wrap, col._span or 1))/wrap*100 .. '%')
        end
    end
    
    return res
end

return NBB