Module:Tree chart

From Ekatra Foundation
Revision as of 14:35, 11 November 2025 by Gurwinder (talk | contribs)
Jump to navigation Jump to search

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

-- Module:Tree chart
-- Defensive renderer that emits a scoped inline stylesheet next to the table.
pcall(function() require('strict') end)

local p = {}
local cells = mw.loadData('Module:Tree chart/data')

-- safe wrapper to insert raw HTML fragments into mw.html nodes
local function safe_wikitext(node, html)
    node:wikitext(html)
end

function p._main(cell_args)
    local out = mw.html.create()

    -- Inline style block scoped to .tree-chart-inline-style (highly specific, uses !important)
    local style = [[
<style type="text/css">
/* Scoped tree styles that override global rules */
.tree-chart-inline-style { border-collapse: collapse !important; border-spacing: 0 !important; vertical-align: top !important; display: inline-table !important; font-family: inherit !important; }
.tree-chart-inline-style td { padding: 0 !important; margin: 0 !important; vertical-align: top !important; box-sizing: border-box !important; white-space: nowrap !important; line-height: 1 !important; font-size: 0.9rem !important; }
.tree-chart-inline-style td.connector { padding: 0 !important; width:1em !important; height:1em !important; min-width:1em !important; min-height:1em !important; max-width:1em !important; max-height:1em !important; line-height:1 !important; }
.tree-chart-inline-style td.box { padding: 0.35em 0.9em !important; white-space: normal !important; font-size: 1rem !important; text-align: center !important; }
.tree-chart-inline-style td[style*="border-bottom"], .tree-chart-inline-style td[style*="border-right"], .tree-chart-inline-style td[style*="border"] { box-sizing: border-box !important; }
</style>
    ]]
    out:wikitext(style)

    -- Build table with explicit style attribute (avoid :css camelCase pitfalls)
    local tbl = out:tag('table')
        :addClass('tree-chart-inline-style')
        :attr{ style = 'border-collapse: collapse; border-spacing: 0; vertical-align: top;' }

    local top = tbl:tag('tr'):attr{ style = 'height:1px; text-align:center;' }
    local bottom = tbl:tag('tr'):attr{ style = 'height:1px; text-align:center;' }

    for _, v in ipairs(cell_args) do
        if type(v) == 'string' then
            local celldef = cells[v]
            if celldef then
                -- celldef.t / celldef.b are fragments produced by Module:Tree chart/data (strings)
                -- Tag their <td> elements as connector cells by adding class="connector"
                if celldef.t then
                    local tstr = tostring(celldef.t)
                    -- add class only if not already present
                    tstr = tstr:gsub('<td(.-)>', function(attrs)
                        -- if class attribute exists, append connector; otherwise add it
                        if attrs:match('class%s*=') then
                            return '<td' .. attrs:gsub('class%s*=%s*"(.-)"', function(c) return string.format(' class="%s connector"', c) end) .. '>'
                        else
                            return '<td' .. attrs .. ' class="connector">'
                        end
                    end)
                    safe_wikitext(top, tstr)
                else
                    top:tag('td'):addClass('connector'):attr{ style = 'width:1em; height:1em;' }
                end

                if celldef.b then
                    local bstr = tostring(celldef.b)
                    bstr = bstr:gsub('<td(.-)>', function(attrs)
                        if attrs:match('class%s*=') then
                            return '<td' .. attrs:gsub('class%s*=%s*"(.-)"', function(c) return string.format(' class="%s connector"', c) end) .. '>'
                        else
                            return '<td' .. attrs .. ' class="connector">'
                        end
                    end)
                    safe_wikitext(bottom, bstr)
                else
                    bottom:tag('td'):addClass('connector'):attr{ style = 'width:1em; height:1em;' }
                end
            else
                -- unknown symbol: render empty connector td
                top:tag('td'):addClass('connector'):attr{ style = 'width:1em; height:1em;' }
                bottom:tag('td'):addClass('connector'):attr{ style = 'width:1em; height:1em;' }
            end
        else
            -- custom cell with text (a boxed cell)
            local colspan = v.colspan or cell_args.colspan or 6
            local rowspan = v.rowspan or cell_args.rowspan or 2
            local border_prop = v.border or cell_args.border or '2'
            local border = tostring(border_prop) .. 'px solid'

            top:tag('td')
                :addClass('box')
                :attr{
                    colspan = tostring(colspan),
                    rowspan = tostring(rowspan),
                    style = 'padding:0.35em 0.9em; border: ' .. border .. '; box-sizing: border-box;'
                }
                :wikitext(v.text or '')

            -- if rowspan < 2 we need a bottom placeholder; most boxes use rowspan=2 so bottom is empty
            if not tonumber(rowspan) or tonumber(rowspan) < 2 then
                bottom:tag('td'):addClass('connector'):attr{ style = 'width:1em; height:1em;' }
            end
        end
    end

    return tostring(tbl)
end

function p.main(frame)
    local args = require('Module:Arguments').getArgs(frame,
        { wrappers = 'Template:Tree chart', trim = false, removeBlanks = false })

    local cell_args = {
        colspan = args.colspan,
        rowspan = args.rowspan,
        border = args.border,
        boxstyle = args.boxstyle
    }

    for _, val in ipairs(args) do
        local trimmedVal = val:match('^%s*(.-)%s*$')
        if trimmedVal == '' then trimmedVal = '$' end

        if cells[trimmedVal] then
            table.insert(cell_args, trimmedVal)
        else
            local rightTrimmedVal = val:gsub('%s+$','')
            local custom = {
                text = args[trimmedVal] or ('{{{'..trimmedVal..'}}}'),
                colspan = args['colspan_'..rightTrimmedVal],
                rowspan = args['rowspan_'..rightTrimmedVal],
                border = args['border_'..rightTrimmedVal],
                boxstyle = args['boxstyle_'..rightTrimmedVal]
            }
            table.insert(cell_args, custom)
        end
    end

    return p._main(cell_args)
end

return p