Module:TemplateData

local TemplateData = { serial = "2017-03-29", suite = "TemplateData" } --[=[ improve template:TemplateData ]=]

local Config = { -- multiple #invoke option names mapped into unique internal fields cat          = "strange", debug        = false, docpageCreate = "suffix", docpageDetect = "subpage", msgDescMiss  = "solo", loudly       = false,    -- show exported element, etc.    solo          = false,    -- complaint on missing description strange      = false,    -- title of maintenance category subpage      = false,    -- pattern to identify subpage suffix       = false     -- subpage creation scheme } local Data = { div    = false,    -- got    = false,    -- table, initial templatedata object heirs  = false,    -- table, params that are inherited less   = false,    -- main description missing lasting = false,   -- old syntax encountered -- low     = false,    -- 1= mode order  = false,    -- parameter sequence params = false,    -- table, exported parameters scream = false,    -- error messages slang  = false,    -- project language code slim   = false,    -- JSON reduced to plain source = false,    -- JSON input strip  = false,    -- evaluation tag    = false,    -- table, exported root element title  = false,    -- page tree   = false     -- table, rewritten templatedata object } local Permit = { colors = { required   = "EAF3FF", suggested = "FFFFFF", optional  = "D0D0D0", deprecated = "FFCBCB" }, params = { aliases     = "table", autovalue  = "string", default    = "string table I18N nowiki", deprecated = "boolean string", description = "string table I18N", example    = "string table I18N nowiki", label      = "string table I18N", inherits   = "string", required   = "boolean", suggested  = "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 string.format( "%s *** %s", Data.scream, alert ) else Data.scream = alert end end -- Fault

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( "%-", "%%%-" ) ) local i, k = Data.source:find( seek, at ) local r, slice, source 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 facility( about, asked ) -- Create description head -- Parameter: --    about  -- table, supposed to contain description --    asked  -- true, if mandatory description -- Returns, with head, or nil local para = mw.html.create( "p" ) 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" ) :addClass( "templatedata-maintain" ) :css( "display", "none" ) for k, v in pairs( about.description[ 2 ] ) do               plus:node( mw.html.create( "li" )                                  :node( mw.html.create( "code" ) :wikitext( k ) )                                 :tag( "br" )                                  :wikitext( fair( v ) ) ) end -- for k, v       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" ) :wikitext( para, plus ) else r = para end end return r end -- facility

local function factory( adapt ) -- Retrieve localized text from system message -- Parameter: --    adapt  -- string, message ID after "templatedata-" -- Returns string, with localized text return mw.message.new( "templatedata-" .. adapt ):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 " .. Data.scream ) if Config.strange then r = string.format( "%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 r   if adjust:find( " ", 1, true ) then local i   = 1 local j, k = adjust:find( " ", i, true ) r = "" while j do           if j > 1 then r = r .. f( adjust:sub( i, j - 1 ) ) end i = j + 10 j, k = adjust:find( " ", i, true ) if j then r = r .. adjust:sub( i, j - 1 ) i = j + 11 end end   -- while j    else r = f( adjust ) end return r end -- fair

local function faraway( alternatives ) -- Retrieve project 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 r1, r2   if not Data.slang then Data.slang = mw.language.getContentLanguage:getCode end for k, v in pairs( alternatives ) do       if type( v ) == "string" then v = mw.text.trim( v ) if v == "" then alternatives[ k ] = nil else n = n + 1 end else alternatives[ k ] = nil end end -- for k, v   if n > 0 then for k, v in pairs( alternatives ) do           if v then if n == 1 then r1 = v               elseif k:lower == Data.slang then alternatives[ k ] = nil r1 = v                   r2 = alternatives break -- for k, v               end end end -- for k, v       if not r1 then local seek = string.format( "^%s-", Data.slang ) for k, v in pairs( alternatives ) do               if v and k:lower:match( seek ) then alternatives[ k ] = nil r1 = v                   r2 = alternatives break -- for k, v               end end -- for k, v           if not r1 then local others = mw.language.getFallbacksFor( slang ) table.insert( others, "en" ) for i = 1, #others do                   seek = others[ i ] if alternatives[ seek ] then r1                  = alternatives[ seek ] alternatives[ seek ] = nil r2                  = alternatives break   -- for i                    end end -- i = 1, #others end if not r1 then for k, v in pairs( alternatives ) do                   if v then alternatives[ k ] = nil r1 = v                       r2 = alternatives break -- for k, v                   end end -- for k, v           end end if r2 then local Multilingual = fetch( "Multilingual" ) for k, v in pairs( r2 ) do               if v  and  not Multilingual.isLang( k ) then Fault( string.format( "Invalid ", k ) ) end end -- for k, v       end end return r1, r2 end -- faraway

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        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             = { } for k2, v2 in pairs( p[ v ] ) do                   p2[ k2 ] = v2                end -- for k2, v2                for k2, v2 in pairs( p[ k ] ) do                    if type( v2 ) ~= "nil" then p2[ k2 ] = v2                   end end -- for k2, v2               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            end end -- for k, v   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 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  = { } for k, v in pairs( Data.tree.params ) do               i = facet( k, 1 ) 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 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 legal  = true local param  = Data.tree.params[ access ] local ranking = { "required", "suggested", "optional", "deprecated" } local r      = mw.html.create( "tr" ) 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:wikitext( access ) if not fine( access ) then code:addClass( "error" ) Fault( string.format( "Bad ID params. ", access ) ) legal = false begin:attr( "data-sort-value", " " .. sort ) end code = mw.html.create( "td" ) :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 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" ) ) end code:wikitext( s ) 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. .aliases", access ) Fault( factory( "invalid-value" ):gsub( "$1", s )  ) legal = false end end

-- description etc.   s = facility( param ) if s then desc:node( s ) end if param.default or param.example or param.autovalue then local details = { "default", "example", "autovalue" } local dl     = mw.html.create( "dl" ) local section, show for i = 1, #details do           s    = details[ i ] show = param[ s ] if show then section = factory( "doc-param-" .. s ) dl:node( mw.html.create( "dt" )                               :wikitext( section ) ) :node( mw.html.create( "dd" )                               :wikitext( show ) ) end end -- i = 1, #details desc:node( dl ) end

-- type if param.type then s    = Permit.types[ param.type ] typed = mw.html.create( "td" ) if s then if s == "string" then Data.params[ access ].type = s               typed:wikitext( factory( "doc-param-type-" .. s ) ) :node( "br" ) :node( mw.html.create( "span" )                                  :addClass( "error" )                                   :wikitext( param.type ) ) Data.lasting = true else s = factory( "doc-param-type-" .. param.type ) typed:wikitext( s ) end else Data.params[ access ].type = "unknown" typed:addClass( "error" ) :wikitext( "INVALID" ) s = string.format( "params. .type", access ) Fault( factory( "invalid-value" ):gsub( "$1", s )  ) legal = false end else typed = mw.html.create( "td" ) :wikitext( factory( "doc-param-type-unknown" ) ) end

-- status if param.required then mode = 1 if param.deprecated then Fault( string.format( "Required deprecated ", 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" ) :wikitext( param.deprecated ) end end

--    r:attr( "id",  "templatedata:" .. mw.uri.anchorEncode( access ) ) :css( "background-color", "#" .. Permit.colors[ status ] ) :node( begin ) :node( code ) :node( desc ) :node( typed ) :node( mw.html.create( "td" )                  :attr( "data-sort-value", tostring( mode ) )                   :node( ranking ) ) :newline if not legal then r:css( "border", "#FF0000 3px solid" ) end return r end -- feature

local function features -- Create ", at )                    else                        r = "root"                    end                    if a then                        r = string.format( "%s ", 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" 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 ) else local translated v, translated = faraway( v ) if translated and  k == "description" then elem = { [ 1 ] = fair( v ), [ 2 ] = translated } else elem = fair( v ) end end if scope:find( "nowiki", 1, true ) then elem = mw.text.nowiki( v ) else v = flat( v ) end else if k == "params" and  not access then v   = nil elem = nil elseif s == "string" then v   = mw.text.nowiki( v ) elem = v                       elseif k == "inherits" then elem = v                           if not Data.heirs then Data.heirs = { } end Data.heirs[ slot ] = v                           v                  = nil 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 not Data.params then Data.params = { } end Data.params[ slot ] = { } tag = Data.params[ slot ] else Data.tag = { } tag     = Data.tag end end tag[ k ] = v                   end else s = string.format( "Type  bad for %s",                                       scope,  f( k, slot ) ) Fault( s ) end else Fault( "Unknown component " .. f( k, slot ) ) end end -- for k, v   else Fault( f .. " needs to be of  type" ) end end -- focus

local function format -- Build presented documentation -- Returns local r = mw.html.create( "div" ) local s = facility( Data.tree, true ) if s then r:node( s ) end s = features if s then r:node( s ) end if Data.tree and Data.tree.format then local e, style s = Data.tree.format:lower( Data.tree.format ) if s == "inline" or  s == "block" then style = "i" else style = "code" end r:node( mw.html.create( "p" )                      :wikitext( "Format: " )                       :node( mw.html.create( style ) :wikitext( s ) ) ) end return r end -- format

local function free -- Remove JSON comment lines Data.source:gsub( "([{,\"'])(%s*\n%s*//.*\n%s*)([},\"'])",                     "%1%3" ) end -- free

local function full -- Build survey table from JSON data, append invisible Data.div = mw.html.create( "div" ) :addClass( "mw-templatedata-doc-wrap" ) 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( format ) 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" ) ) else div:css( "display", "none" ) end Data.div:node( div ) end end -- full

local function furnish( adapt, arglist ) -- Analyze transclusion -- Parameter: --    adapt    -- table, #invoke parameters --    arglist  -- table, template parameters -- Returns string local source for k, v in pairs( Config ) do       if adapt[ k ]  and  adapt[ k ] ~= "" then Config[ v ] = adapt[ k ] end end -- for k, v   Config.loudly = faculty( arglist.debug or adapt.debug ) if mw.site.server:find( "//de.wikipedia.beta.wmflabs.org", 1, true ) then Config.loudly = true end if arglist.JSON then source = arglist.JSON 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 not source then Data.title = mw.title.getCurrentTitle source = find if not source and Config.subpage and  Config.suffix  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 TemplateData.getPlainJSON( source ) return finalize end -- furnish

TemplateData.failsafe = function ( assert ) local r   if not assert  or  assert <= TemplateData.serial then r = TemplateData.serial else r = false end return r end -- TemplateData.failsafe

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 Data.source = adapt free Data.got = mw.text.jsonDecode( Data.source ) if Data.got then full if Data.lasting then Fault( "deprecated type syntax" ) end if Data.less then Fault( Config.solo ) end elseif not Data.strip then Fault( "fatal JSON error" ) 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 TemplateData.failsafe( since ) or  "" end -- p.failsafe

p.TemplateData = function -- Module interface return TemplateData end

return p