Module:InfoboxV2

From The Satanic Wiki
Jump to navigation Jump to search

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

local Infobox = {}
local metatable = {}
local methodtable = {}

metatable.__index = methodtable

metatable.__tostring = function( t )
    return tostring( t:getBoxText() )
end

-- Uses capiunto as the base
local capiunto = require 'capiunto'
local libraryUtil = require( 'libraryUtil' )

--- Returns true if input is a string or number
--- @param input string|number|table
--- @return boolean
local function verifyStringNum( input )
    return type( input ) == 'string' or type( input ) == 'number'
end

--- This fills the box with content
local function addBoxContent( t )
    if t.contentAdded then
        return
    end

    for _, row in ipairs( t.rows ) do
        local label = tostring( row.label )

        if row.type == 'row' then
            if (type( label ) == 'string' or type( label ) == 'number') and ( type( row.data ) == 'string' or type( row.data ) == 'number' ) then
                t.capiunto:addRow( label, row.data, row.class, row.rowClass )
            end
        elseif row.type == 'header' or row.type == 'image' then
            if row.type == 'image' and row.rowClass ~= 'row-image' then
                row.rowClass = row.rowClass .. ' row-image'
            end

            -- Only add the header if it has content
            if mw.ustring.find( row.rowClass, 'row-header', 1, true ) ~= nil and t.headerContentCounts[ label ] ~= nil and t.headerContentCounts[ label ] == 0 then
                -- skip add
            else
                t.capiunto:addRow( nil, label, row.class, row.rowClass )
            end
        elseif row.type == 'subheader' then
            t.capiunto:addSubHeader( label, row.class, row.style )
        end
    end

    t.contentAdded = true
end

--- Iterate table in key order
local function spairs( t, orderFn )
    -- collect the keys
    local keys = {}
    for k, v in pairs( t ) do
        if v ~= nil then
            keys[ #keys + 1 ] = tostring( k )
        end
    end

    -- if order function given, sort by it by passing the table and keys a, b,
    -- otherwise just sort the keys
    if orderFn then
        table.sort( keys, function( a, b ) return orderFn( t, a, b ) end )
    else
        table.sort( keys )
    end

    -- return the iterator function
    local i = 0
    return function()
        i = i + 1
        if keys[ i ] then
            return keys[ i ], t[ keys[ i ] ]
        end
    end
end

--- Parse all available capiunto options from a table
--- Returns only non nil values
---
--- @param args table
--- @return table
local function parseArgs( args )
    local options = {
        [ 'isChild' ]      = args[ 'isChild' ]      or nil,
        [ 'isSubbox' ]     = args[ 'isSubbox' ]     or nil,

        [ 'title' ]        = args[ 'title' ]        or nil,
        [ 'titleClass' ]   = args[ 'titleClass' ]   or nil,
        [ 'titleStyle' ]   = args[ 'titleStyle' ]   or nil,

        [ 'top' ]          = args[ 'top' ]          or nil,
        [ 'topClass' ]     = args[ 'topClass' ]     or nil,
        [ 'topStyle' ]     = args[ 'topStyle' ]     or nil,

        [ 'captionStyle' ] = args[ 'captionStyle' ] or nil,
        [ 'imageStyle' ]   = args[ 'imageStyle' ]   or nil,
        [ 'imageClass' ]   = args[ 'imageClass' ]   or nil,

        [ 'bodyClass' ]    = args[ 'bodyClass' ]    or nil,
        [ 'bodyStyle' ]    = args[ 'bodyStyle' ]    or nil,
        [ 'headerStyle' ]  = args[ 'headerStyle' ]  or nil,
        [ 'labelStyle' ]   = args[ 'labelStyle' ]   or nil,
        [ 'dataStyle' ]    = args[ 'dataStyle' ]    or nil,
    }

    local i = 0
    local keys, values = {}, {}
    for k, v in pairs( options ) do
        i = i + 1
        keys[ i ] = k
        values[ i ] = v
    end

    while i > 0 do
        if keys[ i ] == nil then
            table.remove( keys, i )
            table.remove( values, i )
            break
        end
        i = i - 1
    end

    local finalOptions = {}
    for i = 1, #keys do
        finalOptions[ keys[ i ] ] = values[ i ]
    end

    local finalArgs = {}
    for k, v in pairs( args ) do
        if finalOptions[ k ] == nil and v ~= nil then
            finalArgs[ k ] = v
        end
    end

    return finalOptions, finalArgs
end

--- Replaces a row if labels match and returns the replaced index
--- Otherwise returns the next free index
---
--- @return number New index
local function replaceInRows( t, targetLabel )
    for idx, row in ipairs( t.rows ) do
        if row.label == targetLabel then
            table.remove( t.rows, idx )
            return idx
        end
    end

    return #t.rows + 1
end

--- Adds one of three special rows to the infobox
--- As defined by Capiunto
--- Available rowTypes are: title (caption), top (first row), bottom (last row)
---
--- @param t table The infobox
--- @param rowType string title, top or bottom
--- @param class string Row class
--- @param style string Css style
--- @return table
local function addSpecialRow( t, rowType, text, class, style )
    if t.capiunto == nil then
        return
    end

    if verifyStringNum( text ) then
        t.capiunto.args[ rowType ] = text
    end

    if type( class ) == 'string' then
        t.capiunto.args[ rowType .. 'Class' ] = class
    end

    if type( style ) == 'string' then
        t.capiunto.args[ rowType .. 'Style' ] = style
    end

    return t
end

--- Generates a link to Special:Upload with pre-defined parameters
---
--- @param fileName string|nil The filename including the extension
--- @param title string The title of the infobox
local function generateUploadLink( fileName, category )
	local title = mw.title.new( 'Special:Upload' )

	if fileName == nil then
		fileName = mw.title.getCurrentTitle().text .. '.jpg'
	end
	
	if category == nil then
		category = mw.title.getCurrentTitle().text
	end

	local description = [==[
=={{int:filedesc}}==
{{Information
|description={{en|1=%s}}
|date=%s
|source=<!-- Please insert link to picture -->
|author=RSI
|permission=
|other versions=
}}

=={{int:license-header}}==
{{license-rsi}}

[[Category:%s]]
	]==]

	description = mw.ustring.format(
		description,
		category,
		mw.getContentLanguage():formatDate('Y-m-d H:i:s'),
		category
	)

	local parameters = {
		[ 'wpDestFile' ] = fileName,
		[ 'wpLicense' ] = 'license-rsi',
		[ 'wpUploadDescription' ] = description
	}

	return title:fullUrl( parameters, 'https' )
end

--- Generates a link to Special:Uploadwizard with pre-defined parameters
---
--- @param fileName string|nil The filename excluding the extension
--- @param title string The title of the infobox
local function generateUploadwizardLink( fileName, category )
	local title = mw.title.new( 'Special:Uploadwizard' )

	if fileName == nil then
		fileName = mw.title.getCurrentTitle().text
	end

	if category == nil then
		category = mw.title.getCurrentTitle().text
	end

	local parameters = {
		[ 'categories' ] = category,
		[ 'title' ] = fileName,
		[ 'description' ] = fileName,
		-- Missing: Date
		-- Missing pre license select
	}

	return title:fullUrl( parameters, 'https' )
end

--- Base method to add a row
--- @see https://www.mediawiki.org/wiki/Extension:Capiunto/Infobox
--- @param t table The instance
--- @param rowLabel string Row label
--- @param rowData string Row content (can be anything)
--- @param dataClass string CSS class added to data
--- @param rowCssClass string CSS class added to row
--- @param rowStyle string CSS style only used if type is 'subheader'!
--- @param type string Either 'row', 'header', 'image' or 'subheader'
function methodtable.addRow( t, rowLabel, rowData, dataClass, rowCssClass, rowStyle, type )
    t.checkSelf( t, 'addRow' )

    type = type or 'row'

    if t.removeEmpty == true then
        if type == 'row' and ( rowData == nil or rowData == t.emptyString ) then
            return t
        end
    end

    if type == 'header' then
        t.currentHeader = rowLabel

        if t.headerContentCounts[ rowLabel ] == nil then
            local count = 0
            -- A "single" header is a header without content
            -- This is used to circumvent removing the header as it as no content
            if dataClass == 'single' then
                count = 1
            end

            t.headerContentCounts[ rowLabel ] = count
        end
    end

    -- increment the header count
    if t.currentHeader ~= nil and type == 'row' and t.headerContentCounts[ t.currentHeader ] ~= nil then
        t.headerContentCounts[ t.currentHeader ] = t.headerContentCounts[ t.currentHeader ] + 1
    end

    local pos = -1
    if t.allowReplace == true and #t.rows > 0 then
        pos = replaceInRows( t, rowLabel )
    else
        t.rowCount = t.rowCount + 1
        pos = t.rowCount
    end

    if rowCssClass == nil then
        rowCssClass = 'row-' .. type
        if rowCssClass == 'row-row' then
            rowCssClass = 'row'
        end
    end

    if not mw.ustring.match( rowCssClass, 'row' ) then
        local toAdd = type
        if toAdd ~= 'row' then
            toAdd = 'row-' .. type
        end

        rowCssClass = rowCssClass .. ' ' .. toAdd
    end

    table.insert( t.rows, pos, {
        type = type,
        label = rowLabel,
        data = rowData or nil,
        class = dataClass or nil,
        rowClass = rowCssClass,
        style = rowStyle or nil
    })

    return t
end

--- Adds a header to the infobox
function methodtable.addHeader( t, text, class, rowClass )
    t.checkSelf( t, 'addHeader' )

    rowClass = ( rowClass or '' ) .. ' row-header'

    return t:addRow( text, nil, class, rowClass, nil, 'header' )
end

--- Adds a title row to the infobox
function methodtable.addTitle( t, text, class, rowClass )
    t.checkSelf( t, 'addTitle' )

    rowClass = ( rowClass or '' ) .. ' row-title'

    return t:addRow( text, nil, class, rowClass, nil, 'header' )
end

--- Adds a subheader to the infobox
function methodtable.addSubHeader( t, text, class, style )
    t.checkSelf( t, 'addSubHeader' )

    return t:addRow( text, nil, class, 'row-subheader', style, 'subheader' )
end

--- Adds a caption to the infobox
function methodtable.addCaption( t, text, class, style )
    t.checkSelf( t, 'addCaption' )

    return addSpecialRow( t, 'title', text, class, style )
end

--- Adds a caption to the infobox
function methodtable.addTop( t, text, class, style )
    t.checkSelf( t, 'addTop' )

    return addSpecialRow( t, 'top', text, class, style )
end

--- Adds a bottom to the infobox
function methodtable.addBottom( t, text, class, style )
    t.checkSelf( t, 'addBottom' )

    return addSpecialRow( t, 'bottom', text, class, style )
end

--- Adds an image to the infobox
--- @param file string Wiki page filename
--- @param options table Image options
--- @param checkExistence boolean True to check if the file exists
function methodtable.addImage( t, file, options, checkExistence )
    t.checkSelf( t, 'addImage' )
    local isPlaceholder = false

    if type( file ) ~= 'string' then
        if t.displayPlaceholder == false then
            return
        end

        file = t.placeholderImage
        isPlaceholder = true
    end

    local exists = true
    local title = mw.title.new( mw.uri.decode( file, 'WIKI' ), 6 )

    if title == nil then
        return
    end

    if type( options ) == 'string' then
        options = {
            [ 'rowClass' ] = options
        }
    else
        options = options or {}
    end

    if checkExistence ~= nil and checkExistence == true then
        exists = title.exists
    end

    local class = options[ 'rowClass' ] or nil

    local function buildOptions( imageOptions )
        local out = {}

        for k, v in pairs( imageOptions ) do
            if k ~= 'rowClass' and type( k ) == 'string' and type( v ) == 'string' then
                table.insert( out, k .. '=' .. v )
            elseif type( k ) == 'number' then
                table.insert( out, v )
            end
        end

        return table.concat( out, '|' )
    end

    if isPlaceholder == true then
    	options[ 'link' ] = generateUploadLink()
    	if class == nil then
    		class = 'placeholder'
		else
			class = class .. ' placeholder'
		end
	end

    local imageOptions = buildOptions( options )
    if imageOptions ~= '' then
        imageOptions = '|' .. imageOptions
        
        if isPlaceholder == true then
        	imageOptions = imageOptions .. '|Klicke um Bild hochzuladen'
    	end
    end

    local header = '[[' .. title.prefixedText .. imageOptions .. ']]'

    if exists == false then
        header = 'Datei fehlt'
    end

    return t:addRow( header, nil, class, 'row-image', nil, 'image' )
end

--- Allows to add arbitrary rows from template args
--- @param args table Template arguments
--- @param prefix string|nil An optional prefix that each argument must have to be added as a row
--- @param allowedKeys table|nil An optional table of keys that are allowed as rows, are passed to ustring.match
---
--- Example param = !row -> Only Template arguments in the form of |!row...=Content are added as Rows
--- The prefix gets replaced from each key: |!rowLabel=Content  => Label=Content
---
--- The row type can be set by adding 'subheader' or 'header' to the key
--- Example: |header1=Content => Header row with content 'content' added
--- Multiple headers must be suffixed by numbers
---
--- !!! NOTE !!!
--- Lua tables do not preserve order, to mitigate this, this module SORTS the given arguments ALPHABETICALLY
--- You can prefix arguments with numbers to preserve the order
--- Example Template:
--- {{Infobox
--- |1-header=Header for block one
--- |1.1-RowLabel=This is the content for row 1.1
--- |1.2-RowLabel=This again is the content
--- |2-subheader=Subheader
--- |2.1-Label=Content
--- |2.2...
--- }}
---
--- Example Module Call:
--- infobox.addRowsFromArgs({
---   [ '1-header' ] = 'Header for block one',
---   [ '1.1-RowLabel' ] = 'This is...',
--- })
function methodtable.addRowsFromArgs( t, args, prefix, allowedKeys )
    t.checkSelf( t, 'addRowsFromArgs' )

    if type( args ) ~= 'table' then
        return
    end

    _, args = parseArgs( args )

    local function canAdd( key )
        if key == 'image' then -- Image is added separately
            return false
        end

        if prefix == nil then
            if type( allowedKeys ) == 'table' then
                for _, allowed in pairs( allowedKeys ) do
                    if mw.ustring.match( key, allowed ) then
                        return true
                    end
                end

                return false
            end

            return true
        elseif type( prefix ) == 'string' then
            return mw.ustring.match( key, prefix )
        end

        return true
    end

    for k, v in spairs( args ) do
        if type( k ) == 'string' and type( v ) == 'string' then
            if canAdd( k ) then
                if prefix~= nil and type( prefix ) == 'string' then
                    k = mw.ustring.gsub( k, prefix, '' )
                end

                -- Remove Digits - and . at start of label
                k = mw.ustring.gsub( k, '^[%d%-%.]+', '' )
                -- Removes digits at the end, should enable MW Infobox behaviour
                k = mw.ustring.gsub( k, '[%d]+$', '' )

                local splitted = mw.text.split( v, '<>', true )

                local label = splitted[1]
                local class = splitted[2] or ''
                local rowClass = splitted[3] or ''

                if mw.ustring.match( k, 'subheader' ) then
                    t:addSubHeader( v )
                elseif mw.ustring.match( k, 'header' ) then
                    t:addHeader( label, class, rowClass )
                elseif mw.ustring.match( k, 'title' ) then
                    t:addTitle( label, class, rowClass )
                elseif mw.ustring.match( k, 'caption' ) then
                    t:addCaption( label, class, rowClass )
                elseif mw.ustring.match( k, 'top' ) then
                    t:addTop( label, class, rowClass )
                elseif mw.ustring.match( k, 'bottom' ) then
                    t:addBottom( label, class, rowClass )
                else
                    t:addRow( k, label, class, rowClass )
                end
            end
        end
    end

    return t
end

--- Flag to allow or disable row replacing
--- @param flag boolean
function methodtable.setAllowReplace( t, flag )
    t.checkSelf( t, 'setAllowReplace' )

    t.allowReplace = flag
end

--- Returns the raw capiunto box
--- @return table
function methodtable.getBox( t )
    t.checkSelf( t, 'getBox' )

    addBoxContent( t )

    return t.capiunto
end

--- Returns the table string
--- @return string
function methodtable.getBoxText( t )
    t.checkSelf( t, 'getBoxText' )

    return tostring( t:getBox() )
end

--- Init the infobox
--- @param options table|nil Option table passed to capiunto.create
function Infobox.create( options )
    local instance = {
        -- Table containing Header = count of rows
        headerContentCounts = {},
        -- The currently active header
        currentHeader = nil,
        -- The row tables
        rows = {},
        -- Total number of rows in the box
        rowCount = 0,
        -- Capiunto table
        capiunto = {},
        -- Flag to stop adding rows to the box
        contentAdded = false,
        -- Flag to enable replacing already added rows
        allowReplace = false,
        -- Flag to discard empty rows
        removeEmpty = false,
        -- Optional string which is valued as empty
        emptyString = nil,
        -- Display a placeholder image if addImage does not find an image
        displayPlaceholder = true,
        -- Placeholder Image
        placeholderImage = 'Platzhalter.webp',
    }

    if options.allowReplace ~= nil then
        instance.allowReplace = options.allowReplace
        options.allowReplace = nil
    end

    if options.removeEmpty ~= nil then
        instance.removeEmpty = true
        options.removeEmpty = nil
    end

    if options.emptyString ~= nil then
        instance.emptyString = options.emptyString
        options.emptyString = nil
    end

    if options.displayPlaceholder ~= nil then
        instance.displayPlaceholder = options.displayPlaceholder
        options.displayPlaceholder = nil
    end

    if options.placeholderImage ~= nil then
        instance.placeholderImage = options.placeholderImage
        options.placeholderImage = nil
    end

    setmetatable( instance, metatable )

    instance.capiunto = capiunto.create( parseArgs( options or {} ) )

    instance.checkSelf = libraryUtil.makeCheckSelfFunction( 'Infobox', 'instance', instance, 'Method Call' )

    return instance
end

--- Create a infobox from args
--- @param frame table
--- @return string
function Infobox.fromArgs( frame )
    local arguments = require( 'Module:Arguments' ).getArgs( frame )
    local options, args = parseArgs( arguments )

    if options.bodyClass == nil then
        options.bodyClass = 'floatright'
    end

    local box = Infobox.create( options )

    box:addImage( arguments.image,{
        'frameless',
        '300px'
    } )

    if arguments.image ~= nil then
        arguments.image = nil
    end

    box:addRowsFromArgs( args )

    return tostring( box )
end

return Infobox