×
Create a new article
Write your page title here:
We currently have 230 articles on Farthest Frontier. Type your article name above or create one of the articles listed here!



    Farthest Frontier

    Module:Format TemplateData

    Format TemplateData' – Module with auxilary functions for template documentation, especially by TemplateData.

    Core functionality is improved presentation on documentation pages.

    Improve template documentation page – MediaWiki disappointing

    For presentation of template depiction in VisualEditor agreement was made to abandon all markup and clickable links, permitting all tooltips in all environments. Basically this is reasonable, albeit tooltips with markup and clickable links are supported as HTML application for decades now and JavaScript is present anyway when VisualEditor is used.

    • In consequence it was decided, that also presentation on template documentation views never ever is permitted to contain effective links or markup.
    • That involved, that on many template documentation pages two separated parameter documentation tables are needed and required to be maintained simultaneously: One plain text version for VisualEditor, and a useful one for complex circumstances, with links and markup and lists and tables. – BTW, VisualEditor has not only tooltips, but also a static GUI view, where the lack of deepening links in parameter description is painful.

    This state is indefensible.

    Improved presentation

    In addition to the simple syntax supported by MediaWiki and presented in the VisualEditor, the following features can be added to the JSON code for the template documentation page. They affect the elements classified as InterfaceText, but are only useful for description fields.

    Wikilinks (internal format)

    • Using double square brackets pages can be linked as common.
    • In VisualEditor, only link title is visible, as it is displayed otherwise.

    External links (URL format)

    • Open URL are linked as usual by themselves. In VisualEditor they appear as normal text.
    • External links enclosed in simple square brackets are displayed normally on the template documentation page. In VisualEditor the title is omitted and the URL is displayed so that the users can c&p it and transfer it to the address field of the browser. There is no other way.

    Apostrophs ' for italic and bold

    • They can be used to emphasize on the documentation page and are missing in VisualEditor (regular script).

    HTML entities

    • The following entities can be used: < > & "   and all numeric formats.

    HTML tags

    • HTML tags (and the MediaWiki elements that are not replaced in advance) are removed for the VisualEditor. Otherwise, they remain effective.
    • Attributes are often included in ", which conflicts with the JSON syntax. It is important to make sure that ' is used, which can be a problem with template transclusions.

    <noexport></noexport>

    • The enclosed areas are not exported to the VisualEditor.
    • More complex wiki syntax and extensive explanations can be restricted to the documentation page.
    • Within a noexport area, the line structure of the source text is considered. Otherwise everything is running in a single line, as it would also be represented in the VisualEditor.

    Templates

    • In particular when the template parameter JSON= is used, templates can be distributed anywhere in the JSON code. However, the expanded syntax might collide with the JSON syntax.

    More effects

    • According to the state (required, suggested, optional, deprecated) the table rows are highlighted in light blue, white, gray and pale red.
    • When sorting by state, this importance is taken into account and not the alphabetical sequence of the keywords.
    • Each parameter can be addressed as a jump destination. The fragment is #templatedata:parameter-name.
    • Missing labels are highlighted as errors.
    • A maintenance category is triggered if errors occur.
    • If there are no parameters, the element params:{} is not required.

    Eliminate disadvantages

    Two aspects were found to be particularly disturbing in 2013–2017:

    1. Even if no parameters at all were defined, a table head is always displayed for a table without content. Even more, this is sortable.
      • A reduction was rejected with Template:Phab. A sortable table of the parameters would be always necessary, even if the table has no rows at all and consists only of the header row.
      • This ridiculous statement led to the development of this module in 2016.
    2. Even if the context does not permit that default values or even AutoValue specifications will be defined ever, a content-free six-line definition list is output for each individual parameter value.

    The general comments show that MediaWiki only regards the presentation of TemplateData specifications in the VisualEditor as important. However, someone has to program and maintain the templates and someone needs to generate the template description and make it manageable beside the functionality in the VisualEditor form, but that is beyond ken.

    • Two years later the relatively easy task Template:Phab has been solved by a community originated patch.

    General workflow

    • An attempt is made to read the JSON object (string) from passed template parameters.
    • If this failed, the source code of the current and the documentation page is searched for <templatedata> elements.
    • Two representations are obtained from the JSON object input:
      1. A localized version, markup etc. stripped off, in JSON format.
      2. An HTML structure, basically similar to the MediaWiki representation, possibly with table of the parameters, with enhanced features.
    • The result of the template is a visible documentation with markup, followed by a hidden <templatedata> element. This is done for the export and corresponds to the MediaWiki guidelines.
      • If current page has been identified as documentation page the hidden <templatedata> is suppressed, and those pages do not appear separately in Special:PagesWithProp/templatedata.

    Functions for templates

    Details

    f
    Improve TemplateData-presentation; used in Template:Format TemplateData
    Parameters of template transclusion environment (all optional):
    1
    JSON string or <templatedata> object
    JSON
    JSON string
    (precedes 1)
    Transition from <templatedata> objects with pipe symbols needs special attention: Pipes are to be represented as {{!}}, on double curly brackets one should be encoded by HTML entity.
    TOC
    1 – Insert table of contents after general purpose descriptions; but before parameter list, if present
    Example
    lang
    Language code according to ISO 639 etc.
    lazy
    1 – Presentation only, do not generate an effective data block
    For general method descriptions.
    debug
    1 – developer mode
    source
    1 – show effective JSON source text (after template expansion) for debugging
    Parameters of #invoke for particular project adaption (all optional):
    lang
    Language code according to ISO 639 etc.
    debug
    Development mode, if provided and not equal 0
    cat
    Title of a maintenance category on invalid parameter value etc.
    Deprecated – use configuration module
    docpageCreate
    Pattern for creation of subpage names; %s/Doku
    Deprecated – use configuration module
    docpageDetect
    Pattern for recognition of subpage names; /Doku$
    Deprecated – use configuration module
    msgDescMiss
    Localisation: complaint text on missing description
    Deprecated – use configuration module
    Returns: HTML code; and/or error message, probably with class="error"
    failsafe
    Version management

    Functions for Lua modules (API)

    Some functions described above can be used by other modules:

    local lucky, TemplateData = pcall( require, "Modul:Format TemplateData" )
    if type( TemplateData ) == "table" then
        TemplateData = TemplateData.TemplateData()
    else
        -- failure; TemplateData is the error message
        return "<span class='error'>" .. TemplateData .. "</span>"
    end
    
    TemplateData.failsafe(atleast)
    1. atleast
      optional
      nil or minimal version request or "wikidata"
    Returns: string or false
    TemplateData.getPlainJSON(adapt)
    Reduce enhanced JSON information to MediaWiki JSON
    1. adapt
      string, with JSON (enhanced)
    Returns: string, with JSON (MediaWiki )
    TemplateData.test(adapt, arglist)
    Simulation of template functionality
    1. adapt
      table, #invoke parameters
    2. arglist
      table, template parameters
    Returns: string

    Usage

    Currently focussing on one template only:

    Dependencies

    Configuration

    A local module Module:Format TemplateData/config, if present, facilitates adaptions to the local project.

    A table is expected via mw.loadData. The following entries are optional components:

    catProblem
    Title of a maintenance category on invalid parameter value etc.
    classNoNumTOC
    Name of class for the table of contents; especially to suppress numbering.
    nonumtoc
    classTable
    table with classes for the table of parameters.
    { "wikitable" }
    cssParams
    table with CSS assignments for formatting of single parameters
    cssParWrap
    table with CSS assignments for formatting of the entire parameter table
    docpageCreate
    Pattern for creation of subpage names; %s/Doku
    %s/Doku
    docpageDetect
    Pattern for recognition of subpage names; /Doku$
    /Doku$
    help*********
    Link targets for context sensitive help on details
    helpBoolean
    helpContent
    helpDate
    helpFile
    helpFormat
    Link target on help about wikitext transclusion layout
    helpLine
    helpNumber
    helpPage
    helpString
    helpTemplate
    helpURL
    helpUser
    msgDescMiss
    Localisation: complaint text on missing description
    permit
    table with specification of properties for a single parameter; components:
    boole
    table with specification for boolean presentation
    Two components true and false – each one table:
    css
    table with CSS for this explanation of the value
    lead
    true – show explanation for 0 or 1 respectively preceding the value
    false – show explanation for 0 or 1 respectively following the value
    show
    explanation; string or false to suppress
    css
    table with specifications for rendering of the parameter table; components:
    tablehead
    table with CSS for table head
    required
    table with CSS for required
    suggested
    table with CSS for suggested
    optional
    table with CSS for optional
    deprecated
    table with CSS for deprecated

    local TemplateData = { suite  = "TemplateData",
                           serial = "2022-03-10",
                           item   = 46997995 }
    --[==[
    improve template:TemplateData
    ]==]
    local Failsafe = TemplateData
    
    
    local Config = {
        -- multiple option names mapped into unique internal fields
        basicCnf = { catProblem          = "strange",
                     classMultiColumns   = "selMultClm",
                     classNoNumTOC       = "suppressTOCnum",
                     classTable          = "classTable",
                     cssParWrap          = "cssTabWrap",
                     cssParams           = "cssTable",
                     docpageCreate       = "suffix",
                     docpageDetect       = "subpage",
                     helpBoolean         = "support4boolean",
                     helpContent         = "support4content",
                     helpDate            = "support4date",
                     helpFile            = "support4wiki-file-name",
                     helpFormat          = "supportFormat",
                     helpLine            = "support4line",
                     helpNumber          = "support4number",
                     helpPage            = "support4wiki-page-name",
                     helpString          = "support4string",
                     helpTemplate        = "support4wiki-template-name",
                     helpURL             = "support4url",
                     helpUser            = "support4wiki-user-name",
                     msgDescMiss         = "solo",
                     tStylesTOCnum       = "stylesTOCnum",
                     tStylesMultiColumns = "stylesMultClm" },
        classTable     = { "wikitable" },    -- classes for params table
        debugmultilang = "C0C0C0",
        loudly         = false,    -- show exported element, etc.
        solo           = false,    -- complaint on missing description
        strange        = false,    -- title of maintenance category
        cssTable       = false,    -- styles for params table
        cssTabWrap     = false,    -- styles for params table wrapper
        debug          = false,
        subpage        = false,    -- pattern to identify subpage
        suffix         = false,    -- subpage creation scheme
        suppressTOCnum = false,    -- class for TOC number suppression
        jsonDebug      = "json-code-lint"    -- class for jsonDebug tool
    }
    local Data = {
        div     = false,    -- <div class="mw-templatedata-doc-wrap">
        got     = false,    -- table, initial templatedata object
        heirs   = false,    -- table, params that are inherited
        jump    = false,    -- source position at end of "params"
        less    = false,    -- main description missing
        lasting = false,    -- old syntax encountered
        lazy    = false,    -- doc mode; do not generate effective <templatedata>
        leading = false,    -- show TOC
    --  low     = false,    -- 1= mode
        order   = false,    -- parameter sequence
        params  = false,    -- table, exported parameters
        scream  = false,    -- error messages
        sibling = false,    -- TOC juxtaposed
        slang   = nil,      -- project/user language code
        slim    = false,    -- JSON reduced to plain
        source  = false,    -- JSON input
        strip   = false,    -- <templatedata> evaluation
        tag     = false,    -- table, exported root element
        title   = false,    -- page
        tree    = false     -- table, rewritten templatedata object
    }
    local Permit = {
        builder = { after           = "block",
                    align           = "block",
                    block           = "block",
                    compressed      = "block",
                    dense           = "block",
                    grouped         = "inline",
                    half            = "inline",
                    indent          = "block",
                    inline          = "inline",
                    last            = "block",
                    lead            = "block",
                    newlines        = "*",
                    spaced          = "inline" },
        colors  = { bg          = "FFFFFF",
                    fg          = "000000",
                    tableheadbg = "B3B7FF",
                    required    = "EAF3FF",
                    suggested   = "FFFFFF",
                    optional    = "EAECF0",
                    deprecated  = "FFCBCB" },
        params  = { aliases         = "table",
                    autovalue       = "string",
                    default         = "string table I18N nowiki",
                    deprecated      = "boolean string I18N",
                    description     = "string table I18N",
                    example         = "string table I18N nowiki",
                    label           = "string table I18N",
                    inherits        = "string",
                    required        = "boolean",
                    style           = "string table",
                    suggested       = "boolean",
                    suggestedvalues = "string table number boolean",
                    type            = "string" },
        root    = { description = "string table I18N",
                    format      = "string",
                    maps        = "table",
                    params      = "table",
                    paramOrder  = "table",
                    sets        = "table" },
        search  = "[{,]%%s*(['\"])%s%%1%%s*:%%s*%%{",
        types   = { boolean                   = true,
                    content                   = true,
                    date                      = true,
                    line                      = true,
                    number                    = true,
                    string                    = true,
                    unknown                   = true,
                    url                       = true,
                    ["wiki-file-name"]        = true,
                    ["wiki-page-name"]        = true,
                    ["wiki-template-name"]    = true,
                    ["wiki-user-name"]        = true,
                    ["unbalanced-wikitext"]   = true,
                    ["string/line"]           = "line",
                    ["string/wiki-page-name"] = "wiki-page-name",
                    ["string/wiki-user-name"] = "wiki-user-name" }
    }
    
    
    
    local function Fault( alert )
        -- Memorize error message
        -- Parameter:
        --     alert  -- string, error message
        if Data.scream then
            Data.scream = string.format( "%s *** %s", Data.scream, alert )
        else
            Data.scream = alert
        end
    end -- Fault()
    
    
    
    local function Fetch( ask, allow )
        -- Fetch module
        -- Parameter:
        --     ask    -- string, with name
        --                       "/global"
        --                       "JSONutil"
        --                       "Multilingual"
        --                       "Text"
        --                       "WLink"
        --     allow  -- true: no error if unavailable
        -- Returns table of module
        -- error: Module not available
        local sign = ask
        local r, stem
        if sign:sub( 1, 1 ) == "/" then
            sign = TemplateData.frame:getTitle() .. sign
        else
            stem = sign
            sign = "Module:" .. stem
        end
        if TemplateData.extern then
            r = TemplateData.extern[ sign ]
        else
            TemplateData.extern = { }
        end
        if not r then
            local lucky, g = pcall( require, sign )
            if type( g ) == "table" then
                if stem  and  type( g[ stem ] ) == "function" then
                    r = g[ stem ]()
                else
                    r = g
                end
                TemplateData.extern[ sign ] = r
            elseif not allow then
                error( string.format( "Fetch(%s) %s", sign, g ), 0 )
            end
        end
        return r
    end -- Fetch()
    
    
    
    local function Foreign()
        -- Guess human language
        -- Returns slang, or not
        if type( Data.slang ) == "nil" then
            local Multilingual = Fetch( "Multilingual", true )
            if Multilingual  and
               type( Multilingual.userLangCode ) == "function" then
                Data.slang = Multilingual.userLangCode()
            else
                Data.slang = mw.language.getContentLanguage():getCode()
                                                             :lower()
            end
        end
        if Data.slang  and
           mw.ustring.codepoint( Data.slang, 1, 1 ) > 122 then
            Data.slang = false
        end
        return Data.slang
    end -- Foreign()
    
    
    
    local function facet( ask, at )
        -- Find physical position of parameter definition in JSON
        -- Parameter:
        --     ask  -- string, parameter name
        --     at   -- number, physical position within definition
        -- Returns number, or nil
        local seek = string.format( Permit.search,
                                    ask:gsub( "%%", "%%%%" )
                                       :gsub( "([%-.()+*?^$%[%]])",
                                              "%%%1" ) )
        local i, k, r, slice, source
        if not Data.jump then
            Data.jump = Data.source:find( "params", 2 )
            if Data.jump then
                Data.jump = Data.jump + 7
            else
                Data.jump = 1
            end
        end
        i, k = Data.source:find( seek,  at + Data.jump )
        while i  and  not r do
            source = Data.source:sub( k + 1 )
            slice  = source:match( "^%s*\"([^\"]+)\"s*:" )
            if not slice then
                slice = source:match( "^%s*'([^']+)'%s*:" )
            end
            if ( slice and Permit.params[ slice ] )   or
               source:match( "^%s*%}" ) then
                r = k
            else
                i, k = Data.source:find( seek,  k )
            end
        end    -- while i
        return r
    end -- facet()
    
    
    
    local function facilities( apply )
        -- Retrieve details of suggestedvalues
        -- Parameter:
        --     apply  -- table, with plain or enhanced values
        --               .suggestedvalues  -- table|string|number, or more
        -- Returns
        --     1  -- table, with suggestedvalues
        --     2  -- table, with CSS map, or not
        --     3  -- string, with class, or not
        --     4  -- string, with templatestyles, or not
        local elements = apply.suggestedvalues
        local s        = type( elements )
        local r1, r2, r3, r4
        if s == "table" then
            local values = elements.values
            if type( values ) == "table" then
                r1 = values
                if type( elements.scroll ) == "string" then
                    r2 = r2  or  { }
                    r2.height   = apply.scroll
                    r2.overflow = "auto"
                end
                if type( elements.minwidth ) == "string" then
                    local s = type( elements.maxcolumns )
                    r2 = r2  or  { }
                    r2["column-width"] = elements.minwidth
                    if s == "string"  or
                       s == "number" then
                        s = tostring( elements.maxcolumns )
                        r2["column-count"] = s
                    end
                    if type( Config.selMultClm ) == "string" then
                        r3 = Config.selMultClm
                    end
                    if type( Config.stylesMultClm ) == "string" then
                        local src = Config.stylesMultClm .. "/styles.css"
                        r4 = TemplateData.frame
                                         :extensionTag( "templatestyles",
                                                        nil,
                                                        { src = src } )
                    end
                end
            elseif elements  and  elements ~= "" then
                r1 = elements
            end
        elseif s == "string" then
            s = mw.text.trim( about )
            if s ~= "" then
                r1 = { }
                table.insert( r1,
                              { code = s } )
            end
        elseif s == "number" then
            r1 = { }
            table.insert( r1,
                          { code = tostring( elements ) } )
        end
        return r1, r2, r3, r4
    end -- facilities()
    
    
    
    local function factory( adapt )
        -- Retrieve localized text from system message
        -- Parameter:
        --     adapt  -- string, message ID after "templatedata-"
        -- Returns string, with localized text
        local o = mw.message.new( "templatedata-" .. adapt )
        if Foreign() then
            o:inLanguage( Data.slang )
        end
        return o:plain()
    end -- factory()
    
    
    
    local function faculty( adjust )
        -- Test template arg for boolean
        --     adjust  -- string or nil
        -- Returns boolean
        local s = type( adjust )
        local r
        if s == "string" then
            r = mw.text.trim( adjust )
            r = ( r ~= ""  and  r ~= "0" )
        elseif s == "boolean" then
            r = adjust
        else
            r = false
        end
        return r
    end -- faculty()
    
    
    
    local function failures()
        -- Retrieve error collection and category
        -- Returns string
        local r
        if Data.scream then
            local e = mw.html.create( "span" )
                             :addClass( "error" )
                             :wikitext( Data.scream )
            r = tostring( e )
            mw.addWarning( "'''TemplateData'''<br />" .. Data.scream )
            if Config.strange then
                r = string.format( "%s[[category:%s]]",
                                   r,
                                   Config.strange )
            end
        else
            r = ""
        end
        return r
    end -- failures()
    
    
    
    local function fair( adjust )
        -- Reduce text to one line of plain text, or noexport wikitext blocks
        --     adjust  -- string
        -- Returns string, with adjusted text
        local f    = function ( a )
                         return a:gsub( "%s*\n%s*", " " )
                                 :gsub( "%s%s+", " " )
                     end
        local tags = { { start = "<noexport>",
                         stop  = "</noexport>" },
                       { start = "<exportonly>",
                         stop  = "</exportonly>",
                         l     = false }
                     }
        local r = adjust
        local i, j, k, s, tag
        for m = 1, 2 do
            tag = tags[ m ]
            if r:find( tag.start, 1, true ) then
                s     = r
                r     = ""
                i     = 1
                tag.l = true
                j, k  = s:find( tag.start, i, true )
                while j do
                    if j > 1 then
                        r = r .. f( s:sub( i,  j - 1 ) )
                    end
                    i    = k + 1
                    j, k = s:find( tag.stop, i, true )
                    if j then
                        if m == 1 then
                            r = r .. s:sub( i,  j - 1 )
                        end
                        i    = k + 1
                        j, k = s:find( tag.start, i, true )
                    else
                        Fault( "missing " .. tag.stop )
                    end
                end    -- while j
                r = r .. s:sub( i )
            elseif m == 1 then
                r = f( r )
            end
        end -- for m
        if tags[ 2 ].l then
            r = r:gsub( "<exportonly>.*</exportonly>", "" )
        end
        return r
    end -- fair()
    
    
    
    local function fancy( advance, alert )
        -- Present JSON source
        -- Parameter:
        --     advance  -- true, for nice
        --     alert    -- true, for visible
        -- Returns string
        local r
        if Data.source then
            local support = Config.jsonDebug
            local css
            if advance then
                css = { height = "6em",
                        resize = "vertical" }
                r   = { [ 1 ] = "syntaxhighlight",
                        [ 2 ] = Data.source,
                        lang  = "json",
                        style = table.concat( css, ";" ) }
                if alert then
                    r.class( support )
                end
                r = TemplateData.frame:callParserFunction( "#tag", r )
            else
                css = { [ "font-size" ]   = "77%",
                        [ "line-height" ] = "1.35" }
                if alert then
                    css.resize = "vertical"
                else
                    css.display = "none"
                end
                r = mw.html.create( "pre" )
                           :addClass( support )
                           :css( css )
                           :wikitext( mw.text.encode( Data.source ) )
                r = tostring( r )
            end
            r = "\n".. r
        else
            r = ""
        end
        return r
    end -- fancy()
    
    
    
    local function faraway( alternatives )
        -- Retrieve best language version from multilingual text
        -- Parameter:
        --     alternatives  -- table, to be evaluated
        -- Returns
        --     1  -- string, with best match
        --     2  -- table of other versions, if any
        local n = 0
        local variants = { }
        local r1, r2
        for k, v in pairs( alternatives ) do
            if type( v ) == "string" then
                v = mw.text.trim( v )
                if v ~= ""  and  type( k ) == "string" then
                    k = k:lower()
                    variants[ k ] = v
                    n             = n + 1
                end
            end
        end -- for k, v
        if n > 0 then
            local Multilingual = Fetch( "Multilingual", true )
            if Multilingual  and
               type( Multilingual.i18n ) == "function" then
                local show, slang = Multilingual.i18n( variants )
                if show then
                    r1 = show
                    variants[ slang ] = nil
                    r2 = variants
                end
            end
            if not r1 then
                Foreign()
                for k, v in pairs( variants ) do
                    if n == 1 then
                        r1 = v
                    elseif Data.slang == k then
                        variants[ k ] = nil
                        r1 = v
                        r2 = variants
                    end
                end -- for k, v
            end
            if r2 and Multilingual then
                for k, v in pairs( r2 ) do
                    if v  and  not Multilingual.isLang( k, true ) then
                        Fault( string.format( "%s <code>lang=%s</code>",
                                              "Invalid",
                                              k ) )
                    end
                end -- for k, v
            end
        end
        return r1, r2
    end -- faraway()
    
    
    
    local function fashioned( about, asked, assign )
        -- Create description head
        -- Parameter:
        --     about   -- table, supposed to contain description
        --     asked   -- true, if mandatory description
        --     assign  -- <block>, if to be equipped
        -- Returns <block>, with head, or nil
        local para = assign or mw.html.create( "div" )
        local plus, r
        if about and about.description then
            if type( about.description ) == "string" then
                para:wikitext( about.description )
            else
                para:wikitext( about.description[ 1 ] )
                plus = mw.html.create( "ul" )
                plus:css( "text-align", "left" )
                for k, v in pairs( about.description[ 2 ] ) do
                    plus:node( mw.html.create( "li" )
                                      :node( mw.html.create( "code" )
                                                    :wikitext( k ) )
                                      :node( mw.html.create( "br" ) )
                                      :wikitext( fair( v ) ) )
                end -- for k, v
                if Config.loudly then
                    plus = mw.html.create( "div" )
                                  :css( "background-color",
                                        "#" .. Config.debugmultilang )
                                  :node( plus )
                else
                    plus:addClass( "templatedata-maintain" )
                        :css( "display", "none" )
                end
            end
        elseif Config.solo and asked then
            para:addClass( "error" )
                :wikitext( Config.solo )
            Data.less = true
        else
            para = false
        end
        if para then
            if plus then
                r = mw.html.create( "div" )
                           :node( para )
                           :node( plus )
            else
                r = para
            end
        end
        return r
    end -- fashioned()
    
    
    
    local function fatten( access )
        -- Create table row for sub-headline
        -- Parameter:
        --     access  -- string, with name
        -- Returns <tr>
        local param     = Data.tree.params[ access ]
        local sub, sort = access:match( "(=+)%s*(%S.*)$" )
        local headline  = mw.html.create( string.format( "h%d", #sub ) )
        local r         = mw.html.create( "tr" )
        local td        = mw.html.create( "td" )
                                 :attr( "colspan", "5" )
                                 :attr( "data-sort-value",  "!" .. sort )
        local s
        if param.style then
            s = type( param.style )
            if s == "table" then
                td:css( param.style )
            elseif s == "string" then
                td:cssText( param.style )
            end
        end
        s = fashioned( param, false, headline )
        if s then
            headline = s
        else
            headline:wikitext( sort )
        end
        td:node( headline )
        r:node( td )
        return r
    end -- fatten()
    
    
    
    local function fathers()
        -- Merge params with inherited values
        local n = 0
        local p = Data.params
        local t = Data.tree.params
        local p2, t2
        for k, v in pairs( Data.heirs ) do
            n = n + 1
        end -- for k, v
        for i = 1, n do
            if Data.heirs then
                for k, v in pairs( Data.heirs ) do
                    if v  and  not Data.heirs[ v ] then
                        n               = n - 1
                        t[ k ].inherits = nil
                        Data.heirs[ k ] = nil
                        p2              = { }
                        t2              = { }
                        if p[ v ] then
                            for k2, v2 in pairs( p[ v ] ) do
                                p2[ k2 ] = v2
                            end -- for k2, v2
                            if p[ k ] then
                                for k2, v2 in pairs( p[ k ] ) do
                                    if type( v2 ) ~= "nil" then
                                        p2[ k2 ] = v2
                                    end
                                end -- for k2, v2
                            end
                            p[ k ] = p2
                            for k2, v2 in pairs( t[ v ] ) do
                                t2[ k2 ] = v2
                            end -- for k2, v2
                            for k2, v2 in pairs( t[ k ] ) do
                                if type( v2 ) ~= "nil" then
                                    t2[ k2 ] = v2
                                end
                            end -- for k2, v2
                            t[ k ] = t2
                        else
                            Fault( "No params[] inherits " .. v )
                        end
                    end
                end -- for k, v
            end
        end -- i = 1, n
        if n > 0 then
            local s
            for k, v in pairs( Data.heirs ) do
                if v then
                    if s then
                        s = string.format( "%s &#124; %s", s, k )
                    else
                        s = "Circular inherits: " .. k
                    end
                end
            end -- for k, v
            Fault( s )
        end
    end -- fathers()
    
    
    
    local function favorize()
        -- Local customization issues
        local boole  = { ["font-size"] = "125%" }
        local l, cx = pcall( mw.loadData,
                             TemplateData.frame:getTitle() .. "/config" )
        local scripting, style
        TemplateData.ltr = not mw.language.getContentLanguage():isRTL()
        if TemplateData.ltr then
            scripting = "left"
        else
            scripting = "right"
        end
        boole[ "margin-" .. scripting ] = "3em"
        Permit.boole = { [false] = { css  = boole,
                                     lead = true,
                                     show = "&#x2610;" },
                         [true]  = { css  = boole,
                                     lead = true,
                                     show = "&#x2611;" } }
        Permit.css   = { }
        for k, v in pairs( Permit.colors ) do
            if k == "tableheadbg" then
                k = "tablehead"
            end
            if k == "fg" then
                style = "color"
            else
                style = "background-color"
            end
            Permit.css[ k ] = { }
            Permit.css[ k ][ style ] = "#" .. v
        end -- for k, v
        if type( cx ) == "table" then
            local c, s
            if type( cx.permit ) == "table" then
                if type( cx.permit.boole ) == "table" then
                    if type( cx.permit.boole[ true ] ) == "table" then
                        Permit.boole[ false ]  = cx.permit.boole[ false ]
                    end
                    if type( cx.permit.boole[ true ] ) == "table" then
                        Permit.boole[ true ]  = cx.permit.boole[ true ]
                    end
                end
                if type( cx.permit.css ) == "table" then
                    for k, v in pairs( cx.permit.css ) do
                        if type( v ) == "table" then
                            Permit.css[ k ] = v
                        end
                    end -- for k, v
                end
            end
            for k, v in pairs( Config.basicCnf ) do
                s = type( cx[ k ] )
                if s == "string"  or  s == "table" then
                    Config[ v ] = cx[ k ]
                end
            end -- for k, v
        end
        if type( Config.subpage ) ~= "string"  or
           type( Config.suffix ) ~= "string" then
            local got = mw.message.new( "templatedata-doc-subpage" )
            local suffix
            if got:isDisabled() then
                suffix = "doc"
            else
                suffix = got:plain()
            end
            if type( Config.subpage ) ~= "string" then
                Config.subpage = string.format( "/%s$", suffix )
            end
            if type( Config.suffix ) ~= "string" then
                Config.suffix = string.format( "%%s/%s", suffix )
            end
        end
    end -- favorize()
    
    
    
    local function feasible( all, at, about )
        -- Deal with suggestedvalues within parameter
        -- Parameter:
        --     all    -- parameter details
        --               .default
        --               .type
        --     at     -- string, with parameter name
        --     about  -- suggestedvalues  -- table,
        --                                   value and possibly description
        --                                   table may have elements:
        --                                    .code    -- mandatory
        --                                    .label   -- table|string
        --                                    .support -- table|string
        --                                    .icon    -- string
        --                                    .class   -- table|string
        --                                    .css     -- table
        --                                    .style   -- string
        --                                    .less    -- true: suppress code
        -- Returns
        --     1: mw.html object <ul>
        --     2: sequence table with values, or nil
        local h = { }
        local e, r1, r2, s, v
        if #about > 0 then
            for i = 1, #about do
                e = about[ i ]
                s = type( e )
                if s == "table" then
                    if type( e.code ) == "string" then
                        s = mw.text.trim( e.code )
                        if s == "" then
                            e = nil
                        else
                            e.code = s
                        end
                    else
                        e = nil
                        s = string.format( "params.%s.%s[%d] %s",
                                           at,
                                           "suggestedvalues",
                                           i,
                                           "MISSING 'code:'" )
                    end
                elseif s == "string" then
                    s = mw.text.trim( e )
                    if s == "" then
                        e = nil
                        s = string.format( "params.%s.%s[%d] EMPTY",
                                           at, "suggestedvalues", i )
                        Fault( s )
                    else
                        e = { code = s }
                    end
                elseif s == "number" then
                    e = { code = tostring( e ) }
                else
                    s = string.format( "params.%s.%s[%d] INVALID",
                                       at, "suggestedvalues", i )
                    Fault( s )
                    e = false
                end
                if e then
                    v = v  or  { }
                    table.insert( v, e )
                    if h[ e.code ] then
                        s = string.format( "params.%s.%s REPEATED %s",
                                           at,
                                           "suggestedvalues",
                                           e.code )
                        Fault( s )
                    else
                        h[ e.code ] = true
                    end
                end
            end -- for i
        else
            Fault( string.format( "params.%s.suggestedvalues %s",
                                  at, "NOT AN ARRAY" ) )
        end
        if v then
            local code, d, k, less, story, swift, t, u
            r1 = mw.html.create( "ul" )
            r2 = { }
            for i = 1, #v do
                u = mw.html.create( "li" )
                e = v[ i ]
                table.insert( r2, e.code )
                story = false
                less  = ( e.less == true )
                if not less then
                    swift = e.code
                    if e.support then
                        local scream, support
                        s = type( e.support )
                        if s == "string" then
                            support = e.support
                        elseif s == "table" then
                            support = faraway( e.support )
                        else
                            scream = "INVALID"
                        end
                        if support then
                            s = mw.text.trim( support )
                            if s == "" then
                                scream = "EMPTY"
                            elseif s:find( "[%[%]|%<%>]" ) then
                                scream = "BAD PAGE"
                            else
                                support = s
                            end
                        end
                        if scream then
                            s = string.format( "params.%s.%s[%d].support %s",
                                               at,
                                               "suggestedvalues",
                                               i,
                                               scream )
                            Fault( s )
                        else
                            swift = string.format( "[[:%s|%s]]",
                                                   support, swift )
                        end
                    end
                    if all.type:sub( 1, 5 ) == "wiki-"  and
                       swift == e.code then
                        local rooms = { file = 6,
                                        temp = 10,
                                        user = 2 }
                        local ns = rooms[ all.type:sub( 6, 9 ) ]  or  0
                        t = mw.title.makeTitle( ns, swift )
                        if t and t.exists then
                            swift = string.format( "[[:%s|%s]]",
                                                   t.prefixedText, swift )
                        end
                    end
                    if e.code == all.default then
                        k = 800
                    else
                        k = 300
                    end
                    code = mw.html.create( "code" )
                                  :css( "font-weight", tostring( k ) )
                                  :css( "white-space", "nowrap" )
                                  :wikitext( swift )
                    u:node( code )
                end
                if e.class then
                    s = type( e.class )
                    if s == "string" then
                        u:addClass( e.class )
                    elseif s == "table" then
                        for k, s in pairs( e.class ) do
                            u:addClass( s )
                        end -- for k, s
                    else
                        s = string.format( "params.%s.%s[%d].class INVALID",
                                           at, "suggestedvalues", i )
                        Fault( s )
                    end
                end
                if e.css then
                    if type( e.css ) == "table" then
                        u:css( e.css )
                    else
                        s = string.format( "params.%s.%s[%d].css INVALID",
                                           at, "suggestedvalues", i )
                        Fault( s )
                    end
                end
                if e.style then
                    if type( e.style ) == "string" then
                        u:cssText( e.style )
                    else
                        s = string.format( "params.%s.%s[%d].style INVALID",
                                           at, "suggestedvalues", i )
                        Fault( s )
                    end
                end
                if all.type == "wiki-file-name"  and  not e.icon then
                    e.icon = e.code
                end
                if e.label then
                    s = type( e.label )
                    if s == "string" then
                        s = mw.text.trim( e.label )
                        if s == "" then
                            s = string.format( "params.%s.%s[%d].label %s",
                                               at,
                                               "suggestedvalues",
                                               i,
                                               "EMPTY" )
                            Fault( s )
                        else
                            story = s
                        end
                    elseif s == "table" then
                        story = faraway( e.label )
                    else
                        s = string.format( "params.%s.%s[%d].label INVALID",
                                           at, "suggestedvalues", i )
                        Fault( s )
                    end
                end
                s = false
                if type( e.icon ) == "string" then
                    t = mw.title.makeTitle( 6, e.icon )
                    if t and t.file.exists then
                        local g = mw.html.create( "span" )
                        s = string.format( "[[%s|16px]]", t.prefixedText )
                        g:attr( "role", "presentation" )
                         :wikitext( s )
                        s = tostring( g )
                    end
                end
                if not s  and  not less  and  e.label then
                    s = mw.ustring.char( 0x2013 )
                end
                if s then
                    d = mw.html.create( "span" )
                               :wikitext( s )
                    if TemplateData.ltr then
                        if not less then
                            d:css( "margin-left", "0.5em" )
                        end
                        if story then
                            d:css( "margin-right", "0.5em" )
                        end
                    else
                        if not less then
                            d:css( "margin-right", "0.5em" )
                        end
                        if story then
                            d:css( "margin-left", "0.5em" )
                        end
                    end
                    u:node( d )
                end
                if story then
                    u:wikitext( story )
                end
                r1:newline()
                  :node( u )
            end -- for i
        end
        if not r1  and  v ~= false then
            Fault( string.format( "params.%s.suggestedvalues INVALID", at ) )
            r1 = mw.html.create( "code" )
                        :addClass( "error" )
                        :wikitext( "INVALID" )
        end
        return r1, r2
    end -- feasible()
    
    
    
    local function feat()
        -- Check and store parameter sequence
        if Data.source then
            local i = 0
            local s
            for k, v in pairs( Data.tree.params ) do
                if i == 0 then
                    Data.order = { }
                    i = 1
                    s = k
                else
                    i = 2
                    break -- for k, v
                end
            end -- for k, v
            if i > 1 then
                local pointers = { }
                local points   = { }
                local given    = { }
                for k, v in pairs( Data.tree.params ) do
                    i = facet( k, 1 )
                    if type( v ) == "table" then
                        if type( v.label ) == "string" then
                            s = mw.text.trim( v.label )
                            if s == "" then
                                s = k
                            end
                        else
                            s = k
                        end
                        if given[ s ] then
                            if given[ s ] == 1 then
                                local scream = "Parameter label '%s' detected multiple times"
                                Fault( string.format( scream, s ) )
                                given[ s ] = 2
                            end
                        else
                            given[ s ] = 1
                        end
                    end
                    if i then
                        table.insert( points, i )
                        pointers[ i ] = k
                        i = facet( k, i )
                        if i then
                            s = "Parameter '%s' detected twice"
                            Fault( string.format( s, k ) )
                        end
                    else
                        s = "Parameter '%s' not detected"
                        Fault( string.format( s, k ) )
                    end
                end -- for k, v
                table.sort( points )
                for i = 1, #points do
                    table.insert( Data.order,  pointers[ points[ i ] ] )
                end -- i = 1, #points
            elseif s then
                table.insert( Data.order, s )
            end
        end
    end -- feat()
    
    
    
    local function feature( access )
        -- Create table row for parameter, check and display violations
        -- Parameter:
        --     access  -- string, with name
        -- Returns <tr>
        local mode, s, status
        local fine    = function ( a )
                            s = mw.text.trim( a )
                            return a == s  and
                                   a ~= ""  and
                                   not a:find( "%|=\n" )  and
                                   not a:find( "%s%s" )
                        end
        local begin   = mw.html.create( "td" )
        local code    = mw.html.create( "code" )
        local desc    = mw.html.create( "td" )
        local eager   = mw.html.create( "td" )
        local legal   = true
        local param   = Data.tree.params[ access ]
        local ranking = { "required", "suggested", "optional", "deprecated" }
        local r       = mw.html.create( "tr" )
        local styles  = "mw-templatedata-doc-param-"
        local sort, typed
    
        for k, v in pairs( param ) do
            if v == "" then
                param[ k ] = false
            end
        end -- for k, v
    
        -- label
        sort = param.label or access
        if sort:match( "^%d+$" ) then
            begin:attr( "data-sort-value",
                        string.format( "%05d", tonumber( sort ) ) )
        end
        begin:css( "font-weight", "bold" )
             :wikitext( sort )
    
        -- name and aliases
        code:css( "font-size", "92%" )
            :css( "white-space", "nowrap" )
            :wikitext( access )
        if not fine( access ) then
            code:addClass( "error" )
            Fault( string.format( "Bad ID params.<code>%s</code>", access ) )
            legal = false
            begin:attr( "data-sort-value",  " " .. sort )
        end
        code = mw.html.create( "td" )
                      :addClass( styles .. "name" )
                      :node( code )
        if access:match( "^%d+$" ) then
            code:attr( "data-sort-value",
                       string.format( "%05d", tonumber( access ) ) )
        end
        if type( param.aliases ) == "table" then
            local lapsus, syn
            for k, v in pairs( param.aliases ) do
                code:tag( "br" )
                if type( v ) == "string" then
                    if not fine( v ) then
                        lapsus = true
                        code:node( mw.html.create( "span" )
                                          :addClass( "error" )
                                          :css( "font-style", "italic" )
                                          :wikitext( "string" ) )
                            :wikitext( s )
                    else
                        syn = mw.html.create( "span" )
                                     :addClass( styles .. "alias" )
                                     :css( "white-space", "nowrap" )
                                     :wikitext( s )
                        code:node( syn )
                    end
                else
                    lapsus = true
                    code:node( mw.html.create( "code" )
                                      :addClass( "error" )
                                      :wikitext( type( v ) ) )
                end
            end -- for k, v
            if lapsus then
                s = string.format( "params.<code>%s</code>.aliases", access )
                Fault(  factory( "invalid-value" ):gsub( "$1", s )  )
                legal = false
            end
        end
    
        -- description etc.
        s = fashioned( param )
        if s then
            desc:node( s )
        end
        if param.style then
            s = type( param.style )
            if s == "table" then
                desc:css( param.style )
            elseif s == "string" then
                desc:cssText( param.style )
            end
        end
        if param.suggestedvalues or
           param.default or
           param.example or
           param.autovalue then
            local details = { "suggestedvalues",
                              "default",
                              "example",
                              "autovalue" }
            local dl      = mw.html.create( "dl" )
            local dd, section, show
            for i = 1, #details do
                s    = details[ i ]
                show = param[ s ]
                if show then
                    dd      = mw.html.create( "dd" )
                    section = factory( "doc-param-" .. s )
                    if param.type == "boolean"   and
                       ( show == "0" or show == "1" ) then
                        local boole = Permit.boole[ ( show == "1" ) ]
                        if boole.lead == true then
                            dd:node( mw.html.create( "code" )
                                            :wikitext( show ) )
                              :wikitext( " " )
                        end
                        if type( boole.show ) == "string" then
                            local v = mw.html.create( "span" )
                                             :attr( "aria-hidden", "true" )
                                             :wikitext( boole.show )
                            if boole.css then
                                v:css( boole.css )
                            end
                            dd:node( v )
                        end
                        if type( boole.suffix ) == "string" then
                            dd:wikitext( boole.suffix )
                        end
                        if boole.lead == false then
                            dd:wikitext( " " )
                              :node( mw.html.create( "code" )
                                            :wikitext( show ) )
                        end
                    elseif s == "suggestedvalues" then
                        local v, css, class, ts = facilities( param )
                        if v then
                            local ul
                            ul, v = feasible( param, access, v )
                            if v then
                                dd:newline()
                                  :node( ul )
                                if css then
                                    dd:css( css )
                                    if class then
                                        dd:addClass( class )
                                    end
                                    if ts then
                                        dd:newline()
                                        dd:node( ts )
                                    end
                                end
                                Data.params[ access ].suggestedvalues = v
                            end
                        end
                    else
                        dd:wikitext( show )
                    end
                    dl:node( mw.html.create( "dt" )
                                    :wikitext( section ) )
                      :node( dd )
                end
            end -- i = 1, #details
            desc:node( dl )
        end
    
        -- type
        if type( param.type ) == "string" then
            param.type = mw.text.trim( param.type )
            if param.type == "" then
                param.type = false
            end
        end
        if param.type then
            s     = Permit.types[ param.type ]
            typed = mw.html.create( "td" )
                      :addClass( styles .. "type" )
            if s then
                if s == "string" then
                    Data.params[ access ].type = s
                    typed:wikitext( factory( "doc-param-type-" .. s ) )
                         :tag( "br" )
                    typed:node( mw.html.create( "span" )
                                       :addClass( "error" )
                                       :wikitext( param.type ) )
                    Data.lasting = true
                else
                    local support = Config[ "support4" .. param.type ]
                    s = factory( "doc-param-type-" .. param.type )
                    if support then
                        s = string.format( "[[%s|%s]]", support, s )
                    end
                    typed:wikitext( s )
                end
            else
                Data.params[ access ].type = "unknown"
                typed:addClass( "error" )
                     :wikitext( "INVALID" )
                s = string.format( "params.<code>%s</code>.type", access )
                Fault(  factory( "invalid-value" ):gsub( "$1", s )  )
                legal = false
            end
        else
            typed = mw.html.create( "td" )
                       :wikitext( factory( "doc-param-type-unknown" ) )
            Data.params[ access ].type = "unknown"
            if param.default then
                Data.params[ access ].default = nil
                Fault( "Default value requires <code>type</code>" )
                legal = false
            end
        end
        typed:addClass( "navigation-not-searchable" )
        -- status
        if param.required then
            mode = 1
            if param.autovalue then
                Fault( string.format( "autovalued <code>%s</code> required",
                                      access ) )
                legal = false
            end
            if param.default then
                Fault( string.format( "Defaulted <code>%s</code> required",
                                      access ) )
                legal = false
            end
            if param.deprecated then
                Fault( string.format( "Required deprecated <code>%s</code>",
                                      access ) )
                legal = false
            end
        elseif param.deprecated then
            mode = 4
        elseif param.suggested then
            mode = 2
        else
            mode = 3
        end
        status = ranking[ mode ]
        ranking = factory( "doc-param-status-" .. status )
        if mode == 1  or  mode == 4 then
            ranking = mw.html.create( "span" )
                             :css( "font-weight", "bold" )
                             :wikitext( ranking )
            if type( param.deprecated ) == "string" then
                ranking:tag( "br" )
                ranking:wikitext( param.deprecated )
            end
            if param.suggested  and  mode == 4 then
                s = string.format( "Suggesting deprecated <code>%s</code>",
                                   access )
                Fault( s )
                legal = false
            end
        end
        eager:attr( "data-sort-value", tostring( mode ) )
                    :node( ranking )
                    :addClass( string.format( "%sstatus-%s %s",
                                              styles, status,
                                              "navigation-not-searchable" ) )
    
        -- <tr>
        r:attr( "id",  "templatedata:" .. mw.uri.anchorEncode( access ) )
         :css( Permit.css[ status ] )
         :addClass( styles .. status )
         :node( begin )
         :node( code )
         :node( desc )
         :node( typed )
         :node( eager )
         :newline()
        if not legal then
            r:css( "border", "#FF0000 3px solid" )
        end
        return r
    end -- feature()
    
    
    
    local function features()
        -- Create <table> for parameters
        -- Returns <table>, or nil
        local r
        if Data.tree and Data.tree.params then
            local tbl = mw.html.create( "table" )
            local tr  = mw.html.create( "tr" )
            feat()
            if Data.order  and  #Data.order > 1 then
                tbl:addClass( "sortable" )
            end
            if type( Config.classTable ) == "table" then
                for k, v in pairs( Config.classTable ) do
                    tbl:addClass( v )
                end -- for k, v
            end
            if type( Config.cssTable ) == "table" then
                tbl:css( Config.cssTable )
            end
            tr:addClass( "navigation-not-searchable" )
              :node( mw.html.create( "th" )
                            :attr( "colspan", "2" )
                            :css( Permit.css.tablehead )
                            :wikitext( factory( "doc-param-name" ) ) )
              :node( mw.html.create( "th" )
                            :css( Permit.css.tablehead )
                            :wikitext( factory( "doc-param-desc" ) ) )
              :node( mw.html.create( "th" )
                            :css( Permit.css.tablehead )
                            :wikitext( factory( "doc-param-type" ) ) )
              :node( mw.html.create( "th" )
                            :css( Permit.css.tablehead )
                            :wikitext( factory( "doc-param-status" ) ) )
            tbl:newline()
    --         :node( mw.html.create( "thead" )
                             :node( tr )
    --              )
               :newline()
            if Data.order then
                local leave, s
                for i = 1, #Data.order do
                    s = Data.order[ i ]
                    if s:sub( 1, 1 ) == "=" then
                        leave = true
                        tbl:node( fatten( s ) )
                        Data.order[ i ] = false
                    elseif s:match( "[=|]" ) then
                        Fault( string.format( "Bad param <code>%s</code>",
                                              s ) )
                    else
                        tbl:node( feature( s ) )
                    end
                end -- for i = 1, #Data.order
                if leave then
                    for i = #Data.order, 1, -1 do
                        if not Data.order[ i ] then
                            table.remove( Data.order, i )
                        end
                    end -- for i = #Data.order, 1, -1
                end
                Data.tag.paramOrder = Data.order
            end
            if Config.cssTabWrap or Data.scroll then
                r = mw.html.create( "div" )
                if type( Config.cssTabWrap ) == "table" then
                    r:css( Config.cssTabWrap )
                elseif type( Config.cssTabWrap ) == "string" then
                    -- deprecated
                    r:cssText( Config.cssTabWrap )
                end
                if Data.scroll then
                    r:css( "height",   Data.scroll )
                     :css( "overflow", "auto" )
                end
                r:node( tbl )
            else
                r = tbl
            end
        end
        return r
    end -- features()
    
    
    
    local function fellow( any, assigned, at )
        -- Check sets[] parameter and issue error message, if necessary
        -- Parameter:
        --     any       -- should be number
        --     assigned  -- parameter name
        --     at        -- number, of set
        local s
        if type( any ) ~= "number" then
            s = "<code>sets[%d].params[%s]</code>??"
            Fault( string.format( s,
                                  at,
                                  mw.text.nowiki( tostring( any ) ) ) )
        elseif type( assigned ) == "string" then
            if not Data.got.params[ assigned ] then
                s = "<code>sets[%d].params %s</code> is undefined"
                Fault( string.format( s, at, assigned ) )
            end
        else
            s = "<code>sets[%d].params[%d] = %s</code>??"
            Fault( string.format( s,  k,  type( assigned ) ) )
        end
    end -- fellow()
    
    
    
    local function fellows()
        -- Check sets[] and issue error message, if necessary
        local s
        if type( Data.got.sets ) == "table" then
            if type( Data.got.params ) == "table" then
                for k, v in pairs( Data.got.sets ) do
                    if type( k ) == "number" then
                        if type( v ) == "table" then
                            for ek, ev in pairs( v ) do
                                if ek == "label" then
                                    s = type( ev )
                                    if s ~= "string"  and
                                       s ~= "table" then
                                        s = "<code>sets[%d].label</code>??"
                                        Fault( string.format( s, k ) )
                                    end
                                elseif ek == "params"  and
                                    type( ev ) == "table" then
                                    for pk, pv in pairs( ev ) do
                                        fellow( pk, pv, k )
                                    end -- for pk, pv
                                else
                                    ek = mw.text.nowiki( tostring( ek ) )
                                    s  = "<code>sets[%d][%s]</code>??"
                                    Fault( string.format( s, k, ek ) )
                                end
                            end -- for ek, ev
                        else
                            k = mw.text.nowiki( tostring( k ) )
                            v = mw.text.nowiki( tostring( v ) )
                            s = string.format( "<code>sets[%s][%s]</code>??",
                                               k, v )
                            Fault( s )
                        end
                    else
                        k = mw.text.nowiki( tostring( k ) )
                        s = string.format( "<code>sets[%s]</code> ?????", k )
                        Fault( s )
                    end
                end -- for k, v
            else
                s = "<code>params</code> required for <code>sets</code>"
                Fault( s )
            end
        else
            s = "<code>sets</code> needs to be of <code>object</code> type"
            Fault( s )
        end
    end -- fellows()
    
    
    
    local function finalize( advance )
        -- Wrap presentation into frame
        -- Parameter:
        --     advance  -- true, for nice
        -- Returns string
        local r, lapsus
        if Data.div then
            r = tostring( Data.div )
        elseif Data.strip then
            r = Data.strip
        else
            lapsus = true
            r      = ""
        end
        r = r .. failures()
        if Data.source then
            local live = ( advance or lapsus )
            if not live then
                live = TemplateData.frame:preprocess( "{{REVISIONID}}" )
                live = ( live == "" )
            end
            if live then
                r = r .. fancy( advance, lapsus )
            end
        end
        return r
    end -- finalize()
    
    
    
    local function find()
        -- Find JSON data within page source (title)
        -- Returns string, or nil
        local s = Data.title:getContent()
        local i, j = s:find( "<templatedata>", 1, true )
        local r
        if i then
            local k = s:find( "</templatedata>", j, true )
            if k then
               r = mw.text.trim( s:sub( j + 1,  k - 1 ) )
            end
        end
        return r
    end -- find()
    
    
    
    local function flat( adjust )
        -- Remove formatting from text string for VE
        -- Parameter:
        --     arglist  -- string, to be stripped, or nil
        -- Returns string, or nil
        local r
        if adjust then
            r = adjust:gsub( "\n", " " )
            if r:find( "<noexport>", 1, true ) then
                r = r:gsub( "<noexport>.*</noexport>", "" )
            end
            if r:find( "<exportonly>", 1, true ) then
                r = r:gsub( "</?exportonly>", "" )
            end
            if r:find( "''", 1, true ) then
                r = r:gsub( "'''", "" ):gsub( "''", "" )
            end
            if r:find( "<", 1, true ) then
                local Text = Fetch( "Text" )
                r = Text.getPlain( r:gsub( "<br */?>", "\r\n" ) )
            end
            if r:find( "[", 1, true ) then
                local WLink = Fetch( "WLink" )
                if WLink.isBracketedURL( r ) then
                    r = r:gsub( "%[([hf]tt?ps?://%S+) [^%]]+%]", "%1" )
                end
                r = WLink.getPlain( r )
            end
            if r:find( "&", 1, true ) then
                r = mw.text.decode( r )
                if r:find( "&shy;", 1, true ) then
                    r = r:gsub( "&shy;", "" )
                end
            end
        end
        return r
    end -- flat()
    
    
    
    local function flush()
        -- JSON encode narrowed input; obey unnamed (numerical) parameters
        -- Returns <templatedata> JSON string
        local r
        if Data.tag then
            r = mw.text.jsonEncode( Data.tag ):gsub( "%}$", "," )
        else
            r = "{"
        end
        r = r .. "\n\"params\":{"
        if Data.order then
            local sep = ""
            local s
            for i = 1, #Data.order do
                s   = Data.order[ i ]
                r   = string.format( "%s%s\n%s:%s",
                                     r,
                                     sep,
                                     mw.text.jsonEncode( s ),
                                     mw.text.jsonEncode( Data.params[ s ] ) )
                sep = ",\n"
            end -- for i = 1, #Data.order
        end
        r = r .. "\n}\n}"
        return r
    end -- flush()
    
    
    
    local function focus( access )
        -- Check components; focus multilingual description, build trees
        -- Parameter:
        --     access  -- string, name of parameter, nil for root
        local f = function ( a, at )
                        local r
                        if at then
                            r = string.format( "<code>params.%s</code>", at )
                        else
                            r = "''root''"
                        end
                        if a then
                            r = string.format( "%s<code>.%s</code>", r, a )
                        end
                        return r
                    end
        local parent
        if access then
            parent = Data.got.params[ access ]
        else
            parent = Data.got
        end
        if type( parent ) == "table" then
            local elem, got, permit, s, scope, slot, tag, target
            if access then
                permit = Permit.params
                if type( access ) == "number" then
                    slot = tostring( access )
                else
                    slot = access
                end
            else
                permit = Permit.root
            end
            for k, v in pairs( parent ) do
                scope = permit[ k ]
                if scope then
                    s = type( v )
                    if s == "string"  and  k ~= "format" then
                        v = mw.text.trim( v )
                    end
                    if scope:find( s, 1, true ) then
                        if scope:find( "I18N", 1, true ) then
                            if s == "string" then
                                elem = fair( v )
                            elseif s == "table" then
                                local translated
                                v, translated = faraway( v )
                                if v then
                                    if translated  and
                                       k == "description" then
                                        elem = { [ 1 ] = fair( v ),
                                                 [ 2 ] = translated }
                                    else
                                        elem = fair( v )
                                    end
                                else
                                    elem = false
                                end
                            end
                            if type( v ) == "string" then
                                if k == "deprecated" then
                                    if v == "1" then
                                        v = true
                                    elseif v == "0" then
                                        v = false
                                    end
                                    elem = v
                                elseif scope:find( "nowiki", 1, true ) then
                                    elem = mw.text.nowiki( v )
                                    elem = elem:gsub( "&#13;\n", "<br>" )
                                    v    = v:gsub( string.char( 13 ),  "" )
                                else
                                    v = flat( v )
                                end
                            elseif s == "boolean" then
                                if scope:find( "boolean", 1, true ) then
                                    elem = v
                                else
                                    s = "Type <code>boolean</code> bad for "
                                        .. f( k, slot )
                                    Fault( s )
                                end
                            end
                        else
                            if k == "params"  and  not access then
                                v    = nil
                                elem = nil
                            elseif k == "format"  and  not access then
                                elem = mw.text.decode( v )
                                v    = nil
                            elseif k == "inherits" then
                                elem = v
                                if not Data.heirs then
                                    Data.heirs = { }
                                end
                                Data.heirs[ slot ] = v
                                v                  = nil
                            elseif k == "style" then
                                elem = v
                                v    = nil
                            elseif s == "string" then
                                v    = mw.text.nowiki( v )
                                elem = v
                            else
                                elem = v
                            end
                        end
                        if type( elem ) ~= "nil" then
                            if not target then
                                if access then
                                    if not Data.tree.params then
                                        Data.tree.params = { }
                                    end
                                    Data.tree.params[ slot ] = { }
                                    target = Data.tree.params[ slot ]
                                else
                                    Data.tree = { }
                                    target    = Data.tree
                                end
                            end
                            target[ k ] = elem
                            elem        = false
                        end
                        if type( v ) ~= "nil" then
                            if not tag then
                                if access then
                                    if type( v ) == "string"  and
                                       v.sub( 1, 1 ) == "=" then
                                        v = nil
                                    else
                                        if not Data.params then
                                            Data.params = { }
                                        end
                                        Data.params[ slot ] = { }
                                        tag = Data.params[ slot ]
                                    end
                                else
                                    Data.tag = { }
                                    tag      = Data.tag
                                end
                            end
                            if type( v ) ~= "nil"  and
                               k ~= "suggestedvalues" then
                                tag[ k ] = v
                            end
                        end
                    else
                        s = string.format( "Type <code>%s</code> bad for %s",
                                           scope,  f( k, slot ) )
                        Fault( s )
                    end
                else
                    Fault( "Unknown component " .. f( k, slot ) )
                end
            end -- for k, v
            if not access  and Data.got.sets then
                fellows()
            end
        else
            Fault( f() .. " needs to be of <code>object</code> type" )
        end
    end -- focus()
    
    
    
    local function format()
        -- Build formatted element
        -- Returns <inline>
        local source = Data.tree.format:lower()
        local r, s
        if source == "inline"  or  source == "block" then
            r = mw.html.create( "i" )
                       :wikitext( source )
        else
            local code
            if source:find( "|", 1, true ) then
                local scan = "^[\n ]*%{%{[\n _]*|[\n _]*=[\n _]*%}%}[\n ]*$"
                if source:match( scan ) then
                    code = source:gsub( "\n", "N" )
                else
                    s = mw.text.nowiki( source ):gsub( "\n", "&#92;n" )
                    s = tostring( mw.html.create( "code" )
                                         :wikitext( s ) )
                    Fault( "Invalid format " .. s )
                    source = false
                end
            else
                local words = mw.text.split( source, "%s+" )
                local show, start, support, unknown
                for i = 1, #words do
                    s = words[ i ]
                    if i == 1 then
                        start = s
                    end
                    support = Permit.builder[ s ]
                    if support == start  or
                       support == "*" then
                        Permit.builder[ s ] = true
                    elseif s:match( "^[1-9]%d?" ) and
                           Permit.builder.align then
                        Permit.builder.align = tonumber( s )
                    else
                        if unknown then
                            unknown = string.format( "%s %s", unknown, s )
                        else
                            unknown = s
                        end
                    end
                end -- i = 1, #words
                if unknown then
                    s = tostring( mw.html.create( "code" )
                                         :css( "white-space", "nowrap" )
                                         :wikitext( s ) )
                    Fault( "Unknown/misplaced format keyword " .. s )
                    source = false
                    start  = false
                end
                if start == "inline" then
                    if Permit.builder.half == true then
                        show = "inline half"
                        code = "{{_ |_=_}}"
                    elseif Permit.builder.grouped == true then
                        show = "inline grouped"
                        code = "{{_ | _=_}}"
                    elseif Permit.builder.spaced == true then
                        show = "inline spaced"
                        code = "{{_ | _ = _ }}"
                    end
                    if Permit.builder.newlines == true then
                        show = show or "inline"
                        code = code or "{{_|_=_}}"
                        show = show .. " newlines"
                        code = string.format( "N%sN", code )
                    end
                elseif start == "block" then
                    local space  = ""     -- amid "|" and name
                    local spaced = " "    -- preceding "="
                    local spacer = " "    -- following "="
                    local suffix = "N"    -- closing "}}" on new line
                    show = "block"
                    if Permit.builder.indent == true then
                        start = " "
                        show = "block indent"
                    else
                        start = ""
                    end
                    if Permit.builder.compressed == true then
                        spaced = ""
                        spacer = ""
                        show   = show .. " compressed"
                        if Permit.builder.last == true then
                            show = show .. " last"
                        else
                            suffix = ""
                        end
                    else
                        if Permit.builder.lead == true then
                            show  = show .. " lead"
                            space = " "
                        end
                        if type( Permit.builder.align ) ~= "string" then
                            local n
                            s = " align"
                            if Permit.builder.align == true then
                                n = 0
                                if type( Data.got ) == "table"  and
                                   type( Data.got.params ) == "table" then
                                    for k, v in pairs( Data.got.params ) do
                                        if type( v ) == "table"  and
                                           not v.deprecated  and
                                           type( k ) == "string" then
                                            k = mw.ustring.len( k )
                                            if k > n then
                                                n = k
                                            end
                                        end
                                    end -- for k, v
                                end
                            else
                                n = Permit.builder.align
                                if type( n ) == "number"  and  n > 1 then
                                    s = string.format( "%s %d", s, n )
                                else
                                    n = 0    -- How comes?
                                end
                            end
                            if n > 1 then
                                spaced = string.rep( "_",  n - 1 )  ..  " "
                            end
                            show = show .. s
                        elseif Permit.builder.after == true then
                            spaced = ""
                            show   = show .. " after"
                        elseif Permit.builder.dense == true then
                            spaced = ""
                            spacer = ""
                            show   = show .. " dense"
                        end
                        if Permit.builder.last == true then
                            suffix = spacer
                            show   = show .. " last"
                        end
                    end
                    code = string.format( "N{{_N%s|%s_%s=%s_%s}}N",
                                          start,
                                          space,
                                          spaced,
                                          spacer,
                                          suffix )
                    if show == "block" then
                        show = "block newlines"
                    end
                end
                if show then
                    r = mw.html.create( "span" )
                               :wikitext( show )
                end
            end
            if code then
                source = code:gsub( "N", "\n" )
                code   = mw.text.nowiki( code ):gsub( "N", "&#92;n" )
                code   = mw.html.create( "code" )
                                :css( "margin-left",  "1em" )
                                :css( "margin-right", "1em" )
                                :wikitext( code )
                if r then
                    r = mw.html.create( "span" )
                               :node( r )
                               :node( code )
                else
                    r = code
                end
            end
        end
        if source and Data.tag then
            Data.tag.format = source
        end
        return r
    end -- format()
    
    
    
    local function formatter()
        -- Build presented documentation
        -- Returns <div>
        local r = mw.html.create( "div" )
        local x = fashioned( Data.tree, true, r )
        local s
        if x then
            r = x
        end
        if Data.leading then
            local toc = mw.html.create( "div" )
            local shift
            if Config.suppressTOCnum then
                toc:addClass( Config.suppressTOCnum )
                if type( Config.stylesTOCnum ) == "string" then
                    local src = Config.stylesTOCnum .. "/styles.css"
                    s = TemplateData.frame:extensionTag( "templatestyles",
                                                         nil,
                                                         { src = src } )
                    r:newline()
                     :node( s )
                end
            end
            toc:addClass( "navigation-not-searchable" )
               :css( "margin-top", "0.5em" )
               :wikitext( "__TOC__" )
            if Data.sibling then
                local block = mw.html.create( "div" )
                if TemplateData.ltr then
                    shift = "right"
                else
                    shift = "left"
                end
                block:css( "float", shift )
                     :wikitext( Data.sibling )
                r:newline()
                 :node( block )
                 :newline()
            end
            r:newline()
             :node( toc )
             :newline()
            if shift then
                r:node( mw.html.create( "div" )
                               :css( "clear", shift ) )
                 :newline()
            end
        end
        s = features()
        if s then
            if Data.leading then
                r:node( mw.html.create( "h" .. Config.nested )
                               :wikitext( factory( "doc-params" ) ) )
                 :newline()
            end
            r:node( s )
        end
        if Data.shared then
            local global = mw.html.create( "div" )
                                  :attr( "id", "templatedata-global" )
            local shift
            if TemplateData.ltr then
                shift = "right"
            else
                shift = "left"
            end
            global:css( "float", shift )
                  :wikitext( string.format( "[[%s|%s]]",
                                            Data.shared, "Global" ) )
            r:newline()
             :node( global )
        end
        if Data.tree and Data.tree.format then
            local e = format()
            if e then
                local show = "Format"
                if Config.supportFormat then
                    show = string.format( "[[%s|%s]]",
                                          Config.supportFormat, show )
                end
                r:node( mw.html.create( "p" )
                               :addClass( "navigation-not-searchable" )
                               :wikitext( show .. ": " )
                               :node( e ) )
            end
        end
        return r
    end -- formatter()
    
    
    
    local function free()
        -- Remove JSON comment lines
        if Data.source:find( "//", 1, true ) then
            Data.source:gsub( "([{,\"'])(%s*\n%s*//.*\n%s*)([{},\"'])",
                              "%1%3" )
        end
    end -- free()
    
    
    
    local function full()
        -- Build survey table from JSON data, append invisible <templatedata>
        Data.div = mw.html.create( "div" )
                          :addClass( "mw-templatedata-doc-wrap" )
        if Permit.css.bg then
            Data.div:css( Permit.css.bg )
        end
        if Permit.css.fg then
            Data.div:css( Permit.css.fg )
        end
        focus()
        if Data.tag then
            if type( Data.got.params ) == "table" then
                for k, v in pairs( Data.got.params ) do
                    focus( k )
                end -- for k, v
                if Data.heirs then
                    fathers()
                end
            end
        end
        Data.div:node( formatter() )
        if not Data.lazy then
            Data.slim = flush()
            if TemplateData.frame then
                local div   = mw.html.create( "div" )
                local tdata = { [ 1 ] = "templatedata",
                                [ 2 ] = Data.slim }
                Data.strip = TemplateData.frame:callParserFunction( "#tag",
                                                                    tdata )
                div:wikitext( Data.strip )
                if Config.loudly then
                    Data.div:node( mw.html.create( "hr" )
                                          :css( { height = "7ex" } ) )
                else
                    div:css( "display", "none" )
                end
                Data.div:node( div )
            end
        end
        if Data.lasting then
            Fault( "deprecated type syntax" )
        end
        if Data.less then
            Fault( Config.solo )
        end
    end -- full()
    
    
    
    local function furnish( adapt, arglist )
        -- Analyze transclusion
        -- Parameter:
        --     adapt    -- table, #invoke parameters
        --     arglist  -- table, template parameters
        -- Returns string
        local source
        favorize()
        -- deprecated:
        for k, v in pairs( Config.basicCnf ) do
            if adapt[ k ]  and  adapt[ k ] ~= "" then
                Config[ v ] = adapt[ k ]
            end
        end -- for k, v
        if arglist.heading  and  arglist.heading:match( "^[3-6]$" ) then
            Config.nested = arglist.heading
        else
            Config.nested = "2"
        end
        Config.loudly = faculty( arglist.debug or adapt.debug )
        Data.lazy     = faculty( arglist.lazy )  and  not Config.loudly
        Data.leading  = faculty( arglist.TOC )
        if Data.leading and arglist.TOCsibling then
            Data.sibling = mw.text.trim( arglist.TOCsibling )
        end
        if arglist.lang then
            Data.slang = arglist.lang:lower()
        elseif adapt.lang then
            Data.slang = adapt.lang:lower()
        end
        if arglist.JSON then
            source = arglist.JSON
        elseif arglist.Global then
            source = TemplateData.getGlobalJSON( arglist.Global,
                                                 arglist.Local )
        elseif arglist[ 1 ] then
            local s     = mw.text.trim( arglist[ 1 ] )
            local start = s:sub( 1, 1 )
            if start == "<" then
                Data.strip = s
            elseif start == "{" then
                source = s
            elseif mw.ustring.sub( s, 1, 8 ) ==
                   mw.ustring.char( 127, 39, 34, 96, 85, 78, 73, 81 ) then
                Data.strip = s
            end
        end
        if type( arglist.vertical ) == "string"  and
           arglist.vertical:match( "^%d*%.?%d+[emprx]+$" ) then
            Data.scroll = arglist.vertical
        end
        if not source then
            Data.title = mw.title.getCurrentTitle()
            source = find()
            if not source  and
               not Data.title.text:match( Config.subpage ) then
                local s = string.format( Config.suffix,
                                         Data.title.prefixedText )
                Data.title = mw.title.new( s )
                if Data.title.exists then
                    source = find()
                end
            end
        end
        if not Data.lazy then
            if not Data.title then
                Data.title = mw.title.getCurrentTitle()
            end
            Data.lazy = Data.title.text:match( Config.subpage )
        end
        if type( source ) == "string" then
            TemplateData.getPlainJSON( source )
        end
        return finalize( faculty( arglist.source ) )
    end -- furnish()
    
    
    
    Failsafe.failsafe = function ( atleast )
        -- Retrieve versioning and check for compliance
        -- Precondition:
        --     atleast  -- string, with required version
        --                         or wikidata|item|~|@ or false
        -- Postcondition:
        --     Returns  string  -- with queried version/item, also if problem
        --              false   -- if appropriate
        -- 2020-08-17
        local since  = atleast
        local last   = ( since == "~" )
        local linked = ( since == "@" )
        local link   = ( since == "item" )
        local r
        if last  or  link  or  linked  or  since == "wikidata" then
            local item = Failsafe.item
            since = false
            if type( item ) == "number"  and  item > 0 then
                local suited = string.format( "Q%d", item )
                if link then
                    r = suited
                else
                    local entity = mw.wikibase.getEntity( suited )
                    if type( entity ) == "table" then
                        local seek = Failsafe.serialProperty or "P348"
                        local vsn  = entity:formatPropertyValues( seek )
                        if type( vsn ) == "table"  and
                           type( vsn.value ) == "string"  and
                           vsn.value ~= "" then
                            if last  and  vsn.value == Failsafe.serial then
                                r = false
                            elseif linked then
                                if mw.title.getCurrentTitle().prefixedText
                                   ==  mw.wikibase.getSitelink( suited ) then
                                    r = false
                                else
                                    r = suited
                                end
                            else
                                r = vsn.value
                            end
                        end
                    end
                end
            end
        end
        if type( r ) == "nil" then
            if not since  or  since <= Failsafe.serial then
                r = Failsafe.serial
            else
                r = false
            end
        end
        return r
    end -- Failsafe.failsafe()
    
    
    
    TemplateData.getGlobalJSON = function ( access, adapt )
        -- Retrieve TemplateData from a global repository (JSON)
        -- Parameter:
        --     access  -- string, with page specifier (on WikiMedia Commons)
        --     adapt   -- JSON string or table with local overrides
        -- Returns true, if succeeded
        local plugin = Fetch( "/global" )
        local r
        if type( plugin ) == "table"  and
           type( plugin.fetch ) == "function" then
            local s, got = plugin.fetch( access, adapt )
            if got then
                Data.got    = got
                Data.order  = got.paramOrder
                Data.shared = s
                r           = true
                full()
            else
                Fault( s )
            end
        end
        return r
    end -- TemplateData.getGlobalJSON()
    
    
    
    TemplateData.getPlainJSON = function ( adapt )
        -- Reduce enhanced JSON data to plain text localized JSON
        -- Parameter:
        --     adapt  -- string, with enhanced JSON
        -- Returns string, or not
        if type( adapt ) == "string" then
            local JSONutil = Fetch( "JSONutil", true )
            Data.source = adapt
            free()
            if JSONutil then
                local Multilingual = Fetch( "Multilingual", true )
                local f
                if Multilingual then
                    f = Multilingual.i18n
                end
                Data.got = JSONutil.fetch( Data.source, true, f )
            else
                local lucky
                lucky, Data.got = pcall( mw.text.jsonDecode, Data.source )
            end
            if type( Data.got ) == "table" then
                full()
            elseif not Data.strip then
                local scream = type( Data.got )
                if scream == "string" then
                    scream = Data.got
                else
                    scream = "Data.got: " .. scream
                end
                Fault( "fatal JSON error: " .. scream )
            end
        end
        return Data.slim
    end -- TemplateData.getPlainJSON()
    
    
    
    TemplateData.test = function ( adapt, arglist )
        TemplateData.frame = mw.getCurrentFrame()
        return furnish( adapt, arglist )
    end -- TemplateData.test()
    
    
    
    -- Export
    local p = { }
    
    p.f = function ( frame )
        -- Template call
        local lucky, r
        TemplateData.frame = frame
        lucky, r = pcall( furnish, frame.args, frame:getParent().args )
        if not lucky then
            Fault( "INTERNAL: " .. r )
            r = failures()
        end
        return r
    end -- p.f
    
    p.failsafe = function ( frame )
        -- Versioning interface
        local s = type( frame )
        local since
        if s == "table" then
            since = frame.args[ 1 ]
        elseif s == "string" then
            since = frame
        end
        if since then
            since = mw.text.trim( since )
            if since == "" then
                since = false
            end
        end
        return Failsafe.failsafe( since )  or  ""
    end -- p.failsafe
    
    p.TemplateData = function ()
        -- Module interface
        return TemplateData
    end
    
    return p
    
    Cookies help us deliver our services. By using our services, you agree to our use of cookies.
    Cookies help us deliver our services. By using our services, you agree to our use of cookies.