Jump to content
Toggle menu
  • 8 articles
  • 75 files
  • 7 users
  • 37.8K edits
Neodyland Wiki
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:TemplateBox: Difference between revisions

From Neodyland Wiki
m Documentation
m copy from sandbox
Line 1: Line 1:
--[[
    @exports
        args2table( args, onGetKey, sets )
        paramtable( frame )
        templatedata( frame )
]]
local p = {}
local p = {}


Line 11: Line 18:




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-optional'] = "optional"
i18n['param-required'] = "required"
i18n['param-deprecated'] = "deprecated"
i18n['param-empty'] = "empty"


--! From [[:de:Modul:Expr]]; by [[:de:User:PerfektesChaos]];
--! Derivative work: Rillke
local userLang = mw.message.newRawMessage( '{{int:lang}}' ):parse()
-- Required to determine in which languages the interface texts without langcode are
local contentLangcode = mw.language.getContentLanguage():getCode()
local function msg( 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()


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


-- A "hash" / table of everything TemplateData takes
-- A "hash" / table of everything TemplateData takes
Line 80: Line 125:
         required = false,
         required = false,
         default = '',
         default = '',
         t = 'InterfaceText',
         t = 'string',
         alias = 'def'
         alias = 'def'
     },
     },
Line 95: Line 140:
     }
     }
     -- sets will be treated differently because we can only have a plain structure in wikitext
     -- sets will be treated differently because we can only have a plain structure in wikitext
}
local tableLayout = {
    {
        col = 'param-name',
        cols = 2,
        width = '20%',
        extract = function(item, renderCell)
            local label, alias, param = item.label, "", item.key
            if #label == 0 then
                label = param
            end
            renderCell(label)
            param = '<code>' .. param .. '</code>'
            if item.aliases then
                alias = '<tt>' .. table.concat(alias, '</tt>, <tt>') .. '</tt>'
                param = param .. '<br /><span style="mw-templatedata-doc-param-alias">' .. alias .. '</span>'
            end
            renderCell(param)
        end
    },  {
        col = 'param-desc',
        width = '60%',
        extract = function(item, renderCell)
            renderCell(item.description)
        end
    },  {
        col = 'param-default',
        width = '10%',
        extract = function(item, renderCell)
            local def = item.default or ''
            if #def == 0 then
                def = '<span class="mw-templatedata-doc-muted" style="color:#777; font-variant:small-caps">' .. msg('param-empty') .. '</span>'
            end
            renderCell(def)
        end
    },  {
        col = 'param-status',
        width = '10%',
        extract = function(item, renderCell)
            local stat = msg('param-optional')
            if item.required then
                stat = msg('param-required')
            elseif item.deprecated then
                stat = msg('param-deprecated')
            end
            renderCell(stat)
        end
    }
}
}


Line 115: Line 208:
init( paraminfoTLParams )
init( paraminfoTLParams )


-- A real parser/transformer would look differently but it would likely be much more complex
------------------------------------------------------
-- The TemplateData-portion for [[Template:TemplateBox]]
------------------- GENERAL PART ---------------------
function p.templatedata(frame)
------------------------------------------------------
    -- Load the JSON-Module which will convert LUA tables into valid JSON
function p.args2table(args, onGetKey, sets, asParamArray)
    local JSON = require('Module:JSON')
     -- All kind of strange stuff with the arguments is done, so play safe and make a copy
     -- All kind of strange stuff with the arguments is done, so play safe and make a copy
     local pargs = mw.clone( ( frame:getParent() or {} ).args or {} )
     local pargs = mw.clone( args )
     -- Array-like table containing all parameter-numbers that were passed
     -- Array-like table containing all parameter-numbers that were passed
     local templateArgs = {}
     local templateArgs = {}
     -- Arguments that are localized (i.e. the user passed  1desc-en=English description of parameter one)
     -- Arguments that are localized (i.e. the user passed  1desc-en=English description of parameter one)
     local i18nTemplateArgs = {}
     local i18nTemplateArgs = {}
    -- Required to determine in which languages the interface texts without langcode are
    local contentLangcode = mw.language.getContentLanguage():getCode()
     -- Ensure that tables end up as array/object (esp. when they are empty)
     -- Ensure that tables end up as array/object (esp. when they are empty)
    JSON.strictTypes = true
     local tdata = {description="", params={}, sets={}}
     local tdata = JSON:decode('{"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 extractData = function( pi, number )
         local prefix = number or ''
         local prefix = number or ''
         local key, key2, tdkey, tdkey2, ppv, paramVal
         local ppv, paramVal
        local key1, key2, key3, key4
         local paramKey, paramTable
         local paramKey, paramTable
         if number then
         if number then
Line 141: Line 241:
                 paramKey = tostring( number )
                 paramKey = tostring( number )
             end
             end
             tdata.params[paramKey] = {}
              
             paramTable = tdata.params[paramKey]
            paramTable = {}
             if asParamArray then
                paramTable.key = paramKey
                table.insert(tdata.params, paramTable)
            else
                tdata.params[paramKey] = paramTable
            end
         end
         end
         for p, info in pairs( pi ) do
         for p, info in pairs( pi ) do
             key = prefix .. (info.alias or p)
             key1, key2, key3, key4 = onGetKey(prefix, info.alias, p)
            key2 = prefix .. p
             ppv = pargs[key1] or pargs[key2] or pargs[key3] or pargs[key4] or info.default
            tdkey = key .. '-td'
            tdkey2 = key2 .. '-td'
             ppv = pargs[tdkey] or pargs[tdkey2] or pargs[key] or pargs[key2] or info.default
              
              
             -- Do we have a multilingual item?
             -- Do we have a multilingual item?
             paramVal = i18nTemplateArgs[tdkey] or i18nTemplateArgs[tdkey2] or i18nTemplateArgs[key] or i18nTemplateArgs[key2]
             paramVal = i18nTemplateArgs[key1] or i18nTemplateArgs[key2] or i18nTemplateArgs[key3] or i18nTemplateArgs[key4]
             if 'table' == type( paramVal ) then
             if 'table' == type( paramVal ) then
                 if (nil == paramVal[contentLangcode]) then
                 if (nil == paramVal[contentLangcode]) then
Line 210: Line 313:
         end
         end
         -- Now, treat sets
         -- Now, treat sets
         key = prefix .. 'set'
         if sets then
        tdkey = prefix .. 'set-td'
            key1 = prefix .. 'set-td'
        paramVal = pargs[tdkey] or pargs[key]
            key2 = prefix .. 'set'
        if paramVal then
            paramVal = pargs[key1] or pargs[key2]
            local found = false
            if paramVal then
            for i, s in ipairs( tdata.sets ) do
                local found = false
                if s.label == paramVal then
                for i, s in ipairs( tdata.sets ) do
                    table.insert( s.params, p )
                    if s.label == paramVal then
                     found = true
                        table.insert( s.params, p )
                        found = true
                     end
                end
                if not found then
                    table.insert( tdata.sets, {
                        label = paramVal,
                        params = { p }
                    } )
                 end
                 end
            end
            if not found then
                table.insert( tdata.sets, {
                    label = paramVal,
                    params = { p }
                } )
             end
             end
         end
         end
Line 234: Line 339:
         mw.log(warning)
         mw.log(warning)
         tdata.description = warning
         tdata.description = warning
         return JSON:encode(tdata);
         return tdata, 0;
     end
     end
      
      
Line 267: Line 372:
     end
     end
     -- Then, start building the actual template
     -- Then, start building the actual template
     extractData( paraminfoTemplate )
     if not asParamArray then
        extractData( paraminfoTemplate )
    end
     for i, number in pairs( templateArgs ) do
     for i, number in pairs( templateArgs ) do
         extractData( paraminfoTLParams, number )
         extractData( paraminfoTLParams, number )
     end
     end
    return tdata, #templateArgs
end
------------------------------------------------------
------------ CUSTOM PARAMETER TABLE PART -------------
------------------------------------------------------
function p.paramtable(frame)
    local pargs = ( frame:getParent() or {} ).args or {}
    local onGetKey, tdata, paramLen
   
    if 'only' == pargs.useTemplateData then
        return 'param table - output suppressed'
    end
    -- A custom key-pref-function
    onGetKey = 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
    tdata, paramLen = p.args2table(pargs, onGetKey, false, true)
   
   
    if 0 == paramLen then
        return 'noparams'
    end
   
    local row, rows = '', {}
    local renderCell = function(wikitext)
        if type(wikitext) == 'table' then
            wikitext = frame:expandTemplate{ title = 'LangSwitch', args = wikitext }
        end
        row = row .. '<td>' .. wikitext .. '</td>'
    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) .. '</th>'
    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)
        end
        table.insert(rows, row)
    end
    return '<table class="wikitable templatebox-table"><tr>' .. table.concat(rows, '</tr><tr>') .. '</tr></table>'
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)
    -- 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
    local tdata = p.args2table(( frame:getParent() or {} ).args or {}, nil, true)
     -- And finally return the result
     -- And finally return the result
     return JSON:encode(tdata);
     return JSON:encode(tdata)
end
end


return p
return p

Revision as of 11:35, 16 August 2013

Module documentation[ create · purge ]
This documentation is transcluded from Module:TemplateBox/doc.
--[[
    @exports
        args2table( args, onGetKey, sets )
        paramtable( 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


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-optional'] = "optional"
i18n['param-required'] = "required"
i18n['param-deprecated'] = "deprecated"
i18n['param-empty'] = "empty"

--! From [[:de:Modul:Expr]]; by [[:de:User:PerfektesChaos]]; 
--! Derivative work: Rillke
local userLang = mw.message.newRawMessage( '{{int:lang}}' ):parse()
-- Required to determine in which languages the interface texts without langcode are
local contentLangcode = mw.language.getContentLanguage():getCode()
local function msg( 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()


-- 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 = {
        required = true,
        default = '',
        t = 'InterfaceText',
        alias = 'desc'
    }
}
local paraminfoTLParams = {
    label = {
        required = false,
        default = '',
        t = 'InterfaceText'
    },
    required = {
        required = true,
        default = false,
        extract = function(pargs, number, paramVal)
            local req = (pargs[number .. 'stat'] == 'required')
            return tobool( paramVal or req )
        end
    },
    description = {
        required = false,
        default = '',
        t = 'InterfaceText',
        alias = 'd'
    },
    deprecated = {
        required = true,
        default = false,
        extract = function(pargs, number, paramVal)
            local depr = (pargs[number .. 'stat'] == 'deprecated')
            return tobool( paramVal or depr )
        end
    },
    aliases = {
        required = false,
        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 = {
        required = false,
        default = '',
        t = 'string',
        alias = 'def'
    },
    type = {
        required = false,
        default = 'unknown',
        t = 'selection',
        selection = 'unknown|number|string|string/wiki-user-name|string/wiki-page-name|string/line'
    },
    inherits = {
        required = false,
        default = nil,
        t = 'string'
    }
    -- sets will be treated differently because we can only have a plain structure in wikitext
}
local tableLayout = {
    {
        col = 'param-name',
        cols = 2,
        width = '20%',
        extract = function(item, renderCell)
            local label, alias, param = item.label, "", item.key
            if #label == 0 then
                label = param
            end
            renderCell(label)
            param = '<code>' .. param .. '</code>'
            if item.aliases then
                alias = '<tt>' .. table.concat(alias, '</tt>, <tt>') .. '</tt>'
                param = param .. '<br /><span style="mw-templatedata-doc-param-alias">' .. alias .. '</span>'
            end
            renderCell(param)
        end
    },  {
        col = 'param-desc',
        width = '60%',
        extract = function(item, renderCell)
            renderCell(item.description)
        end
    },  {
        col = 'param-default',
        width = '10%',
        extract = function(item, renderCell)
            local def = item.default or ''
            if #def == 0 then
                def = '<span class="mw-templatedata-doc-muted" style="color:#777; font-variant:small-caps">' .. msg('param-empty') .. '</span>'
            end
            renderCell(def)
        end
    },  {
        col = 'param-status',
        width = '10%',
        extract = function(item, renderCell)
            local stat = msg('param-optional')
            if item.required then
                stat = msg('param-required')
            elseif item.deprecated then
                stat = msg('param-deprecated')
            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
init( paraminfoTemplate )
init( paraminfoTLParams )

------------------------------------------------------
------------------- GENERAL PART ---------------------
------------------------------------------------------
function p.args2table(args, onGetKey, sets, asParamArray)
    -- 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
        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)
            ppv = pargs[key1] or pargs[key2] or pargs[key3] or pargs[key4] or info.default
            
            -- Do we have a multilingual item?
            paramVal = i18nTemplateArgs[key1] or i18nTemplateArgs[key2] or i18nTemplateArgs[key3] or i18nTemplateArgs[key4]
            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
                else
                    error( p .. ': Is of type ' ..  paramType .. ' but should be of type ' .. (info.t or 'unknown'), 1 )
                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
    
    if not pargs.useTemplateData then
        local warning = "Warning: Module:TemplateBox - templatedata invoked but not requested by user (setting useTemplateData=1)."
        mw.log(warning)
        tdata.description = warning
        return tdata, 0;
    end
    
    -- First, analyse the structure of the provided arguments
    for a, v in pairs( pargs ) do
        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 not asParamArray then
        extractData( paraminfoTemplate )
    end
    for i, number in pairs( templateArgs ) do
        extractData( paraminfoTLParams, number )
    end
    return tdata, #templateArgs
end



------------------------------------------------------
------------ CUSTOM PARAMETER TABLE PART -------------
------------------------------------------------------
function p.paramtable(frame)
    local pargs = ( frame:getParent() or {} ).args or {}
    local onGetKey, tdata, paramLen
    
    if 'only' == pargs.useTemplateData then
        return 'param table - output suppressed'
    end
    -- A custom key-pref-function
    onGetKey = 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
    tdata, paramLen = p.args2table(pargs, onGetKey, false, true)
    
    
    if 0 == paramLen then
        return 'noparams'
    end
    
    local row, rows = '', {}
    local renderCell = function(wikitext)
        if type(wikitext) == 'table' then
            wikitext = frame:expandTemplate{ title = 'LangSwitch', args = wikitext }
        end
        row = row .. '<td>' .. wikitext .. '</td>'
    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) .. '</th>'
    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)
        end
        table.insert(rows, row)
    end
    return '<table class="wikitable templatebox-table"><tr>' .. table.concat(rows, '</tr><tr>') .. '</tr></table>'
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)
    -- 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
    local tdata = p.args2table(( frame:getParent() or {} ).args or {}, nil, true)
    -- And finally return the result
    return JSON:encode(tdata)
end

return p
Cookies help us deliver our services. By using our services, you agree to our use of cookies.