Modul:MediaWikiGadgetDefinition

local MediaWikiGadgetDefinition = { suite  = "MediaWikiGadgetDefinition",
                                    serial = "2024-03-03",
                                    item   = 111520596 }
--[==[
Documentation, validation and comparison of MediaWiki gadget definitions
]==]


local Failsafe     = MediaWikiGadgetDefinition
local GlobalMod    = MediaWikiGadgetDefinition



local ACTIVE   = { }
local COMMONS  = { }
local ERROR    = { }
local GLOBAL   = { }
local HTML     = { bgcolorSleeping = "D0D0D0" }
local I18N     = { }
local OPTIONS  = { }
local ROW      = { comment = { live   = false,
                               suffix = "#" } }
local TEMPLATE = { }
local VALUES   = { }



COMMONS.tabs =    { config        = "MediaWikiGadgetDefinition",
                    actions       = "MediaWiki/URLactions",
                    contentModels = "MediaWiki/Contentmodels",
                    skins         = "MediaWiki/SkinsWMF",
                    modules       = "MediaWiki/ResourceLoaderModules"
                  }
OPTIONS.config  = { { sign = "ID",
                      live = true,
                      mode = 0,
                      type = "string",
                      scan = "rootpage" },
                    { sign = "ResourceLoader",
                      live = true,
                      mode = 1,
                      type = "boolean" },
                    { sign = "default",
                      live = true,
                      mode = 1,
                      type = "boolean" },
                    { sign = "hidden",
                      live = true,
                      mode = 1,
                      type = "boolean" },
                    { sign = "package",
                      live = true,
                      mode = 1,
                      type = "boolean" },
                    { sign = "targets",
                      live = true,
                      mode = 1,
                      type = { "string" },
                      scan = "array" },
                    { sign = "skins",
                      live = true,
                      mode = 1,
                      type = { "string" },
                      scan = "array" },
                    { sign = "rights",
                      live = true,
                      mode = 1,
                      type = { "string" },
                      scan = "letter-id" },
                    { sign = "actions",
                      live = true,
                      mode = 1,
                      type = { "string" },
                      scan = "array" },
                    { sign = "namespaces",
                      live = true,
                      mode = 1,
                      type = { "number" },
                      scan = "namespace" },
                    { sign = "specials",
                      live = false,
                      mode = 1,
                      type = { "string" },
                      scan = "LetterID" },
                    { sign = "contentModels",
                      live = true,
                      mode = 1,
                      type = { "string" },
                      scan = "array" },
                    { sign = "pageprops",
                      live = false,
                      mode = 1,
                      type = { "string" },
                      scan = "letter-_ID" },
                    { sign = "transcludes",
                      live = false,
                      mode = 1,
                      type = { "string" },
                      scan = "page" },
                    { sign = "groups",
                      live = false,
                      mode = 1,
                      type = { "string" },
                      scan = "letter-id" },
                    { sign = "userlangs",
                      live = false,
                      mode = 1,
                      type = { "string" },
                      scan = "lang" },
                    { sign = "contentlangs",
                      live = false,
                      mode = 1,
                      type = { "string" },
                      scan = "lang" },
                    { sign = "category",
                      live = false,
                      mode = 1,
                      type = "string",
                      scan = "page" },
                    { sign = "categories",
                      live = true,
                      mode = 1,
                      type = { "string" },
                      scan = "page" },
                    { sign = "type",
                      live = true,
                      mode = 1,
                      type = "string",
                      scan = "radio" },
                    { sign = "top",
                      live = true,
                      mode = 1,
                      type = "boolean" },
                    { sign = "supportsUrlLoad",
                      live = true,
                      mode = 1,
                      type = "boolean" },
                    { sign = "dependencies",
                      live = true,
                      mode = 2,
                      type = { "string" },
                      lone = true,
                      scan = "letter.id" },
                    { sign = "scripts",
                      live = true,
                      mode = 3,
                      type = { "string" },
                      scan = "page" },
                    { sign = "styles",
                      live = true,
                      mode = 3,
                      type = { "string" },
                      scan = "page" },
                    { sign = "datas",
                      live = true,
                      mode = 3,
                      type = { "string" },
                      scan = "page" },
                    { sign = "peers",
                      live = true,
                      mode = 3,
                      type = { "string" },
                      lone = true },
                    { sign = "messages",
                      live = false,
                      mode = 3,
                      type = { "string" },
                      lone = true,
                      scan = "rootpage" },
                    { sign = "Comment",
                      live = false,
                      mode = 4,
                      type = "string" }
                  }
VALUES.arrays =   { actions       = false,
                    contentModels = false,
                    skins         = false,
                    targets       = { { "desktop", true },
                                      { "mobile",  true } }
                  }
VALUES.radio =    { type = { { "general", true },
                             { "styles",  true } }
                  }
VALUES.validate = { LetterID       = { { "%u%a+" } },
                    lang           = { { "%l%l%l?",
                                         "%l%l%l?-%a+" } },
                    ["letter-_ID"] = { { "%l[%-_%a]+%a" },
                                       { "[%-_][%-_]" } },
                    ["letter-id"]  = { { "%l[%-%l]+%l" },
                                       { "--" } },
                    ["letter.id"]  = { { "%l[%.%l]+%l" },
                                       { "%.%." } },
                    namespace      = { { "-?[1-9]%d*" } },
                    page           = { { "[^#%[%]<>{}|]+" },
                                       { "[ _][ _]" } },
                    rootpage       = { { "[^/#%[%]<>{}|]+" },
                                       { "[ _][ _]" } }
                  }
I18N.texts =
    { ActiveSpec           = { en = "Active specification.",
                               de = "Aktive Spezifikation." },
      Assigned             = { en = "Assigned",
                               de = "Zuweisung" },
      AtLine               = { en = "at line",
                               de = "in Zeile" },
      BadAssigned          = { en = "assignment to sole boolean",
                               de = "Wertzuweisung an boolean" },
      BadLines             = { en = "Bad definition lines" },
      BadNamespace         = { en = "Bad namespace",
                               de = "Namensraum ungültig" },
      BadTemplateArg       = { en = "Template argument no string",
                               de = "Vorlagenparameter kein string" },
      BadTemplateArgs      = { en = "Template arguments no table",
                               de = "Vorlagenparameter keine table" },
      defaultTrue          = { en = "default for all",
                               de = "Vorgabe für alle" },
      defaultFalse         = { en = "activation needed",
                               de = "benötigt Aktivierung" },
      Detail               = { en = "Property",
                               de = "Eigenschaft" },
      hiddenTrue           = { en = "hidden",
                               de = "versteckt" },
      hiddenFalse          = { en = "configurable",
                               de = "konfigurierbar" },
      IDmissing            = { en = "ID missing",
                               de = "ID erforderlich" },
      InvalidHeadID        = { en = "Invalid headline ID",
                               de = "Ungültige Überschrift-ID" },
      InvalidHeadline      = { en = "Invalid headline",
                               de = "Ungültige Überschrift" },
      InvalidID            = { en = "Invalid gadget ID",
                               de = "Ungültige Gadget ID" },
      InvalidResource      = { en = "Invalid resource page name",
                               de = "Seitenname der Ressource ungültig" },
      InvalidScope         = { en = "Scope invalid",
                               de = "Ungültiger Scope" },
      MissingValue         = { en = "Missing value",
                               de = "Wert fehlt" },
      NoClosingBracket     = { en = "Closing bracket missing",
                               de = "Schließende Klammer fehlt" },
      NoCommentsYet        = { en = "Comments not yet available",
                               de = "Kommentare noch nicht möglich" },
      NoGadgetID           = { en = "Gadget ID missing",
                               de = "Gadget ID erforderlich" },
      NoID                 = { en = "Missing identifier",
                               de = "Bezeichner fehlt" },
      NoOptionID           = { en = "Option name missing",
                               de = "Optionsname fehlt" },
      NoResources          = { en = "No resource page name",
                               de = "Kein Seitenname einer Ressource" },
      packageTrue          = { en = "package support",
                               de = "package-Unterstützung" },
      packageFalse         = { en = "no package support",
                               de = "keine package-Unterstützung" },
      RepeatedID           = { en = "Gadget ID repeated",
                               de = "Gadget ID wiederholt" },
      RepeatedOption       = { en = "Option name repeated",
                               de = "Optionsname wiederholt" },
      ResourceLoaderTrue   = { en = "supported",
                               de = "unterstützt" },
      ResourceLoaderFalse  = { en = "not supported",
                               de = "nicht unterstützt" },
      SpecMismatch         = { en = "Specification mismatch:",
                               de = "Spezifikation abweichend:" },
      supportsUrlLoadTrue  = { en = "URL loading supported",
                               de = "Laden per URL unterstützt" },
      supportsUrlLoadFalse = { en = "URL loading not supported",
                               de = "Laden per URL nicht unterstützt" },
      topTrue              = { en = "top loading",
                               de = "top-Laden" },
      topFalse             = { en = "regular loading sequence",
                               de = "normale Ladereihenfolge" },
      UnknownOptionID      = { en = "Unknown option name",
                               de = "Optionsname unbekannt" },
      Whitespace           = { en = "Superfluous whitespace",
                               de = "Überflüssiger Whitespace" }
    }



local find = function ( area, at )
    -- Check for existing page
    -- Precondition:
    --     area    -- number, of namespace, or not
    --     at      -- string, with name of page, or mw.title
    -- Postcondition:
    --     Returns true if page existing
    local r, t
    if area then
        t = mw.title.makeTitle( area, at )
    elseif type( at ) == "table" then
        t = at
    end
    if t and  t.protectionLevels.edit then
        r = true
    end
    return r
end -- find()



local foreignModule = function ( access, advanced, append, alt, alert )
    -- Fetch global module
    -- Precondition:
    --     access    -- string, with name of base module
    --     advanced  -- true, for require(); else mw.loadData()
    --     append    -- string, with subpage part, if any; or false
    --     alt       -- number, of wikidata item of root; or false
    --     alert     -- true, for throwing error on data problem
    -- Postcondition:
    --     Returns whatever, probably table
    -- 2019-10-29
    local storage = access
    local finer = function ()
                      if append then
                          storage = string.format( "%s/%s",
                                                   storage,
                                                   append )
                      end
                  end
    local fun, lucky, r, suited
    if advanced then
        fun = require
    else
        fun = mw.loadData
    end
    GlobalMod.globalModules = GlobalMod.globalModules or { }
    suited = GlobalMod.globalModules[ access ]
    if not suited then
        finer()
        lucky, r = pcall( fun,  "Module:" .. storage )
    end
    if not lucky then
        if not suited  and
           type( alt ) == "number"  and
           alt > 0 then
            suited = string.format( "Q%d", alt )
            suited = mw.wikibase.getSitelink( suited )
            GlobalMod.globalModules[ access ] = suited or true
        end
        if type( suited ) == "string" then
            storage = suited
            finer()
            lucky, r = pcall( fun, storage )
        end
        if not lucky and alert then
            error( "Missing or invalid page: " .. storage, 0 )
        end
    end
    return r
end -- foreignModule()



ACTIVE.first = function ()
    -- Initialize site effective definitions
    local source = "Gadgets-definition"
    local ns, s, t
    ACTIVE.start = "Gadget-"
    if GLOBAL.scope == "site" then
        s = GLOBAL.config["2302"]
        if type( s ) == "string"  and
           s ~= ""  and
           s ~= "-" then
            ns           = 2302
            source       = s
            ACTIVE.ns    = 2300
            ACTIVE.start = ""
        else
            ns = 8
        end
    elseif GLOBAL.scope == "global" then
    elseif GLOBAL.scope == "text" then
    else
        ns, s = GLOBAL.focus( GLOBAL.scope )
        if ns then
            ACTIVE.ns = ns
            if s:sub( -1, 1 ) ~= "/" then
                s = s .. "/"
            end
            source       = s .. source
            ACTIVE.start = s .. "Gadget-"
        end
    end
    if ns then
        t             = mw.title.makeTitle( ns, source )
        ACTIVE.ns     = ACTIVE.ns or ns
        ACTIVE.source = t.prefixedText
        ACTIVE.live   = find( false, t )
        if ACTIVE.live then
            ACTIVE.story = GLOBAL.frame():expandTemplate{ title = t }
            ACTIVE.title = t
        end
    end
end  -- ACTIVE.first()



COMMONS.factory = function ( all, access )
    -- Retrieve global configuration
    -- Precondition:
    --     all       -- table, with raw tabular data
    --     access    -- string, with keyword
    -- Postcondition:
    --     Returns table, with configuration data
    local r = { }
    local t = { }
    local e, s, v
    table.insert( t, "string" )
    if access == "modules" then
        table.insert( t, "string" )
    elseif access == "actions" then
        table.insert( t, "boolean" )
    elseif access == "contentModels" then
    elseif access == "skins" then
        table.insert( t, "boolean" )
    end
    for i = 1, #all do
        e = all[ i ]
        if type( e ) == "table"  and
           type( e[ 1 ] ) == "string" then
            s = mw.text.trim( e[ 1 ] )
            if s ~= ""  and
               not  s:find( "[ \"\n=&|{}%[%]<>#]" ) then
                v = e[ 2 ]
                if t[ 2 ] then
                    if type( v ) ~= t[ 2 ] then
                        s = false
                    end
                elseif access == "contentModels" then
                    v = true
                end
                if s then
                    if access == "modules" then
                        r[ s ] = v
                    else
                        e = { }
                        table.insert( e, s )
                        table.insert( e, v )
                        table.insert( r, e )
                    end
                end
            end
        end
    end -- for i
    return r
end  -- COMMONS.factory()



COMMONS.fetch = function ( access )
    -- Retrieve global configuration
    -- Precondition:
    --     access    -- string, with keyword
    -- Postcondition:
    --     Returns table, with configuration data, or true if unavailable
    COMMONS.tabDATA = COMMONS.tabDATA  or  { }
    if not COMMONS.tabDATA[ access ] then
        local storage = COMMONS.tabs[ access ]
        local v
        if storage then
            local lucky, data
            storage = string.format( "%s.tab", storage )
            --lucky, data = pcall( mw.ext.data.get, storage )
            if type( data ) == "table" then
                data = data.data
                if type( data ) == "table" then
                    v = data
                end
            end
        end
        if v then
            v = COMMONS.factory( v, access )
        else
            v = true
        end
        COMMONS.tabDATA[ access ] = v
    end
    return COMMONS.tabDATA[ access ]
end  -- COMMONS.fetch()



ERROR.fault = function ( about, area )
    -- Register an error
    -- Precondition:
    --     about    -- string, with message
    --     area     -- string, with field, or not
    local e = { }
    ERROR.errors = ERROR.errors  or  { }
    table.insert( e, about )
    table.insert( e, area )
    table.insert( ERROR.errors, e )
end  -- ERROR.fault()



ERROR.flat = function ( about, align )
    -- Format code
    -- Precondition:
    --     about    -- string, with message
    --     align    -- true, if string requested
    -- Postcondition:
    --     Returns mw.html, or string
    local r = mw.html.create( "code" )
                     :wikitext( mw.text.nowiki( about ) )
    if align then
        r = tostring( r )
    end
    return r
end  -- ERROR.flat()




ERROR.full = function ()
    -- Retrieve list of all error messages
    -- Postcondition:
    --     Returns mw.html, or not
    local r
    return r
end  -- ERROR.full()



GLOBAL.features = function ()
    -- Retrieve local configuration
    -- Postcondition:
    --     Configuration updated where requested
    local site = GLOBAL.frame():getTitle() .. "/local"
    local seek = string.format( "^%s:(.+)$",
                                mw.site.namespaces[ 828 ].name )
    site = site:match( seek, 1 )
    if find( 828, site ) then
        local lucky, config = pcall( mw.loadData, site )
        if type( config ) == "table" then
            if type( config.modules ) == "table" then
                for k, v in pairs( config.modules ) do
                    GLOBAL.modules[ k ] = v
                end -- for k, v
            end
            if type( config.actions ) == "table" then
                for k, v in pairs( config.actions ) do
                    VALUES.arrays.actions[ k ] = v
                end -- for k, v
            end
            if type( config.IDexceptions ) == "table" then
                for k, v in pairs( config.IDexceptions ) do
                    GLOBAL.IDexceptions[ k ] = v
                end -- for k, v
            end
        end
    end
end  -- GLOBAL.features()



GLOBAL.first = function ()
    -- Initialize global and local configuration, if not yet done
    -- Postcondition:
    --     Configuration up to date
    if not GLOBAL.config then
        local config = COMMONS.fetch( "config" )
        if type( config ) ~= "table" then
            config = { }
        end
        GLOBAL.config = config
        VALUES.full()
        config = COMMONS.fetch( "modules" )
        if type( config ) ~= "table" then
            config = { }
        end
        GLOBAL.modules = config
        GLOBAL.IDexceptions = { }
        GLOBAL.features()
        OPTIONS.first()
    end
end  -- GLOBAL.first()



GLOBAL.focus = function ( area )
    -- Analyze page name in name space
    -- Precondition:
    --     area    -- string, with page name, not in gadget spaces
    -- Postcondition:
    --     Returns:
    --         1. namespace number
    --         2. normalized page title
    local i = area:find( ":", 1, true )
    local rNS, rT
    if i then
        local s
        if i == 1 then
            rNS = 0
            rT  = area:sub( i + 1 )
        else
            local q
            s = mw.text.trim( area:sub( 1,  i - 1 ) )
            q = mw.site.namespaces[ s ]
            if q  and
               q.id ~= 8  and
               q.id ~= 2300  and
               q.id ~= 2302 then
                rNS = q.id
            end
        end
        if rNS then
            s  = mw.text.trim( area:sub( i + 1 ) )
            rT = string.format( "%s:%s",
                                mw.site.namespaces[ rNS ].name,
                                s )
        end
    else
        rNS = 0
        rT  = area
    end
    return  rNS, rT
end  -- GLOBAL.focus()



GLOBAL.frame = function ( frame )
    -- Retrieve and memorize frame object
    -- Precondition:
    --     frame    -- frame object, or not
    -- Postcondition:
    --     Returns frame object
    if frame then
        GLOBAL.Frame = frame
    end
    if not GLOBAL.Frame then
        GLOBAL.Frame = mw.getCurrentFrame()
    end
    return GLOBAL.Frame
end  -- GLOBAL.frame()



HTML.feature = function ( at, assigned, about, fault )
    -- Format single gadget option table row
    -- Precondition:
    --     at          -- string, with gadget option name
    --     assigned    -- gadget option value
    --     about       -- table, with gadget option configuration
    --     fault       -- function, as error handler
    -- Postcondition:
    --     Returns mw.html object <tr>
    local r   = mw.html.create( "tr" )
    local td1 = mw.html.create( "td" )
    local td2 = mw.html.create( "td" )
    local e, ns, s
    td1:wikitext( at )
    if at:sub( 1, 1 ) == "-" then
        s = at:sub( 2 )
        td1:attr( "data-sort-value",  s .. "-" )
    else
        s = at
    end
    if about.type == "boolean" then
        if assigned then
            s = s .. "True"
        else
            s = s .. "False"
        end
        td2:wikitext( I18N.fetch( s ) )
    elseif type( about.type ) == "table"  and
           type( assigned ) == "table" then
        if s == "targets"  or
           s == "skins"  or
           s == "rights"  or
           s == "actions"  or
           s == "contentModels"  or
           s == "pageprops"  or
           s == "groups"  or
           s == "type" then
            for i = 1, #assigned do
                e = mw.html.create( "code" )
                           :css( "margin-right", "0.4em" )
                           :css( "white-space", "nowrap" )
                           :wikitext( assigned[ i ] )
                td2:node( e )
            end -- for i
        elseif s == "namespaces" then
            local q
            for i = 1, #assigned do
                s  = assigned[ i ]
                ns = tonumber( s )
                e  = mw.html.create( "code" )
                            :css( "margin-right", "0.4em" )
                            :wikitext( s )
                q  = mw.site.namespaces[ ns ]
                if q then
                    e:attr( "title",   q.name .. ":" )
                else
                    e:css( "background-color", "#FF0000" )
                    e:css( "color", "#FFFF00" )
                    e:css( "font-weight", "bold" )
                end
                td2:node( e )
            end -- for i
        elseif s == "specials" then
            local space = mw.site.namespaces.special.name
            local k     = #space + 2
            for i = 1, #assigned do
                s = mw.text.nowiki( assigned[ i ] )
                e = mw.html.create( "code" )
                           :css( "margin-right", "0.4em" )
                           :wikitext( s )
                s = GLOBAL.frame():callParserFunction( "#special",
                                                       assigned[ i ] )
                e:attr( "title",  s:sub( k ) )
                td2:node( e )
            end -- for i
        elseif s == "transcludes"  or
               s == "categories" then
            local link14 = ( s == "categories" )
            for i = 1, #assigned do
                s = mw.text.nowiki( assigned[ i ] )
                if link14 then
                    s = string.format( ":%s:%s",
                                       mw.site.namespaces[14].name,
                                       s )
                end
                s = string.format( "[[%s]]", s )
                e = mw.html.create( "span" )
                           :css( "margin-right", "0.4em" )
                           :css( "white-space", "nowrap" )
                e:wikitext( s )
                td2:node( e )
            end -- for i
        elseif s == "userlangs"  or
               s == "contentlangs" then
        elseif s == "dependencies" then
            local hash = COMMONS.tabDATA.modules
            if type( hash ) ~= "table" then
                hash = false
            end
            for i = 1, #assigned do
                s = mw.text.nowiki( assigned[ i ] )
                e = mw.html.create( "span" )
                           :css( "margin-right", "0.4em" )
                           :css( "white-space", "nowrap" )
                if hash and hash[ s ] then
                    s = string.format( "[[%s|%s]]",  hash[ s ],  s )
                end
                e:wikitext( s )
                td2:node( e )
            end -- for i
        elseif s == "scripts"  or
               s == "styles"  or
               s == "datas"  or
               s == "peers" then
            local t
            for i = 1, #assigned do
                s = mw.text.nowiki( assigned[ i ] )
                t = mw.title.makeTitle( ACTIVE.ns,
                                        ACTIVE.start .. s )
                e = mw.html.create( "span" )
                           :css( "margin-right", "0.4em" )
                           :css( "white-space", "nowrap" )
                           :wikitext( string.format( "[[%s|%s]]",
                                                     t.prefixedText,
                                                     s ) )
                td2:node( e )
            end -- for i
        elseif s == "messages" then
        else
            td2:wikitext( tostring(assigned) )
        end
    else
        td2:wikitext( tostring(assigned) )
    end
    if not about.live then
        r:css( "background-color",  "#" .. HTML.bgcolorSleeping )
    end
    r:node( td1 )
     :node( td2 )
    return r
end  -- HTML.feature()



HTML.features = function ( assigned )
    -- Format gadget options table
    -- Precondition:
    --     assigned    -- table, with gadget options
    -- Postcondition:
    --     Returns mw.html object <table>
    local r = mw.html.create( "table" )
    local n = 0
    local fault = function ( add )
                      assigned.ERRORS = assigned.ERRORS  or  { }
                      table.insert( assigned.ERRORS, add )
                  end
    local e, o, s, td, th1, th2
    r:addClass( "wikitable" )
    if assigned.ID then
        local t
        s = mw.text.nowiki( assigned.ID )
        t = mw.title.makeTitle( 8,  "Gadget-" .. s )
        e = mw.html.create( "caption" )
                   :addClass( "mw-code" )
                   :attr( "id", s )
                   :wikitext( string.format( "[[%s|%s]]",
                                             t.prefixedText, s ) )
        r:newline()
         :node( e )
        if find( false, t ) then
            s  = GLOBAL.frame():callParserFunction( "int",
                                                    t.text )
            td = mw.html.create( "td" )
                        :attr( "colspan", "2" )
                        :wikitext( s )
            r:newline()
             :node( mw.html.create( "tr" )
                           :addClass( "sortbottom" )
                           :node( td ) )
        end
    else
        fault( I18N.fetch( "NoID" ) )
    end
    th1 = mw.html.create( "th" )
                 :wikitext( I18N.fetch( "Detail" ) )
    th2 = mw.html.create( "th" )
                 :addClass( "unsortable" )
                 :wikitext( I18N.fetch( "Assigned" ) )
    r:newline()
     :node( mw.html.create( "tr" )
                   :node( th1 )
                   :node( th2 ) )
    for i = 2,  #OPTIONS.order - 1 do
        o = OPTIONS.order[ i ]
        s = o.sign
        e = assigned[ s ]
        if type( e ) ~= "nil" then
            r:newline()
             :node( HTML.feature( s, e, o, fault ) )
            n = n + 1
        end
    end -- for i
    if n > 1 then
        r:addClass( "sortable" )
    end
    if assigned.Comment then
        td = mw.html.create( "td" )
                    :addClass( "sortbottom" )
                    :attr( "colspan", "2" )
                    :wikitext( assigned.Comment )
        r:newline()
         :node( mw.html.create( "tr" )
                       :node( td ) )
    end
    if assigned.ERRORS then
        e = mw.html.create( "ul" )
        for i = 1, #assigned.ERRORS do
            e:node( mw.html.create( "li" )
                           :wikitext( assigned.ERRORS[ i ] ) )
        end -- for i
        td = mw.html.create( "td" )
                    :addClass( "error" )
                    :addClass( "sortbottom" )
                    :attr( "colspan", "2" )
                    :node( e )
        r:newline()
         :node( mw.html.create( "tr" )
                       :node( td ) )
    end
    r:newline()
    return r
end  -- HTML.features()



HTML.flat = function ( apply )
    -- Format coded value
    -- Precondition:
    --     assigned    -- string, with value
    -- Postcondition:
    --     Returns mw.html object <div>
    local r = mw.html.create( "div" )
                     :addClass( "mw-code" )
                     :css( "white-space", "nowrap" )
                     :css( "overflow", "auto" )
                     :css( "padding-right", "2em" )
                     :wikitext( mw.text.nowiki( apply ) )
    return r
end  -- HTML.flat()



HTML.folder = function ( assigned )
    -- Build gadget options table with resulting definition row and link
    -- Precondition:
    --     assigned    -- table, with gadget options
    -- Postcondition:
    --     Returns mw.html object <div>
    local e = mw.html.create( "div" )
                     :wikitext( string.format( "[[%s]]",
                                               ACTIVE.source ) )
    local g = ROW.fiat( assigned )
    local r = mw.html.create( "div" )
                     :node( HTML.features( assigned ) )
                     :newline()
                     :node( HTML.flat( g ) )
                     :newline()
                     :node( e )
    local d = ROW.found( assigned.ID )
    if d then
        d = OPTIONS.fair( assigned, d )
        e = mw.html.create( "div" )
        if d then
            local diff = mw.html.create( "div" )
            e:css( "color", "#FF0000" )
             :css( "font-weight", "bold" )
             :wikitext( I18N.fetch( "SpecMismatch" ) )
            diff:css( "margin-left", "1em" )
                :css( "margin-right", "1em" )
            for i = 1, #d do
                diff:node( mw.html.create( "code" )
                                  :wikitext( mw.text.nowiki( d[ i ] ) ) )
            end -- for i
            e:node( diff )
        else
            e:css( "color", "#00A000" )
             :css( "font-weight", "bold" )
             :wikitext( I18N.fetch( "ActiveSpec" ) )
        end
        r:newline()
         :node( e )
    end
    return r
end  -- HTML.folder()



I18N.fetch = function ( ask )
    -- Retrieve localized text
    -- Precondition:
    --     ask    -- string, with keyword
    -- Postcondition:
    --     Returns string, with translation
    local e = I18N.texts[ ask ]
    local r
    if e then
        if not I18N.slang then
            I18N.slang = mw.language.getContentLanguage():getCode()
                                                         :lower()
        end
        r = e[ I18N.slang ]
        if not e then
            r = e.en
        end
    end
    return r  or  string.format( "I18N{%s}", ask )
end  -- I18N.fetch()



OPTIONS.fair = function ( a1, a2 )
    -- Compare two definitions
    -- Precondition:
    --     a1    -- table, with definitions
    --     a2    -- table, with definitions
    -- Postcondition:
    --     Returns hash table, of mismatching assignments
    local l, o, r, s, v1, v2
    for i = 1, #OPTIONS.order do
        o = OPTIONS.order[ i ]
        if o.live then
            s  = o.sign
            v1 = a1[ s ]
            v2 = a2[ s ]
            if v1  and  v2  and  v1 ~= v2 then
                if type( v1 ) == "table"  and
                   type( v2 ) == "table" then
                    if #v1 == #v2 then
                        l = false
                        for k = 1, #v1 do
                            if v1[ k ] ~= v2[ k ] then
                                l = true
                                break -- for k
                            end
                        end -- for k
                        if not l then
                            v1 = false
                            v2 = false
                        end
                    end
                end
                if v1 ~= v2 then
                    r = r  or  { }
                    o = { }
                    table.insert( o, v1 )
                    table.insert( o, v2 )
                    r[ s ] = o
                end
            end
        end
    end -- for i
    return r
end  -- OPTIONS.fair()



OPTIONS.first = function ()
    -- Initialize gadget options configuration
    -- Postcondition:
    --     Configuration up to date
    local e, s, u
    OPTIONS.hash  = { }
    OPTIONS.order = { }
    for i = #OPTIONS.config, 1, -1 do
        e = OPTIONS.config[ i ]
        if not e.live then
            s = GLOBAL[ e.sign ]
            if s == "1"  or
               s == true then
                e.live = true
            end
        end
        s = e.sign
        OPTIONS.hash[ s ] = e
        if e.mode then
            if e.mode == 1  and
               not e.lone  and
               e.type ~= "boolean" then
                s = "-" .. e.sign
                if not OPTIONS.hash[ s ] then
                    u = { sign = s,
                          live = false,
                          mode = 1,
                          type = e.type,
                          scan = e.scan }
                    table.insert( OPTIONS.order, 1, u )
                    OPTIONS.hash[ s ] = u
                    table.insert( OPTIONS.config, u )
                end
            end
            table.insert( OPTIONS.order, 1, e )
        end
    end -- for i
end  -- OPTIONS.first()



ROW.failed = function ( assigned, at )
    -- Create line number referenced error in Gadgets-definition
    -- Precondition:
    --     assigned    -- string, with bad data
    --     at          -- number, with line number
    -- Postcondition:
    --     Returns string, with formatted message, leading space
    local r
    if type( assigned ) == "string"  and
       type( at ) == "number" then
        r = mw.html.create( "span" )
                   :node( ERROR.flat( assigned ) )
                   :wikitext( string.format( " %s ",
                                             I18N.fetch( "AtLine" ) ) )
                   :node( mw.html.create( "code" )
                                 :wikitext( string.format( "%d", at ) ) )
        r = " " .. tostring( r )
    end
    return r or ""
end  -- ROW.failed()



ROW.fetch = function ( all, access )
    -- Parse line in Gadgets-definition
    -- Precondition:
    --     all       -- table, with all line specifications
    --                  .hash  -- table, with gadget ID mapping
    --                  .rows  -- sequence table, with line content
    --     access    -- number or string, to be accessed
    -- Postcondition:
    --     Returns table, with line properties
    local r
    if type( all ) == "table" then
        local errors, slot
        local fault = function ( add )
                              errors = errors  or  { }
                              local s = add
                              if slot then
                                  s = s .. ROW.failed( slot,
                                                       all.hash[ slot ] )
                              end
                              table.insert( errors, s )
                      end
        local s = type( access )
        local n
        if s == "number" then
            n = access
        elseif s == "string" then
            n    = all.hash[ access ]
            slot = access
        end
        if n then
            local story = all.rows[ n ]
            if type( story ) == "string"  and
               story ~= "" then
                local i, j, o, opts, ress
                story = story:gsub( "^%s*%*%s*", "" )
                r = { }
                if type( ROW.comment ) == "table"  and
                   type( ROW.comment.suffix ) == "string"  and
                   ROW.comment.suffix ~= "" then
                    i = story:find( ROW.comment.suffix )
                    if i then
                        if i == 1 then
                            r.Comment = story:sub( 2 )
                        else
                            r.Comment = story:sub( i + 1 )
                            story     = story:sub( 1,  i - 1 )
                            story     = mw.text.trim( story )
                        end
                        r.Comment = mw.text.trim( r.Comment )
                    end
                end
                if r.Comment  and
                   not  ( ROW.comment.live or ROW.lazy ) then
                    fault( I18N.fetch( "NoCommentsYet" ) )
                end
                if not ROW.lazy then
                    fault( I18N.fetch( "Whitespace" ) )
                end
                i = story:find( "[", 1, true )
                if i then
                    if i > 1 then
                        slot = mw.text.trim( story:sub( 1,  i - 1 ) )
                        j = story:find( "]",  i + 1,  true )
                        if j then
                            s    = mw.text.trim( story:sub( i + 1,
                                                            j - 1 ) )
                            opts = mw.text.split( s, "%s*|%s*" )
                            if #opts < 1 then
                                opts = false
                            end
                            story = mw.text.trim( story:sub( j + 1 ) )
                        else
                            fault( I18N.fetch( "NoClosingBracket" ) )
                            story = ""
                        end
                    else
                        slot  = ""
                        story = ""
                    end
                else
                    i = story:find( "|", 1, true )
                    if i then
                        slot  = mw.text.trim( story:sub( 1,  i - 1 ) )
                        story = mw.text.trim( story:sub( i + 1 ) )
                    else
                        slot  = ""
                        story = ""
                    end
                end
                if slot == "" then
                    fault( I18N.fetch( "NoGadgetID" ) )
                    ress = { }
                else
                    ress = VALUES.feed( story, "%s*|%s*" )  or  { }
                end
                if opts then
                    local set, v
                    for i = 1, #opts do
                        s = mw.text.trim( opts[ i ] )
                        j = s:find( "=", i, true )
                        if j then
                            if j == 1 then
                                set = false
                                o   = false
                                s   = string.format( "%s %s",
                                             ERROR.flat( slot, true ),
                                             I18N.fetch( "NoOptionID" ) )
                                fault( s )
                            else
                                set = s:sub( j + 1 )
                                set = mw.text.trim( set )
                                s   = s:sub( 1,  j - 1 )
                                s   = mw.text.trim( s )
                                o   = OPTIONS.hash[ s ]
                            end
                        else
                            o   = OPTIONS.hash[ s ]
                            set = false
                        end
                        if o then
                            if r[ o.sign ] then
                                s = string.format( "%s&#124;%s",
                                                   slot,
                                                   o.sign )
                                s = string.format( "%s %s",
                                         ERROR.flat( s, true ),
                                         I18N.fetch( "RepeatedOption" ) )
                                fault( s )
                            elseif type( o.type ) == "table" then
                                if set then
                                    v = VALUES.feed( set,
                                                     ",",
                                                     o.type[ 1 ]
                                                            == "number" )
                                else
                                    v = false
                                end
                                if type( v ) == "table" then
                                    if o.sign == "peers" then
                                        ROW.fill( r, v, "peers", fault )
                                    end
                                else
                                    s = string.format( "%s&#124;%s=",
                                                       slot,
                                                       o.sign )
                                    s = string.format( "%s %s",
                                                       s,
                                           I18N.fetch( "MissingValue" ) )
                                    fault( s )
                                end
                            elseif o.type == "boolean" then
                                if set then
                                    v = false
                                    s = string.format( "%s&#124;%s=",
                                                       slot,
                                                       o.sign )
                                    s = string.format( "%s %s",
                                                       s,
                                           I18N.fetch( "BadAssigned" ) )
                                else
                                    v = true
                                end
                            elseif o.type == "string" then
                                if set then
                                    s = string.format( "%s&#124;%s=",
                                                       slot,
                                                       o.sign )
                                    s = string.format( "%s %s",
                                                       s,
                                           I18N.fetch( "MissingValue" ) )
                                    fault( s )
                                else
                                    v = set
                                end
                            end
                            if v then
                                r[ o.sign ] = v
                            end
                        else
                            s = string.format( "%s %s",
                                         I18N.fetch( "UnknownOptionID" ),
                                         ERROR.flat( s, true ) )
                            fault( s )
                        end
                    end -- for i
                end
                if #ress > 0 then
                    ROW.fill( r, ress, false, fault )
                elseif o  and  o.sign ~= "peers" then
                    fault( I18N.fetch( "NoResources" ) )
                end
            end
        end
    end
    return r
end  -- ROW.fetch()



ROW.fiat = function ( about )
    -- Create line for Gadgets-definition
    -- Precondition:
    --     about    -- table, with gadget options
    -- Postcondition:
    --     Returns string, with line
    local r
    if about.ID then
        local o, set, sources, v
        r = string.format( "* %s", about.ID )
        for i = 2, #OPTIONS.order do
            o = OPTIONS.order[ i ]
            if o.live then
                v = about[ o.sign ]
                if v then
                    if o.mode == 1  or  o.mode == 2 then
                        if set then
                            set = set .. "|"
                        else
                            set = ""
                        end
                        set = set .. o.sign
                        if type( o.type ) == "table"  and
                           type( v ) == "table" then
                            set = set .. "="
                            for k = 1, #v do
                                if k > 1 then
                                    set = set .. ","
                                end
                                set = set .. v[ k ]
                            end -- for k
                        end
                    elseif o.mode == 3 then
                        sources = sources  or  ""
                        for k = 1, #v do
                            sources = string.format( "%s|%s",
                                                     sources,
                                                     v[ k ] )
                        end -- for k
                    end
                end
            end
        end -- for i
        if set then
            r = string.format( "%s[%s]", r, set )
        end
        if sources then
            r = r .. sources
        end
    else
        r = "*"
    end
    return r
end  -- ROW.fiat()



ROW.fill = function ( all, assign, ancestor, fault )
    -- Distribute resource pages
    -- Precondition:
    --     all         -- table, with gadget options
    --     about       -- sequence table, with resource pages
    --     ancestor    -- "peers", or not
    --     fault       -- function, as error handler
    -- Postcondition:
    --     resource page has been added to all table
    if type( all ) == "table"  and
       type( assign ) == "table" then
        local types
        local src, swap, types
        if ancestor == "peers" then
            types = { peers = "%.css$" }
        else
            types = { scripts = "%.js$",
                      styles  = "%.css$",
                      datas   = "%.json$" }
        end
        for i = 1, #assign do
            src  = assign[ i ]
            swap = false
            for k, v in pairs( types ) do
                if src:find( v ) then
                    swap = k
                end
            end -- for k, v
            if swap then
                all[ swap ] = all[ swap ]  or  { }
                table.insert( all[ swap ], src )
            else
                fault( string.format( "%s %s",
                                      I18N.fetch( "InvalidResource" ),
                                      ERROR.flat( src, true ) ) )
            end
        end -- for i
    end
end  -- ROW.fill()



ROW.first = function ( all, apply )
    -- Convert Gadgets-definition lines into tables
    -- Precondition:
    --     all      -- string, with all Gadgets-definition lines
    --     apply    -- mw.title or string, with origin
    -- Postcondition:
    --     Returns table, with
    --             .rows      -- sequence table of line strings
    --             .hash      -- mapping table of gadget ID to lines
    --             .elts      -- sequence table of heading IDs and lines
    --             .origin    -- mw.title or string
    local r
    if type( all ) == "string" then
        local errors
        local fault = function ( add )
                          errors = errors  or  { }
                          table.insert( errors, add )
                      end
        local elts = { }
        local hash = { }
        local rows = mw.text.split( all, "%s*\n%s*" )
        local k, s, scream
        r = { origin = apply }
        for i = 1, #rows do
            s = mw.text.trim( rows[ i ] )
            if s ~= "" then
                if s:sub( 1, 2 ) == "==" then
                    scream = false
                    k, s = s:match( "^(==+)%s*([^%s=].+[^%s=])%s*%1$" )
                    if s then
                        table.insert( elts, s )
                        if not VALUES.fine( s, "rootpage" ) then
                            scream = "InvalidHeadID"
                        end
                    else
                        scream = "InvalidHeadline"
                    end
                    if scream then
                        s = I18N.fetch( scream ) .. ROW.failed( "==", i )
                        fault( s )
                    end
                else
                    s = s:gsub( "^%s*%*%s*", "" )
                    k = s:find( "[%[|]" )
                    if k then
                        if k == 1 then
                            s = ""
                        else
                            s = mw.text.trim( s:sub( 1,  k - 1 ) )
                        end
                    end
                    if s ~= "" then
                        scream = false
                        table.insert( elts, i )
                        if hash[ s ] then
                            scream = "RepeatedID"
                        else
                            hash[ s ] = i
                            if not VALUES.fine( s, "rootpage" ) then
                                scream = "InvalidID"
                            end
                        end
                        if scream then
                            s = I18N.fetch( scream )
                                .. ROW.failed( s, i )
                            fault( s )
                        end
                    end
                end
            end
        end -- for i
        r.rows  = rows
        r.elems = elts
        r.hash  = hash
        if errors then
            s = I18N.fetch( "BadLines" ) .. ROW.from( apply )
            table.insert( errors, s, 1 )
            for i = 1, #errors do
                ERROR.fault( errors[ i ], "ROW" )
            end -- for i
        end
    end
    return r
end  -- ROW.first()



ROW.found = function ( ask )
    -- Retrieve single entry from active
    -- Precondition:
    --     ask    -- string, with entry ID
    local r
    if ACTIVE.live  and  not ACTIVE.def then
        ACTIVE.def = ROW.first( ACTIVE.story, ACTIVE.title )
    end
    if ACTIVE.def then
        r = ROW.fetch( ACTIVE.def, ask )
    end
    return r
end  -- ROW.found()



ROW.from = function ( all )
    -- Format origin
    -- Precondition:
    --     all    -- table, with Gadgets-definition representation
    -- Postcondition:
    --     Returns string, with hint
    local r
    if type( all ) == "table" then
        if type( all.origin ) == "table" then
            local s = all.origin.prefixedText
            if type( s ) == "string" then
                r = string.format( "[[%s]]", s )
            else
                r = "??????????"
            end
        else
            r = "TEXT"
        end
    end
    return r or ""
end  -- ROW.from()



TEMPLATE.fill = function ( all )
    -- Convert template parameters into hash
    -- Precondition:
    --     all    -- table, with template parameters
    -- Postcondition:
    --     Returns table, with options
    local r = { }
    if type( all ) == "table" then
        local errors
        local fault = function ( add )
                          r.ERRORS = r.ERRORS  or  { }
                          table.insert( r.ERRORS, add )
                      end
        local n, o, s, seps, vals
        r = { }
        for k, v in pairs( all ) do
            if type( v ) == "string" then
                o = OPTIONS.hash[ k ]
                if o then
                    s = type( o.type )
                    if s == "string" then
                        s = o.type
                    elseif s == "table" then
                        s = string.format( "{%s}", o.type[ 1 ] )
                    else
                        s = false
                    end
                    if s == "boolean" then
                        if v == "1" then
                            r[ k ] = true
                        else
                            r[ k ] = false
                        end
                    elseif s == "string" then
                        r[ k ] = v
                    elseif s then
                        v = v:gsub( "&#124;", "|" )
                             :gsub( "&#x7C;", "|" )
                             :gsub( "&#x7c;", "|" )
                        if o.scan == "page" then
                            seps = "%s*|%s*"
                        else
                            seps = "[%s,|]+"
                        end
                        vals = mw.text.split( v, seps )
                        for i = 1, #vals do
                            v = mw.text.trim( vals[ i ] )
                            if v == "" then
                                v = false
                            elseif s == "{number}" then
                                n = tonumber( v )
                                if n then
                                    v = n
                                else
                                    s = string.format( "%s %s",
                                            I18N.fetch( "BadNamespace" ),
                                            ERROR.flat( v, true ) )
                                    v = false
                                end
                            end
                            if v then
                                r[ k ] = r[ k ]  or  { }
                                table.insert( r[ k ],  v )
                            end
                        end -- for i
                    end
                else
                    s = string.format( "%s %s",
                                       I18N.fetch( "UnknownOptionID" ),
                                       ERROR.flat( k, true ) )
                    fault( s )
                end
            else
                s = string.format( "%s %s",
                                   I18N.fetch( "BadTemplateArg" ),
                                   ERROR.flat( k, true ) )
                fault( s )
            end
        end -- for k, v
    else
        fault( I18N.fetch( "BadTemplateArgs" ) )
    end
    return r
end  -- TEMPLATE.fill()



TEMPLATE.filter = function ( all, apart )
    -- Discard ignorable template parameters
    -- Precondition:
    --     all      -- table, with template parameters
    --     apart    -- string, with ignored template parameters
    -- Postcondition:
    --     Returns table, with template parameters
    local clear = { }
    local r = { }
    for i = 1, #apart do
        clear[ apart[ i ] ] = true
    end -- for i
    for k, v in pairs( all ) do
        if not clear[ k ] then
            r[ k ] = v
        end
    end -- for k, v
    return r
end  -- TEMPLATE.filter()



VALUES.features = function ( all, adjust, after )
    -- Convert list of items
    -- Precondition:
    --     all       -- string, with list of items
    --     adjust    -- string, with type of item
    --     after     -- string, with separator
    -- Postcondition:
    --     Returns table, with items, or not
    local scan     = mw.text.trim( all )
    local suitable = adjust[ 1 ]
    local r, sep, v, vals
    if after == "," then
        sep = "%s*,%s*"
    else
        sep = "[%s,|]+"
    end
    vals = mw.text.split( scan, sep )
    for i = 1, #vals do
        s = mw.text.split( vals[ i ] )
        if s == "" then
            v = false
        elseif suitable == "number" then
            v = VALUES.fine( s, "namespace" )
        else
            v = s
        end
        if v then
            r = r  or  { }
            table.insert( r, v )
        end
    end -- for i
    return r
end  -- VALUES.features()



VALUES.feed = function ( analyse, at, ask )
    -- Check value
    -- Precondition:
    --     analyse    -- string, with list
    --     at         -- string, with separator
    --     ask        -- true, requesting number elements, or not
    -- Postcondition:
    --     Returns table, or string with error, or nothing
    local r = mw.text.split( analyse, at )
    local s
    for i = #r, 1, -1 do
        s = mw.text.trim( r[ i ] )
        if s == "" then
            table.remove( r, i )
        elseif ask then
            if s:match( "^-?%d+$" ) then
                r[ i ] = tonumber( s )
            else
                r = { }
                break -- for i
            end
        end
    end -- for i
    if #r == 0 then
        r = false
    end
    return r
end  -- VALUES.feed()



VALUES.fine = function ( analyse, as )
    -- Check value
    -- Precondition:
    --     analyse    -- string or number, with value
    --     as         -- string, with key of pattern
    -- Postcondition:
    --     Returns true, if fine
    local s = type( analyse )
    local n, r
    if s == "string" then
        local req = VALUES.validate[ as ]
        s = type( req )
        if s == "table" then
            local oblis = req[ 1 ]
            local excls = req[ 2 ]
            if type( oblis ) == "table" then
                for i = 1, #oblis do
                    s = oblis[ i ]
                    if type( s ) == "string" then
                        r = analyse:match( string.format( "^%s$", s ) )
                        if r then
                            r = true
                            break -- for i
                        end
                    end
                end -- for i
            end
            if r and type( excls ) == "table" then
                for i = 1, #excls do
                    s = excls[ i ]
                    if type( s ) == "string"  and
                       analyse:match( s ) then
                        r = false
                        break -- for i
                    end
                end -- for i
            end
        elseif s == "string" then
        end
        if type( r ) ~= "boolean" then
            ERROR.fault( "VALUES.validate: " .. as,  "INTERNAL" )
        elseif r  and  as == "namespace" then
            n = tonumber( analyse )
        end
    elseif s == "number" then
        n = analyse
    end
    if n then
        if mw.site.namespaces[ n ] then
            r = n
        else
            r = false
        end
    end
    return r
end  -- VALUES.fine()



VALUES.full = function ()
    -- Retrieve list of permitted items
    -- Postcondition:
    --     tables with permitted items present
    for k, v in pairs( VALUES.arrays ) do
        if not VALUES.arrays[ k ] then
            VALUES.arrays[ k ] = COMMONS.fetch( k )  or  { }
        end
    end -- for k, v
end  -- VALUES.full()



MediaWikiGadgetDefinition.f = function ( arglist, frame )
    -- Perform standard functionality
    -- Precondition:
    --     arglist    -- table, with parameters
    --     frame      -- frame object, or not
    -- Postcondition:
    --     Returns mw.html, or not
    local r, s
    GLOBAL.frame( frame )
    if type( arglist.id ) == "string" then
        GLOBAL.id = mw.text.trim( arglist.id )
    end
    if type( arglist.template ) == "table" then
        GLOBAL.template = arglist.template
        if type( GLOBAL.template.ID ) == "string" then
            GLOBAL.id = mw.text.trim( GLOBAL.template.ID )
        end
        if type( arglist.IGNORE ) == "table" then
            GLOBAL.template = TEMPLATE.filter( GLOBAL.template,
                                               arglist.IGNORE )
        end
    end
    if GLOBAL.id == ""  or
       GLOBAL.id == "-" then
        GLOBAL.id = false
    end
    s = arglist.use
    if type( s ) == "string" then
        s = mw.text.trim( s )
        if s ~= ""  and
           s ~= "-" then
            ACTIVE.story = s
            GLOBAL.scope = "text"
            if not GLOBAL.id then
                GLOBAL.id = "*"
            end
        end
    end
    if GLOBAL.id then
        local tbl
        if type( arglist.strictRows ) == "boolean" then
            ROWS.lazy = arglist.strictRows
        end
        if type( arglist.Build ) == "string" then
            s = arglist.Build:upper()
            if s == "JSON"  or  s == "ROWS" then
                GLOBAL.syntax = s
            end
        end
        GLOBAL.syntax = GLOBAL.syntax  or  "HTML"
        if not GLOBAL.scope  and
           type( arglist.scope ) == "string" then
            s = mw.text.trim( arglist.scope )
            if s == "site"  or
               s == "global" then
                GLOBAL.scope = s
            elseif s == "" then
                GLOBAL.scope = "site"
            else
                if s:find( ":", 1, true ) then
                    local ns
                    ns, GLOBAL.scope = GLOBAL.focus( s )
                end
            end
            if not GLOBAL.scope then
                ERROR.fault( I18N.fetch( "InvalidScope" ),  "#invoke" )
            end
        end
        GLOBAL.scope = GLOBAL.scope or "site"
        if arglist.Export  and
           GLOBAL.scope == "site"  and
           GLOBAL.syntax == "HTML" then
            HTML.export = true
        end
        GLOBAL.first()
        if GLOBAL.scope ~= "global" then
            ACTIVE.first()
        end
        if GLOBAL.template then
            tbl = TEMPLATE.fill( GLOBAL.template )
            r   = HTML.folder( tbl )
        end
    else
        ERROR.fault( I18N.fetch( "IDmissing" ),  "#invoke" )
    end
    return r
end -- .f()



MediaWikiGadgetDefinition.params = function ()
    -- Communicate possible template parameters
    -- Postcondition:
    --     Returns mw.html <ul>
    local r = mw.html.create( "ul" )
    local e, o
    OPTIONS.first()
    for i = 2,  #OPTIONS.order do
        o = OPTIONS.order[ i ]
        e = mw.html.create( "li" )
                   :wikitext( o.sign )
        if not o.live then
            e:css( "text-decoration", "line-through" )
        end
        r:newline()
         :node( e )
    end -- for i
    return r
end -- .params()



MediaWikiGadgetDefinition.suggestedvalues = function ( array )
    local r
    if type( VALUES.arrays[ array ] ) == "boolean" then
        local vals = COMMONS.fetch( array )
        if type( vals ) == "table" then
            local sep = ""
            local e
            for i = 1, #vals do
                e = vals[ i ]
                if e[ 2 ] then
                    r   = string.format( "%s%s\"%s\"",
                                         r or "",
                                         sep,
                                         e[ 1 ] )
                    sep = ", "
                end
            end -- for i
        end
    end
    r = r or "ERROR"
    return string.format( "[ %s ]", r )
end -- .suggestedvalues()



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
    -- 2024-03-01
    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
        elseif link then
            r = false
        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()



-- Export
local p = { }

p.f = function ( frame )
    local params = { }
    if frame.args.TEMPLATE == "1" then
        params.template = frame:getParent().args
        if frame.args.IGNORE  and
           frame.args.IGNORE ~="" then
            params.IGNORE = mw.text.split( frame.args.IGNORE,
                                           "%s*|%s*" )
        end
    else
        if frame.args.Use  and
           frame.args.Use ~=""  and
           frame.args.Use ~="-" then
            params.use = frame.args.Use
        else
            if frame.args.ExportLink == 1 then
                params.export = true
            end
            params.scope = frame.args.Scope
        end
    end
    if frame.args.StrictRows == "1" then
        params.strictRows = true
    end
    if frame.args.ID  and
       frame.args.ID ~= ""  and
       frame.args.ID ~= "-" then
        params.id = frame.args.ID
    end
    if frame.args.Build  and
       frame.args.Build ~="" then
        params.build = frame.args.Build
    end
    return MediaWikiGadgetDefinition.f( params, frame )  or  ""
end    -- p.f

p.params = function ( frame )
    return MediaWikiGadgetDefinition.params()
end    -- p.params

p.suggestedvalues = function ( frame )
    local s = frame.args[ 1 ]
    if s then
        s = mw.text.trim( s )
        if s == "" then
            s = false
        end
    end
    return MediaWikiGadgetDefinition.suggestedvalues( s )
end    -- p.suggestedvalues

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

setmetatable( p,  { __call = function ( func, ... )
                                 setmetatable( p, nil )
                                 return Failsafe
                             end } )

return p