Module:Tree chart: Difference between revisions

From Ekatra Foundation
Jump to navigation Jump to search
No edit summary
Tag: Reverted
No edit summary
Tag: Reverted
Line 1: Line 1:
-- Module:Tree chart
-- Module:Tree chart
-- Defensive renderer for table-based tree pieces
-- Defensive renderer that emits a scoped inline stylesheet next to the table.
-- Avoids hard require('strict') so it won't fail on installs without that library.
 
pcall(function() require('strict') end)
pcall(function() require('strict') end)


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


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)
function p._main(cell_args)
     local ret = mw.html.create()
     local out = mw.html.create()
     -- use exact CSS property names (strings) for hyphenated properties
 
     local tbl = ret:tag('table')
    -- Inline style block scoped to .tree-chart-inline-style (highly specific, uses !important)
                :addClass('tree-chart-table')
    local style = [[
                :css{
<style type="text/css">
                    ['border-collapse'] = 'collapse',
/* Scoped tree styles that override global rules */
                    ['border-spacing'] = '0',
.tree-chart-inline-style { border-collapse: collapse !important; border-spacing: 0 !important; vertical-align: top !important; display: inline-table !important; font-family: inherit !important; }
                    ['vertical-align'] = 'top'
.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'):css{ ['height'] = '1px', ['text-align'] = 'center' }
     local top = tbl:tag('tr'):attr{ style = 'height:1px; text-align:center;' }
     local bottom = tbl:tag('tr'):css{ ['height'] = '1px', ['text-align'] = 'center' }
     local bottom = tbl:tag('tr'):attr{ style = 'height:1px; text-align:center;' }


     for _, v in ipairs(cell_args) do
     for _, v in ipairs(cell_args) do
Line 27: Line 39:
             local celldef = cells[v]
             local celldef = cells[v]
             if celldef then
             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
                 if celldef.t then
                     top:wikitext(celldef.t)
                     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
                 else
                     top:tag('td'):wikitext('')
                     top:tag('td'):addClass('connector'):attr{ style = 'width:1em; height:1em;' }
                 end
                 end
                 if celldef.b then
                 if celldef.b then
                     bottom:wikitext(celldef.b)
                     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
                 else
                     bottom:tag('td'):wikitext('')
                     bottom:tag('td'):addClass('connector'):attr{ style = 'width:1em; height:1em;' }
                 end
                 end
             else
             else
                 top:tag('td'):wikitext('')
                -- unknown symbol: render empty connector td
                 bottom:tag('td'):wikitext('')
                 top:tag('td'):addClass('connector'):attr{ style = 'width:1em; height:1em;' }
                 bottom:tag('td'):addClass('connector'):attr{ style = 'width:1em; height:1em;' }
             end
             end
         else
         else
            -- custom cell with text (a boxed cell)
             local colspan = v.colspan or cell_args.colspan or 6
             local colspan = v.colspan or cell_args.colspan or 6
             local rowspan = v.rowspan or cell_args.rowspan or 2
             local rowspan = v.rowspan or cell_args.rowspan or 2
Line 48: Line 83:


             top:tag('td')
             top:tag('td')
                 :attr{ colspan = tostring(colspan), rowspan = tostring(rowspan) }
                :addClass('box')
                :css{ padding = '0.2em' }
                 :attr{
                :attr{ style = 'border: ' .. border .. ';' } -- use explicit style when assembling string
                    colspan = tostring(colspan),
                 :cssText(v.boxstyle or cell_args.boxstyle or '')
                    rowspan = tostring(rowspan),
                    style = 'padding:0.35em 0.9em; border: ' .. border .. '; box-sizing: border-box;'
                 }
                 :wikitext(v.text or '')
                 :wikitext(v.text or '')


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


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


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


         if cells[trimmedVal] then
         if cells[trimmedVal] then

Revision as of 14:35, 11 November 2025

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