Module:TemplateBox

From FIThydrowiki
Jump to navigation Jump to search

--[[

   @exports
       usagesample( frame )
       argcount( frame )
       args2table( args, onGetKey, forCustom )
       paramtable( frame )
       description( frame )
       templatedata( frame )

]]

local p = {}

-- Helper function, not exposed function tobool(st)

   if type( st ) == 'string' then
       return st == 'true'
   else
       return not not st
   end

end


-- Required to determine in which languages the interface texts without langcode are local contentLangcode = mw.language.getContentLanguage():getCode() -- Forward declaration local msg, langIsInit, userLang local messagePrefix = "templatedata-doc-" local i18n = {} i18n['params'] = "Template parameters" i18n['param-name'] = "Parameter" i18n['param-desc'] = "Description" i18n['param-type'] = "Type" i18n['param-default'] = "Default" i18n['param-status'] = "Status" i18n['param-status-optional'] = "optional" i18n['param-status-required'] = "required" i18n['param-status-suggested'] = "suggested" i18n['param-status-deprecated'] = "deprecated" i18n['param-default-empty'] = "empty"

function initLangModule()

   if langIsInit then
       return
   end
   --! From de:Modul:Expr; by de:User:PerfektesChaos; 
   --! Derivative work: Rillke
   userLang = mw.getCurrentFrame():preprocess( '⧼lang⧽' )
   msg = function( key )
       -- Retrieve localized message string in content language
       -- Precondition:
       --     key  -- string; message ID
       -- Postcondition:
       --     Return some message string
       -- Uses:
       --     >  messagePrefix
       --     >  i18n
       --     >  userLang
       --     mw.message.new()
       local m = mw.message.new( messagePrefix .. key )
       local r = false
       if m:isBlank() then
           r = i18n[ key ]
       else
           m:inLanguage( userLang )
           r = m:plain()
       end
       if not r then
           r = '((('.. key .. ')))'
       end
       return r
   end -- msg()
   
   langIsInit = true

end

-- A "hash" / table of everything TemplateData takes -- to ease maintenance.

-- The type is automatically determined if t is omitted. -- If the type does not match or can't be converted, an error will be thrown! -- Available types (LUA-Types with exceptions): -- InterfaceText, boolean, number, selection, table, string -- selection*: - requires a selection-string of pipe-separated possibilities to be supplied -- InterfaceText*: A free-form string (no wikitext) in the content-language of the wiki, or, -- an object containing those strings keyed by language code. local paraminfoTemplate = {

   description = {
       default = ,
       t = 'InterfaceText',
       alias = 'desc'
   }

} local paraminfoTLParams = {

   label = {
       default = ,
       t = 'InterfaceText'
   },
   required = {
       default = false,
       extract = function(pargs, number, paramVal)
           local req = (pargs[number .. 'stat'] == 'required')
           return tobool( paramVal or req )
       end
   },
   suggested = {
       default = false,
       extract = function(pargs, number, paramVal)
           local sugg = (pargs[number .. 'stat'] == 'suggested')
           return tobool( paramVal or sugg )
       end
   },
   description = {
       default = ,
       t = 'InterfaceText',
       alias = 'd'
   },
   deprecated = {
       default = false,
       extract = function(pargs, number, paramVal)
           local depr = (pargs[number .. 'stat'] == 'deprecated')
           return tobool( paramVal or depr )
       end
   },
   aliases = {
       default = ,
       t = 'table',
       extract = function(pargs, number, paramVal)
           local key = number .. 'aliases'
           local tdkey = key .. '-td'
           local aliases = pargs[tdkey] or pargs[key]
           if aliases then
               aliases = mw.text.split( aliases, '/', true )
           end
           return aliases
       end
   },
   default = {
       default = ,
       t = 'string',
       alias = 'def'
   },
   type = {
       default = 'unknown',
       t = 'selection',
       selection = 'unknown|number|string|string/wiki-user-name|string/wiki-page-name|string/line|line|wiki-page-name|wiki-file-name|wiki-user-name|content|unbalanced-wikitext'
   },
   inherits = {
       default = nil,
       t = 'string'
   }
   -- sets will be treated differently because we can only have a plain structure in wikitext

} local tableLayout = {

   {
       col = 'param-name',
       width = '15%',
       extract = function(item, renderCell, monolingual)
           local alias, param = , item.key
           local aliasTT = ''
           param = '' .. param .. ''
           if item.aliases then
               alias = aliasTT .. table.concat(item.aliases, '
' .. aliasTT) .. ''

param = table.concat({param, '

', alias, '

'})

           end
           renderCell(param, colspan)
       end
   },  {
       col = 'param-desc',
       cols = 2,
       width = '65%',
       extract = function(item, renderCell, monolingual)
           local label = item.label or 
           label = monolingual(label)
           local labelLen = #label
           local colspan = 2 - labelLen
       
           if labelLen > 0 then
               renderCell(label)
           end
       
           renderCell(monolingual(item.description), colspan)
       end
   },  {
       col = 'param-default',
       width = '10%',
       extract = function(item, renderCell, monolingual)
           local def = monolingual(item.default) or 
           if #def == 0 then
               def = '' .. msg('param-default-empty') .. ''
           end
           renderCell(def)
       end
   },  {
       col = 'param-status',
       width = '10%',
       extract = function(item, renderCell, monolingual)
           local stat = msg('param-status-optional')
           if item.required then
               stat = '' .. msg('param-status-required') .. ''
           elseif item.deprecated then
               stat = msg('param-status-deprecated')
           elseif item.suggested then
               stat = msg('param-status-suggested')
           end
           renderCell(stat)
       end
   }

}

-- Initialize param info -- Avoids having to add redundant information to the preceding tables function init( which )

   local setDefault = function(v)
       if v.t == nil and v.default ~= nil then
           v.t = type( v.default )
       end
       if v.selection then
           v.selection = '|' .. v.selection .. '|'
       end
   end
   for a, v in pairs( which ) do
       setDefault(v)
   end

end function initParamTables()

   init( paraminfoTemplate )
   init( paraminfoTLParams )

end



USAGE PART ----------------------


function p.argcount( frame )

   local pargs = ( frame:getParent() or {} ).args or {}
   local ac = 0
   for i, arg in pairs( pargs ) do
       if ('number' == type(i)) then
           ac = ac + 1
       end
   end
   return ac

end

function p.usagesample( frame )

   local pargs = ( frame:getParent() or {} ).args or {}
   local multiline = (pargs.lines == 'multi' or pargs.print == 'multi' or pargs.print == 'infobox')
   local align = pargs.print == 'infobox'
   if not pargs.lines and not pargs.print and pargs.type == 'infobox' then
       multiline = true
       align = true
   end
   local sepStart = ' |'
   local sepEnd = multiline  and '\n' or 
   local sep = sepEnd
   local subst = #(pargs.mustbesubst or ) > 0 and 'subst:' or 
   local beforeEqual = multiline  and ' ' or 
   local equal = beforeEqual .. '= '
   local templateTitle = pargs.name or 
   local args, argName, result = {}
   local maxArgLen, eachArg = 0
   sep = sep .. sepStart
   
   local comapareLegacyVal = function(val)
       return val == 'optional-' or val == 'deprecated'
   end
   local shouldShow = function(i)
       if comapareLegacyVal(pargs[i .. 'stat']) or
           comapareLegacyVal(pargs[i .. 'stat-td']) or
           pargs[i .. 'deprecated'] == true then 
               return false
           end
       return true
   end
   
   eachArg = function(cb)
       for i, arg in pairs( pargs ) do
           if ('number' == type(i)) then
               argName = mw.text.trim( arg or  )
               if #argName == 0 then
                   argName = tostring(i)
               end
               
               if shouldShow(i) then
                   cb(argName)
               end
           end
       end
   end
   
   if align then
       eachArg(function( arg )
           local argL = #arg
           maxArgLen = argL > maxArgLen and argL or maxArgLen
       end)
   end
   
   eachArg(function( arg )
       local space = 
       if align then
           space = (' '):rep(maxArgLen - #arg)
       end
       table.insert( args, argName .. space .. equal )
   end)
   
   if #args == 0 then
       sep = 
       sepEnd = 
       sepStart = 
   end
   if #templateTitle == 0 then
       templateTitle = mw.title.getCurrentTitle().text
   end
   result = table.concat( args, sep )
   result = table.concat({ mw.text.nowiki('Template:'), subst, templateTitle, sep, result, sepEnd, '' })
   if multiline then
       -- Preserve whitespace in front of new lines
       result = frame:callParserFunction{ name = '#tag', args = { 'poem', result } }
   end
   return result

end



GENERAL PART ---------------------


function p.args2table(args, onGetKey, consumer)

   initParamTables()
   
   local sets, asParamArray, laxtype, processParams, processDesc
   if 'paramtable' == consumer then
       asParamArray = true
       processParams = true
       laxtype = true
   elseif 'templatedata' == consumer then
       sets = true
       processParams = true
       processDesc = true
       unstrip = true
   elseif 'description' == consumer then
       processDesc = true
       laxtype = true
   end
   -- All kind of strange stuff with the arguments is done, so play safe and make a copy
   local pargs = mw.clone( args )
   -- Array-like table containing all parameter-numbers that were passed
   local templateArgs = {}
   -- Arguments that are localized (i.e. the user passed  1desc-en=English description of parameter one)
   local i18nTemplateArgs = {}
   -- Ensure that tables end up as array/object (esp. when they are empty)
   local tdata = {description="", params={}, sets={}}
   local isArray  = { __tostring = function() return "JSON array"  end }    isArray.__index  = isArray
   setmetatable(tdata.sets, isArray)
   onGetKey = onGetKey or function( prefix, alias, param )
       local key, key2, tdkey, tdkey2
       key = prefix .. (alias or param)
       key2 = prefix .. param
       tdkey = key .. '-td'
       tdkey2 = key2 .. '-td'
       return tdkey, tdkey2, key, key2
   end
   
   local extractData = function( pi, number )
       local prefix = number or 
       local ppv, paramVal
       local key1, key2, key3, key4
       local paramKey, paramTable, processKey
       if number then
           paramKey = mw.text.trim( pargs[number] )
           if  == paramKey then
               paramKey = tostring( number )
           end
           
           paramTable = {}
           if asParamArray then
               paramTable.key = paramKey
               table.insert(tdata.params, paramTable)
           else
               tdata.params[paramKey] = paramTable
           end
       end
       for p, info in pairs( pi ) do
           key1, key2, key3, key4 = onGetKey(prefix, info.alias, p)
           paramVal = nil
           
           processKey = function(key)
               if paramVal ~= nil then return end
               local plain, multilingual = pargs[key], i18nTemplateArgs[key]
               paramVal = multilingual or plain
           end
           processKey( key1 )
           processKey( key2 )
           processKey( key3 )
           processKey( key4 )
           
           -- Ensure presence of entry in content language
           ppv = pargs[key1] or pargs[key2] or pargs[key3] or pargs[key4] or info.default
           if 'table' == type( paramVal ) then
               if (nil == paramVal[contentLangcode]) then
                   paramVal[contentLangcode] = ppv
               end
           else
               paramVal = ppv
           end
           if 'function' == type( info.extract ) then
               if 'string' == type( paramVal ) then
                   paramVal = mw.text.trim( paramVal )
                   if  == paramVal then
                       paramVal = nil
                   end
               end
               paramVal = info.extract( pargs, number, paramVal )
           end
           
           local insertValue = function()
               if number then
                   paramTable[p] = paramVal
               else
                   tdata[p] = paramVal
               end
           end
           
           if info.selection then
               if info.selection:find( paramVal, 1, true ) then
                   insertValue()
               end
           elseif 'InterfaceText' == info.t then
               if ({ table=1, string=1 })[type( paramVal )] then
                   insertValue()
               end
           else
               local paramType = type( paramVal )
               if 'string' == info.t and 'string' == paramType then
                   paramVal = mw.text.trim( paramVal )
                   if  ~= paramVal then
                       insertValue()
                   end
               elseif 'boolean' == info.t then
                   paramVal = tobool(paramVal)
                   insertValue()
               elseif 'number' == info.t then
                   paramVal = tonumber(paramVal)
                   insertValue()
               elseif paramType == info.t then
                   insertValue()
               elseif paramType == 'nil' then
                   -- Do nothing
               elseif not laxtype and 'string' == info.t and 'table' == paramType then
                   -- Convert multilingual object into content language string
                   paramVal = paramVal[contentLangcode]
                   insertValue()
               else
                   if laxtype then
                       insertValue()
                   else
                       error( p .. ': Is of type ' ..  paramType .. ' but should be of type ' .. (info.t or 'unknown'), 1 )
                   end
               end
           end
       end
       -- Now, treat sets
       if sets then
           key1 = prefix .. 'set-td'
           key2 = prefix .. 'set'
           paramVal = pargs[key1] or pargs[key2]
           if paramVal then
               local found = false
               for i, s in ipairs( tdata.sets ) do
                   if s.label == paramVal then
                       table.insert( s.params, p )
                       found = true
                   end
               end
               if not found then
                   table.insert( tdata.sets, {
                       label = paramVal, 
                       params = { p }
                   } )
               end
           end
       end
   end
   
   -- First, analyse the structure of the provided arguments
   for a, v in pairs( pargs ) do
       if unstrip then
           v = mw.text.unstrip( v )
           pargs[a] = v
       end
       if type( a ) == 'number' then
           table.insert( templateArgs, a )
       else
           local argSplit = mw.text.split( a, '-', true )
           local argUnitl = {}
           local argAfter = {}
           local isTDArg = false
           local containsTD = a:find( '-td', 1, true )
           for i, part in ipairs( argSplit ) do
               if isTDArg or (containsTD == nil and i > 1) then
                   -- This is likely a language version
                   table.insert( argAfter, part )
               else
                   table.insert( argUnitl, part )
               end
               if part == 'td' then
                   isTDArg = true
               end
           end
           if #argAfter > 0 then
               argUnitl = table.concat( argUnitl, '-' )
               argAfter = table.concat( argAfter, '-' )
               i18nTemplateArgs[argUnitl] = i18nTemplateArgs[argUnitl] or {}
               i18nTemplateArgs[argUnitl][argAfter] = v
           end
       end
   end
   -- Then, start building the actual template
   if processDesc then
       extractData( paraminfoTemplate )
   end
   if processParams then
       for i, number in pairs( templateArgs ) do
           extractData( paraminfoTLParams, number )
       end
   end
   return tdata, #templateArgs

end




CUSTOM PARAMETER TABLE PART -------------


-- A custom key-pref-function local customOnGetKey = function( prefix, alias, param )

   local key, key2, tdkey, tdkey2
   key = prefix .. (alias or param)
   key2 = prefix .. param
   tdkey = key .. '-td'
   tdkey2 = key2 .. '-td'
   return key2, key, tdkey2, tdkey

end local toUserLanguage = function(input, frame)

   if type(input) == 'table' then
       input = frame:expandTemplate{ title = 'LangSwitch', args = input }
   end
   return input

end

function p.description(frame)

   local pargs = ( frame:getParent() or {} ).args or {}
   local tdata, paramLen
   local monolingual = function(input)
       return toUserLanguage(input, frame)
   end
   tdata, paramLen = p.args2table(pargs, customOnGetKey, 'description')
   return monolingual(tdata.description)

end


function p.paramtable(frame)

   local pargs = ( frame:getParent() or {} ).args or {}
   local tdata, paramLen
   
   if 'only' == pargs.useTemplateData then
       return 'param table - output suppressed'
   end
   
   -- Initialize the language-related stuff
   initLangModule()
   local monolingual = function(input)
       return toUserLanguage(input, frame)
   end
   tdata, paramLen = p.args2table(pargs, customOnGetKey, 'paramtable')
   
   
   if 0 == paramLen then
       return 
   end
   
   local row, rows = , {}
   local renderCell = function(wikitext, colspan)

local colspan, oTd = colspan or 1, ''

       if colspan > 1 then

oTd = ''

       end

row = table.concat({ row, oTd, wikitext, '' }) end -- Create the header for i, field in ipairs( tableLayout ) do local style = ' style="width:' .. field.width .. '"' local colspan = if field.cols then colspan = ' colspan="' .. field.cols .. '"' end local th = '<th' .. style .. colspan .. '>' row = row .. th .. msg(field.col) .. '' end table.insert(rows, row) -- Now transform the Lua-table into an HTML-table for i, item in ipairs( tdata.params ) do row = for i2, field in ipairs( tableLayout ) do field.extract(item, renderCell, monolingual) end table.insert(rows, row) end return '

' .. table.concat(rows, '') .. '

'

end




TEMPLATEDATA PART ------------------


-- A real parser/transformer would look differently but it would likely be much more complex -- The TemplateData-portion for Template:TemplateBox function p.templatedata(frame)

   local tdata
   local args = frame.args or {}
   local formatting = args.formatting
   local pargs = ( frame:getParent() or {} ).args or {}
   local useTemplateData = pargs.useTemplateData
   if  (formatting == 'pretty' and useTemplateData ~= 'export') or
       (not useTemplateData) or
       (useTemplateData == 'export' and formatting ~= 'pretty') then
           local warning = "Warning: Module:TemplateBox - templatedata invoked but not requested by user (setting useTemplateData=1)."
           mw.log(warning)
           tdata = '{"description":"' .. warning .. '","params":{},"sets":[]}'
           return tdata
   end
   
   -- Load the JSON-Module which will convert LUA tables into valid JSON
   local JSON = require('Module:JSON')
   JSON.strictTypes = true
   -- Obtain the object containing info
   tdata = p.args2table(pargs, nil, 'templatedata')
   -- And finally return the result
   if formatting == 'pretty' then
       return JSON:encode_pretty(tdata)
   else
       return JSON:encode(tdata)
   end

end

return p