Modul:Tutorial

Die Dokumentation für dieses Modul kann unter Modul:Tutorial/Doku erstellt werden

local Tutorial = { serial = "2018-02-11",
                   suite  = "Tutorial" }
--[=[
Support Tutorial tree
]=]



Tutorial.txt =
    { sequence = "bad page for page@Tutorial",
      slot     = "''Start of a trail.''",
      sole     = "''This page got currently no content assigned.''",
      sparse   = "''Nothing to say.''",
      start    = "Start",
      starter  = "Start trail",
      suite    = "All",
      summary0 = "'summary' missing",
      superior = "''Below all single pages are merged.''",
      survey   = "''Start page: [[%s|here]]''"
    }



local function faculty( args, arg )
    -- Retrieve boolean template arg
    --     args  -- (table)
    --     arg   -- (string, number)
    local r = args[ arg ]
    if r == ""  or  r == "0" then
        r = false
    end
    return r and true  or  false
end -- faculty()




local function fault( alert )
    -- Report problem
    --     alert  -- (string) message
    -- Returns: string
    local cat    = Tutorial.errCat   or ""
    local class  = Tutorial.errClass or ""
    local style  = Tutorial.errStyle or ""
    local show   = Tutorial.errText  or ""
    local suffix = type( alert )
    local e      = mw.html.create( "span" )
                          :addClass( "error" )
    local r
    if class ~= "" then
        e:addClass( class )
    end
    if style ~= "" then
        e:cssText( style )
    end
    if show == "" then
        show = "Error"
    end
    if suffix == "string" then
        show = string.format( "%s: %s", show, alert )
    elseif suffix == "number" then
        show = string.format( "%s %d", show, alert )
    end
    e:wikitext( show )
    r = tostring( e )
    if cat ~= "" then
        r = string.format( "%s[[Category:%s]]", r, cat )
    end
    return r
end -- fault()



Tutorial.facet = function ( access )
    -- Create page link with tooltip, if present
    --     access  -- (string) subpage
    -- Returns: string
    local swap = string.format( "%s:%s/%s",
                                Tutorial.page.nsText,
                                Tutorial.page.baseText,
                                access )
    local src, show = Tutorial.foreign( access )
    if show then
        -- TODO    plaintext show
        show = tostring( mw.html.create( "span" )
                                :attr( "title", show )
                                :wikitext( access ) )
    else
        show = access
    end
    return  string.format( "[[%s|%s]]", swap, show )
end -- Tutorial.facet()



Tutorial.facility = function ( appoint, at )
    -- Retrieve base page and subpage name
    --     appoint  -- module title and base page, page derived else
    --     at       -- page name, or nil
    -- Returns:
    --     1. module title and base page
    --     2. page name
    local r1, r2
    if appoint then
        r1 = appoint
    else
        local room = mw.site.namespaces[ Tutorial.page.namespace ]
        r1 = string.format( "%s:%s",
                            room.subject.name,
                            Tutorial.page.baseText )
    end
    if type( at ) == "string" then
        r2 = mw.text.trim( at )
        if r2 == "" then
            r2 = false
        end
    end
    if not r2 then
        r2 = Tutorial.page.subpageText
    end
    return  r1, r2
end -- Tutorial.facility()



Tutorial.fake = function ( album, at )
    -- Transclude content
    --     album  -- number of trail
    --     at     -- number of section
    -- Returns: (string) page content
    local trails = Tutorial.def.trails
    local sort   = Tutorial.page.subpageText
    local r
    if type( trails ) == "table" then
        local trail = trails[ album ]
        if type( trail ) == "table" then
            local start = trail[ 1 ]
            trail = trail[ 2 ]
            if type( start ) == "string"  and
               type( trail ) == "table" then
                local single = trail[ at ]
                start = mw.text.trim( start )
                if type( single ) == "string"  and
                   start ~= "" then
                    r = Tutorial.feed( start, single, trail, album, at )
                end
            end
        end
    end
    if r then
        sort = "§ " .. sort
    else
        r    = Tutorial.txt.sole
        sort = "# " .. sort
    end
    r = r .. Tutorial.fold( sort )
    return r
end -- Tutorial.fake()



Tutorial.father = function ( ancestor )
    -- Top page: link all trails
    --     ancestor  -- (string) supreme page
    -- Returns: (mw.html) <dl>, or nil
    local r
    if type( Tutorial.def.trails ) == "table" then
        local dl = mw.html.create( "dl" )
        local dd, ea, eg, et, ex, section, src, story, sum, trail
        story = string.format( "&#91;[[%s/%%s|%s]]&#93;",
                               ancestor,
                               Tutorial.txt.suite )
        for i = 1, #Tutorial.def.trails do
            trail = Tutorial.def.trails[ i ]
            if type( trail ) == "table" then
                section = trail[ 1 ]
                if type( section ) == "string" then
                    r = true
                    dl:newline()
                      :node( mw.html.create( "dt" )
                                    :wikitext( section ) )
                    src, sum = Tutorial.foreign( section )
                    if sum ~= section then
                        dl:newline()
                          :node( mw.html.create( "dd" )
                                        :wikitext( sum ) )
                    end
                    trail = trail[ 2 ]
                    if type( trail ) == "table" then
                        dd = mw.html.create( "dd" )
                        ea = mw.html.create( "small" )
                                    :wikitext( string.format( story,
                                                              section ) )
                        eg = mw.html.create( "span" )
                        Tutorial.forward( eg, i, 1, 1,
                                          Tutorial.txt.start,
                                          true )
                        et, ex = Tutorial.flip( ancestor, trail, i )
                        if et then
                            dd:newline()
                              :node( eg )
                              :newline()
                              :node( et )
                              :newline()
                              :node( ea )
                              :newline()
                              :node( ex )
                        end
                    end
                    dl:newline()
                      :node( dd )
                end
            end
        end -- i = 1, #Tutorial.def.trails
        if r then
            r = dl
        end
    end
    return r
end -- Tutorial.father()



Tutorial.features = function ( ancestor, all, album, attribute, ahead )
    -- List of direct links to trail subpages
    --     ancestor   -- (string) supreme page
    --     all        -- (table) subpages
    --     album      -- (number) trail
    --     attribute  -- (boolean) numbered list
    --     ahead      -- (boolean) show both name and summary
    -- Returns: (mw.html)
    local n = #all
    local r
    if n > 0 then
        local sequence = string.format( "[[%s/%d-%%d|%%s]]",
                                        ancestor,
                                        album )
        local ttips    = Tutorial.future( all, at, false, ancestor )
        local s, show
        if attribute then
            s = "ol"
        else
            s = "ul"
        end
        r = mw.html.create( s )
        for i = 1, #all do
            s = ttips[ i ]
            if ahead and s then
                show = string.format( sequence, i, all[ i ] )
                show = string.format( "%s &#8211; %s", show, s )
            else
                show = string.format( sequence,  i,  s or all[ i ] )
            end
            r:node( mw.html.create( "li" )
                           :wikitext( show ) )
        end -- i = 1, #all
    else
        r = mw.html.create( "span" )
                   :wikitext( Tutorial.txt.sparse )
    end
    return r
end -- Tutorial.features()



Tutorial.feed = function ( above, access, all, album, at )
    -- Equip page transclusion
    --     above   -- (string) start
    --     access  -- (string) subpage
    --     all     -- (table) subpages
    --     album   -- (number) trail
    --     at      -- (number) section
    -- Returns: (string), or nil
    local src, sum = Tutorial.foreign( access )
    local r
    if src then
        local box   = Tutorial.further( all, album, at )
        local start = Tutorial.facet( above )
        r = Tutorial.flat( sum, start, box )
        if type( Tutorial.pageSource ) == "table"  and
           type( Tutorial.pageSource.source ) == "string" then
            local e = mw.html.create( "div" )
                             :css( "clear",         "right" )
                             :css( "float",         "right" )
                             :css( "margin-top",    "-1em" )
                             :css( "margin-bottom", "1em" )
                             :css( "padding",       "2px" )
           if type( Tutorial.pageSource.class ) == "string" then
                e:addClass( Tutorial.pageSource.class )
            end
            if Tutorial.pageSource.loose then
                e:css( "display", "none" )
            end
            if type( Tutorial.pageSource.style ) == "string" then
                e:cssText( Tutorial.pageSource.style )
            end
            e:wikitext( string.format( "[[%s:%s/%s|%s]]",
                                       Tutorial.page.nsText,
                                       Tutorial.page.baseText,
                                       access,
                                       Tutorial.pageSource.source ) )
            r = string.format( "%s\n%s\n", r, tostring( e ) )
        end
        r = r .. Tutorial.frame:preprocess( src )
    end
    return r
end -- Tutorial.feed()



Tutorial.fence = function ( access )
    -- Retrieve page protection
    -- Precondition:
    --     access  -- (string) subpage
    -- Returns: (string) level sign
    local t = mw.title.new( string.format( "%s/%s",
                                           Tutorial.page.baseText,
                                           access ),
                            Tutorial.page.nsText )
    local r
    if t.exists then
        local p = t.protectionLevels
        if type( p ) == "table" then
            p = p.edit
            if type( p ) == "table" then
                for k, v in pairs( p ) do
                    if v == "editeditorprotected" then
                        r = "&frac34;"
                    elseif v == "autoconfirmed" then
                        r = "&frac12;"
                    elseif v == "sysop" then
                        r = "1"
                    end
                end -- for k, v
            end
        end
    end
    return r or "0"
end -- Tutorial.fence()



Tutorial.fenced = function ( album, at, access )
    -- Retrieve page group protection
    -- Precondition:
    --     album   -- number of trail
    --     at      -- number of section
    --     access  -- (string) subpage
    -- Returns: (string) both levels
    return string.format( "%s,%s",
                          Tutorial.fence( string.format( "%d-%d",
                                                         album, at ) ),
                          Tutorial.fence( access ) )
end -- Tutorial.fenced()



Tutorial.fetch = function ( appoint, allow )
    -- Attach config or tree module
    -- Precondition:
    --     appoint  -- module title, "config" if "", page derived else
    --     allow    -- (boolean) suppress error message
    -- Returns: (table) with library, or (string) error message
    local suite = appoint
    local got, lucky, r, source
    if not Tutorial.page then
        Tutorial.page = mw.title.getCurrentTitle()
    end
    if type( suite ) == "string" then
        suite = mw.text.trim( suite )
        if suite == "" then
            suite = false
        end
    else
        suite = string.format( "%s:%s",
                               Tutorial.page.nsText,
                               Tutorial.page.baseText )
    end
    if suite then
        source = string.format( "Module:%s/%s",
                                Tutorial.suite, suite )
        lucky, got = pcall( require, source )
        if type( got ) == "table" then
            r = got
            if type( got[ suite ] ) == "function" then
                r = got[ suite ]()
            end
        end
        if type( r ) ~= "table" then
            if allow then
                r = ""
            else
                r = string.format( "%s invalid", source )
            end
        end
    end
    source = string.format( "Module:%s/config", Tutorial.suite )
    lucky, got = pcall( require, source )
    if type( got ) == "table" then
        for k, v in pairs( got ) do
            if type( v ) == "table" then
                if not Tutorial[ k ] then
                    Tutorial[ k ] = { }
                end
                if type( Tutorial[ k ] ) == "table" then
                    local t = Tutorial[ k ]
                     for kk, vv in pairs( v ) do
                         t[ kk ] = vv
                     end -- for kk, vv
                end
            else
                Tutorial[ k ] = v
            end
        end -- for k, v
    end
    return r
end -- Tutorial.fetch()



Tutorial.field = function ( access, add )
    -- Create box
    --     access  -- 2nd headline, string, or nil
    --     add     -- action area, mw.html <div>, or nil
    -- Returns: (string) box
    local bas = Tutorial.def
    local btm = bas.bottom
    local css = bas.style
    local f   = function ()
                    local r = mw.html.create( "td" )
                    if type( css.fgc ) == "string" then
                        r:css( "border-top-color", "#" .. css.fgc )
                         :css( "border-top-style", "solid" )
                         :css( "border-top-width", "1px" )
                    end
                    return r
                end -- f()
    local tbl = mw.html.create( "table" )
                       :addClass( "wikiTutorial" )
                       :attr( "role", "navigation" )
                       :css( "float",         "right !important" )
                       :css( "margin-left",   "1em !important" )
                             -- mobile resets first float table (infobox)
                       :css( "margin-bottom", "1em" )
    local te  = mw.html.create( "th" )
    if type( css.bgc ) == "string" then
        tbl:css( "background-color", "#" .. css.bgc )
    end
    if type( css.fgc ) == "string" then
        tbl:css( "color", "#" .. css.fgc )
    end
    if type( css.border ) == "table" then
        if type( css.fgc ) == "string" then
           tbl:css( "border-color", "#" .. css.fgc )
        end
        if type( css.border.style ) == "string" then
           tbl:css( "border-style", css.border.style )
        end
        if type( css.border.width ) == "string" then
           tbl:css( "border-width", css.border.width )
        end
    end
    if type( css.icon ) == "string" then
        local size
        if type( css.size ) == "string" then
            size = css.size:match( "^(%d+)px$" )
            if size then
                size = tonumber( size ) + 4
                size = string.format( "%dpx", size )
            else
                size = css.size
            end
        else
            size = "15px"
        end
        te:wikitext( string.format( "[[File:%s|%s]]",
                                    css.icon,
                                    size ) )
    end
    if type( bas.show ) == "string" then
       local e = mw.html.create( "span" )
                        :css( "margin-left", "5px" )
       local s = bas.supreme
       if type( s ) == "string" then
           s = string.format( "[[%s|%s]]", s, bas.show )
       else
           s = bas.show
       end
       e:wikitext( s )
        if type( css.size ) == "string" then
            e:css( "font-size", css.size )
        end
        te:node( e )
    end
    te:css( "padding-left", "4px" )
      :css( "padding-right", "4px" )
      :css( "text-align", "left" )
    tbl:newline()
       :node( mw.html.create( "tr" )
                     :node( te ) )
    if type( access ) == "string" then
        tbl:newline()
           :node( mw.html.create( "tr" )
                         :node( f():wikitext( access ) ) )
    end
    if type( add ) == "table" then
        tbl:newline()
           :node( mw.html.create( "tr" )
                         :node( f():node( add ) ) )
    end
    if type( btm ) == "table" then
        te = f()
        if type( btm.service ) == "string" then
            local s
            if type( btm.show ) == "string" then
                s = "|" .. btm.show
            else
                s = ""
            end
            te:wikitext( string.format( "[[%s%s]]",
                                        btm.service,
                                        s ) )
        elseif type( btm.show ) == "string" then
            te:wikitext( btm.show )
        end
        tbl:newline()
           :node( mw.html.create( "tr" )
                         :node( te ) )
    end
    return tostring( tbl )
end -- Tutorial.field()



Tutorial.find = function ( at )
    -- Retrieve trail lessons
    --     at  -- trail name
    -- Returns:
    --     (table) or nil
    --     (number) trail
    local r1, r2
    for i = 1, #Tutorial.def.trails do
        trail = Tutorial.def.trails[ i ]
        if type( trail ) == "table" then
            if trail[ 1 ] == at then
                r1 = trail[ 2 ]
                r2 = i
                break -- i = 1, #Tutorial.def.trails
            end
        end
    end -- i = 1, #Tutorial.def.trails
    return r1, r2
end -- Tutorial.find()



Tutorial.flat = function ( about, above, add )
    -- Single content page
    --     about  -- summary
    --     above  -- trail start, if in trail
    --     add    -- action area, mw.html <div>, or nil
    -- Returns: (string)
    local css = Tutorial.def.style
    local e   = mw.html.create( "div" )
    local r   = Tutorial.field( above, add )
    e:wikitext( about )
    if type( css ) == "table" then
        if type( css.bgc ) == "string" then
            e:css( "background-color", "#" .. css.bgc )
        end
        if type( css.fgc ) == "string" then
            e:css( "color", "#" .. css.fgc )
        end
        if type( css.h1 ) == "table" then
            for k, v in pairs( css.h1 ) do
                e:css( k, v )
            end -- for k, v
        end
    end
    r = string.format( "%s\n%s%s",
                       r,
                       tostring( e ),
                       Tutorial.fold( Tutorial.page.subpageText ) )
    return r
end -- Tutorial.flat()



Tutorial.flip = function ( ancestor, all, album )
    -- Create trail extension for survey
    --     ancestor  -- (string) supreme page
    --     all       -- (table) subpages
    --     album     -- (number) trail
    -- Returns:
    --     (mw.html) toggle <span>, or nil
    --     (mw.html) list <div>, or nil
    local r1 = mw.html.create( "span" )
                      :css( "margin-left", "2em" )
                      :css( "margin-right", "2em" )
    local r2 = mw.html.create( "div" )
    Tutorial.flipper( r1,  string.format( "tutorial-%d", album ) )
    r2:attr( "id",
             string.format( "mw-customcollapsible-tutorial-%d", album ) )
      :addClass( "mw-collapsible mw-collapsed" )
      :css( "display", "none" )
      :newline()
      :node( Tutorial.features( ancestor, all, album, false, true ) )
    return r1, r2
end -- Tutorial.flip()



Tutorial.flipper = function ( apply, assign )
    -- Create toggle
    --     apply    -- (mw.html)
    --     assign   -- (string) ID
    local sel  = "nomobile"
                 -- mobile no collapsible yet
                 -- derive from .client-nojs display:none
    local sign = "mw-customtoggle"
    sel = string.format( "%s %s-%s %s-%s-show %s-%s-hide",
                         sel, sign, assign, sign, assign, sign, assign )
    apply:addClass( sel )
         :newline()
         :node( Tutorial.flipflop( true, assign ) )
         :newline()
         :node( Tutorial.flipflop( false, assign ) )
end -- Tutorial.flipper()



Tutorial.flipflop = function ( activate, assign )
    -- Create single toggle image
    --     activate  -- (boolean) more (down), or not
    --     assign    -- (string) ID, or nil
    -- Returns: (mw.html), or (string) if not
    local j       = 16
    local support = mw.ustring.char( 43, 47, 8722 )
    local sign    = "OOjs UI icon caret%s.svg"
    local r, s
    if activate then
        s = "Down"
    else
        s = "Up"
    end
    r = string.format( "[[File:%s|%dpx|link=#top|%s]]",
                       string.format( sign, s ),  j,  support )
    if assign then
        if activate then
            s = "show"
        else
            s = "hide"
        end
        r = mw.html.create( "span" )
                   :addClass( "mw-collapsible" )
                   :attr( "id",
                          string.format( "mw-customcollapsible-%s-%s",
                                         assign, s ) )
                   :wikitext( r )
        if not activate then
            r:addClass( "mw-collapsed" )
             :css( "display", "none" )
        end
    end
    return r
end -- Tutorial.flipflop()



Tutorial.focus = function ( at, about, appoint )
    -- Retrieve page name of particular lesson in trail
    --     at       -- page name, or nil
    --     about    -- lesson name, or nil (means first in trail)
    --     appoint  -- module title and base page, page derived else
    -- Returns: (string)
    local suite, start = Tutorial.facility( appoint, at )
    local r, trail
    for i = 1, #Tutorial.def.trails do
        trail = Tutorial.def.trails[ i ]
        if type( trail ) == "table"  and
           type( trail[ 2 ] ) == "table"  and
           ( trail[ 1 ] == start  or
             trail[ 2 ][ 1 ] == start ) then
            if about then
                trail = trail[ 2 ]
                for k = 1, #trail do
                    if trail[ k ] == about then
                        r = string.format( "%s/%d-%d", suite, i, k )
                        break -- k = 1, #trail
                    end
                end -- k = 1, #trail
            else
                r = string.format( "%s/%d-1", suite, i )
            end
            break -- i = 1, #Tutorial.def.trails
        end
    end -- i = 1, #Tutorial.def.trails
    if not r then
        local t
        if about then
            t = mw.title.new( string.format( "%s/%s",
                                             Tutorial.page.baseText,
                                             about ),
                              Tutorial.page.nsText )
            if t.exists then
                r = t.prefixedText
            end
        end
        if not r then
            t = mw.title.new( Tutorial.page.baseText,
                              Tutorial.page.nsText )
            r = t.prefixedText
        end
    end
    return r
end -- Tutorial.focus()



Tutorial.fold = function ( align )
    -- Create DEFAULTSORT and Category
    --     align  -- sortkey
    -- Returns:
    --     (string) may be empty
    local r
    if type( Tutorial.def.cat ) == "string" then
        local s = Tutorial.frame:callParserFunction( "DEFAULTSORT",
                                                     align )
        r = string.format( "\n[[Category:%s]]", Tutorial.def.cat )
    else
        r = ""
    end
    return r
end -- Tutorial.fold()



Tutorial.folder = function ()
    -- Create maintenance survey
    -- Returns:
    --     (mw.html)
    local r
    if type( Tutorial.def.trails ) == "table" then
        local dl     = mw.html.create( "dl" )
        local e      = mw.html.create( "span" )
        local room   = mw.site.namespaces[ Tutorial.page.namespace ]
        local story  = "[[%s:%s/%%s|%%s]]"
        local single = "[[%s:%s/%%d-%%d|%%d]]. " .. story .. " %s %%s %s"
        local stitch = "[[%s:%s/%%d-%%d|@]]/[[%s:%s/%%s|@]] %%s"
        local section, src, strict, sum, trail, ttips
        e:css( "font-size",   "smaller" )
         :css( "margin-left", "2em" )
         :newline()
         :wikitext( string.format( stitch,
                                   room.talk.name,
                                   Tutorial.page.baseText,
                                   room.talk.name,
                                   Tutorial.page.baseText ) )
        single = string.format( single,
                                Tutorial.page.nsText,
                                Tutorial.page.baseText,
                                Tutorial.page.nsText,
                                Tutorial.page.baseText,
                                mw.ustring.char( 8211 ),
                                tostring( e ) )
        story  = string.format( story,
                                Tutorial.page.nsText,
                                Tutorial.page.baseText )
        for i = 1, #Tutorial.def.trails do
            trail = Tutorial.def.trails[ i ]
            if type( trail ) == "table" then
                section = trail[ 1 ]
                if type( section ) == "string" then
                    r   = true
                    src = string.format( story, section, section )
                    e   = mw.html.create( "span" )
                    e:css( "font-size",   "smaller" )
                     :css( "margin-left", "2em" )
                     :newline()
                     :wikitext( string.format( "[[%s:%s/%s|@]]",
                                               room.talk.name,
                                               Tutorial.page.baseText,
                                               section ) )
                    dl:newline()
                      :node( mw.html.create( "dt" )
                                    :wikitext( src )
                                    :node( e ) )
                    src, sum = Tutorial.foreign( section )
                    if sum ~= section then
                        dl:newline()
                          :node( mw.html.create( "dd" )
                                        :wikitext( sum ) )
                    end
                    trail = trail[ 2 ]
                    if type( trail ) == "table" then
                        ttips = Tutorial.future( trail )
                        for k = 1, #trail do
                            strict = Tutorial.fenced( i, k, trail[ k ] )
                            src    = string.format( single,
                                                    i, k, k,
                                                    trail[ k ],
                                                    trail[ k ],
                                                    ttips[ k ],
                                                    k, k,
                                                    trail[ k ],
                                                    strict )
                            dl:newline()
                              :node( mw.html.create( "dd" )
                                            :wikitext( src ) )
                        end -- k = 1, #trail
                    end
                end
            end
        end -- i = 1, #Tutorial.def.trails
        if r then
            r = dl
        end
    end
    return r
end -- Tutorial.folder()



Tutorial.foreign = function ( access, above )
    -- Retrieve page and summary
    --     access  -- (string) subpage
    --     above   -- (string) supreme page, or nil
    -- Returns:
    --     (string)  source, or nil
    --     (string)  summary
    local page, source, sum, title
    if above then
        title = mw.title.new( above )
    else
        title = Tutorial.page
    end
    page   = mw.title.makeTitle( title.nsText,
                                 string.format( "%s/%s",
                                                title.baseText,
                                                access ) )
    source = page:getContent()
    if source then
        if not Tutorial.patSummary then
            Tutorial.patSummary = string.format( "%s%s%s%s%s",
                                                 "%{%{%s*#invoke:%s*",
                                                 "Tutorial%s*",
                                                 "|%s*s%l+%s*",
                                                 "|%s*summary%s*=%s*",
                                                 "([^%}]+)%s*%}%}" )
        end
        sum = source:match( Tutorial.patSummary )
        if sum then
            sum = mw.text.trim( sum )
        end
    end
    return source,  sum or access
end -- Tutorial.foreign()



Tutorial.forward = function ( apply, album, at, advance, about, alter )
    -- Arrow button (horizontal)
    --     apply    -- (mw.html) section
    --     album    -- (number) trail, or false
    --     at       -- (number) target section, or false
    --     advance  -- (number) 1: rightward -- -1: left -- 0: start
    --     about    -- (string) tooltip, or false
    --     alter    -- (boolean) true: small
    local e = mw.html.create( "div" )
    local j, k, m, s, src
    if advance == -1 then
        s = " left"
    elseif advance == 0 then
        s = ""
    else
        s = " right"
    end
    if alter then
        m = 9
        j = 1
        k = 2
    else
        m = 24
        j = 3
        k = 4
    end
    apply:css( "margin",   string.format( "%dpx", j ) )
    s   = string.format( "File:White triangle%s.svg", s )
    src = string.format( "[[%s|%dpx|link=%%s]]", s, m )
    if at then
        local btn = mw.html.create( "span" )
        btn:addClass( "mw-ui-button mw-ui-progressive" )
           :css( "padding",  string.format( "%dpx", k ) )
           :css( "min-width", "0" )
           :wikitext( string.format( src, "" ) )
        if album then
            local f = function ( allow, at, arrow )
                          local wrap = mw.html.create( "span" )
                          wrap:wikitext( string.format( "[%s %s]",
                                                        at, arrow ) )
                          if allow then
                              wrap:addClass( "nomobile" )
                          else
                              wrap:addClass( "mobile" )
                                  :css( "display", "none" )
                          end
                          apply:node( wrap )
                      end
            local show
            s = string.format( "%d-%d", album, at )
            btn:attr( "title",  about or s )
            s = string.format( "%s:%s/%s",
                               Tutorial.page.nsText,
                               Tutorial.page.baseText,
                               s )
            s = tostring( mw.uri.canonicalUrl( s ) )
            apply:addClass( "plainlinks" )
                 :attr( "role", "button" )
            show = tostring( btn )
            f( true, s, show )
            s = s:gsub( "(//%l+%.)", "%1m." )
            f( false, s, show )
        else
            btn:attr( "title", "Demo" )
            apply:node( btn )
        end
        apply:css( "background-color", "#3366CC" )    -- fallback
    else
        src = string.format( src, "|&#215;" )
        apply:css( "background-color", "#B0B0B0" )
             :css( "padding",  string.format( "%dpx", k ) )
             :wikitext( src )
    end
end -- Tutorial.forward()



Tutorial.full = function ( at )
    -- Collect all trail page contents
    --     at  -- trail name
    -- Returns: (string) collection, or nil
    local r, trail
    for i = 1, #Tutorial.def.trails do
        trail = Tutorial.def.trails[ i ]
        if type( trail ) == "table" then
            if trail[ 1 ] == at then
                if type( trail[ 2 ] ) == "table" then
                    trail = trail[ 2 ]
                    if type( trail ) == "table" then
                        local src, sum = Tutorial.foreign( at )
                        local e, m, n, s
                        e = mw.html.create( "div" )
                                   :css( "clear", "both" )
                                   :css( "margin-top", "1em" )
                                   :css( "margin-bottom", "1em" )
                                   :wikitext( Tutorial.txt.superior )
                        r = string.format( "%s\n%s\n%s\n__TOC__",
                                           Tutorial.flat( at ),
                                           sum,
                                           tostring( e ) )
                        for k = 1, #trail do
                            s = trail[ k ]
                            if type( s ) == "string" then
                                s2, sum = Tutorial.foreign( s )
                                if type( s2 ) == "string" then
                                    m, n = s2:find( "}}\n?[^\n]+\n" )
                                    if n then
                                        s  = s2:sub( 1,  n - 1 )
                                        s2 = s2:sub( n )
                                    else
                                        s  = s2
                                        s2 = ""
                                    end
                                    s2 = s:gsub( "'''", "" ) .. s2
                                    s  = Tutorial.frame:preprocess( s2 )
                                    e  = mw.html.create( "h2" )
                                                :wikitext( sum )
                                    r  = string.format( "%s\n%s\n%s",
                                                        r,
                                                        tostring( e ),
                                                        s )
                                end
                            end
                        end -- k = 1, #trail
                    end
                end
                break -- i = 1, #all
            end
        end
    end -- i = 1, #Tutorial.def.trails
    return r
end -- Tutorial.full()



Tutorial.further = function ( all, album, at )
    -- Trail navigation
    --     all     -- (table) subpages
    --     album   -- (number) trail
    --     at      -- (number) section
    -- Returns: (mw.html)
    local before = mw.html.create( "div" )
    local down   = mw.html.create( "div" )
    local pop    = mw.html.create( "div" )
    local post   = mw.html.create( "div" )
    local row    = mw.html.create( "div" )
    local r      = mw.html.create( "div" )
    local ul     = mw.html.create( "ul" )
    local ttips  = Tutorial.future( all, at )
    local e, m, s, support, t
    if at > 1 then
        m       = at - 1
        support = ttips[ m ]
    end
    before:css( "float", "left" )
    Tutorial.forward( before, album, m, -1, support )
    if at < #all then
        m       = at + 1
        support = ttips[ m ]
    else
        m = false
    end
    post:css( "float", "right" )
    Tutorial.forward( post, album, m, 1, support )
    row:node( before )
       :node( post )
    Tutorial.flipper( down, "tutorial" )
    if at > 1 then
        local beg = mw.html.create( "div" )
                           :css( "float", "left" )
        Tutorial.forward( beg, album, 1, 0, Tutorial.txt.starter, true )
        down = mw.html.create( "div" )
                      :node( beg )
                      :newline()
                      :node( down )
    end
    pop:attr( "id", "mw-customcollapsible-tutorial" )
       :addClass( "mw-collapsible mw-collapsed" )
       :addClass( "mobile" )
       :css( "display",    "none" )
       :css( "clear",      "both" )
       :css( "text-align", "left" )
       :newline()
    for i = 1, #all do
        s = all[ i ]
        e = mw.html.create( "span" )
        if i == at then
            e:css( "font-weight", "bold" )
        else
            t = mw.html.create( "span" )
                  :attr( "title", ttips[ i ] )
                  :wikitext( s )
            s = string.format( "[[%s:%s/%d-%d|%s]]",
                               Tutorial.page.nsText,
                               Tutorial.page.baseText,
                               album,
                               i,
                               tostring( t ) )
        end
        ul:node( mw.html.create( "li" )
                        :node( e:wikitext( s ) ) )
          :newline()
    end -- i = 1, #all
    pop:node( ul )
    r:css( "text-align", "center" )
     :node( row )
     :newline()
     :node( mw.html.create( "div" )
                   :css( "clear", "both" ) )
     :newline()
     :node( down )
     :newline()
     :node( pop )
    return r
end -- Tutorial.further()



Tutorial.future = function ( all, already, another, above )
    -- Retrieve tooltips
    --     all       -- (table) subpages
    --     already   -- (number) trail
    --     another   -- (string) text if already, or nil
    --     above     -- (string) supreme page, or nil
    -- Returns: (table)
    local r = { }
    local d, s, sub
    for i = 1, #all do
        if i == already then
            s = another or "'''&lt;*&gt;'''"
        else
            sub = all[ i ]
            d, s = Tutorial.foreign( sub, above )
        end
        -- TODO    plaintext
        table.insert( r, s )
    end -- i = 1, #all
    return r
end -- Tutorial.future()



-- Export
local p = { }

p.begin = function ( frame )
    -- Retrieve page name of first lesson in trail
    --     1 -- title part of trail or first page
    --     2 -- Wikipedia:Tutorial, or derive from page name
    local t = frame.args[ 2 ]
    local r = Tutorial.fetch( t )
    if type( r ) == "table" then
        Tutorial.def   = r
        Tutorial.frame = frame
        r              = Tutorial.focus( frame.args[ 1 ], false, t )
    end
    return r
end -- p.begin



p.gadget = function ( frame )
    -- Retrieve GUI elements
    local def = Tutorial.fetch( frame.args[ 1 ] )
    local r   = mw.html.create( "span" )
    if type( def ) == "table" then
        local sel = frame.args[ 2 ]
        Tutorial.def = def
        if sel == "go" then
            Tutorial.forward( r, false, true, 1, false, true )
        elseif sel == "top" then
            Tutorial.forward( r, false, true, 0, false, true )
        elseif sel == "expand" then
            r:wikitext( Tutorial.flipflop( true ) )
             :wikitext( Tutorial.flipflop( false ) )
        end
    end
    return r
end -- p.gadget



p.index = function ( frame )
    -- List all trails or lessons (main or maintenance page)
    local r = Tutorial.fetch()
    if type( r ) == "table" then
        local sel = frame.args[ 1 ]
        Tutorial.def   = r
        Tutorial.frame = frame
        if  frame.args.mode == "main"  or  not sel then
            sel = sel or Tutorial.page.prefixedText
            r   = Tutorial.father( sel )
        elseif sel == "trail" then
            local t2 = Tutorial.page.subpageText:match( "^(%d+)%-%d+$" )
            local t1
            if t2 then
                if type( Tutorial.def.trails ) == "table" then
                    t2  = tonumber( t2 )
                    sel = Tutorial.def.trails[ t2 ]
                    if type( sel ) == "table"  and
                       type( sel[ 2 ] ) == "table" then
                        t1 = sel[ 2 ]
                    end
                end
            else
                t1, t2 = Tutorial.find( Tutorial.page.subpageText )
            end
            if type( t1 ) == "table" then
                local less = faculty( frame.args, "less" )
                sel = string.format( "%s:%s",
                                     Tutorial.page.nsText,
                                     Tutorial.page.baseText )
                r   = Tutorial.features( sel, t1, t2, true, not less )
            else
                local e = mw.html.create( "div" )
                e:css( "clear", "both" )
                 :css( "margin-top", "1em" )
                 :css( "margin-bottom", "1em" )
                 :wikitext( string.format( Tutorial.txt.survey,
                                           Tutorial.focus() ) )
                r = tostring( e )
            end
        elseif sel == "maintain" then
            r = Tutorial.folder()
        end
    end
    return r or ""
end -- p.index



p.isVirtual = function ()
    -- Is this a virtual page?
    local r = Tutorial.page.subpageText:find( "^%d+%-%d+$" )
    if faculty( frame.args, "not" ) then
        r = not r
    end
    if r then
        r = "1"
    else
        r = ""
    end
    return r
end -- p.isVirtual



p.page = function ( frame )
    -- Virtual lesson page
    local r = Tutorial.fetch()
    if type( r ) == "table" then
        Tutorial.def = r
        local k, i = Tutorial.page.subpageText:match( "^(%d+)%-(%d+)$" )
        if k then
            Tutorial.frame = frame
            r = Tutorial.fake( tonumber( k ),  tonumber( i ) )
        else
            r = fault( Tutorial.txt.sequence )
        end
    else
        r = fault( r )
    end
    return r
end -- p.page



p.redirect = function ( frame )
    -- Retrieve page name of particular lesson in trail
    --     1 -- title part of trail or first page
    --     2 -- lesson
    --     3 -- Wikipedia:Tutorial, or derive from page name
    local t = frame.args[ 3 ]
    local r
    if t then
        Tutorial.page = mw.title.new( t )
    end
    r = Tutorial.fetch()
    if type( r ) == "table" then
        Tutorial.def   = r
        Tutorial.frame = frame
        r              = Tutorial.focus( frame.args[ 1 ],
                                         frame.args[ 2 ],
                                         t )
    end
    return r
end -- p.redirect



p.single = function ( frame )
    -- Single content page
    local r = Tutorial.fetch( false, true )
    if type( r ) == "table" then
        Tutorial.def = r
        local summary = frame.args.summary or ""
        if summary == "" then
            r = fault( Tutorial.txt.summary0 )
        else
            local parent = frame:getParent()
            Tutorial.frame = frame
            if parent:getTitle()  ==  Tutorial.page.prefixedText then
                r = Tutorial.flat( summary )
            else
                r = ""
            end
        end
    elseif r ~= "" then
        r = fault( r )
    end
    return r
end -- p.single



p.start = function ( frame )
    -- Start page of a trail
    local r = Tutorial.fetch()
    if type( r ) == "table" then
        local summary = frame.args.summary or ""
        if summary == "" then
            r = fault( Tutorial.txt.summary0 )
        else
            local sort
            Tutorial.def   = r
            Tutorial.frame = frame
            if type( Tutorial.def.trails ) == "table" then
                r = Tutorial.full( Tutorial.page.subpageText )
            end
            if r then
                sort = "* "
            else
                r    = Tutorial.txt.slot
                sort = "? "
            end
            r = r .. Tutorial.fold( sort .. Tutorial.page.subpageText )
        end
    else
        r = fault( r )
    end
    return r
end -- p.start



p.style = function ( frame )
    -- Retrieve standard style
    -- Parameter
    --     1 -- Wikipedia:Tutorial
    --     2 -- bgc, icon, ...
    local def = Tutorial.fetch( frame.args[ 1 ] )
    local r
    if type( def ) == "table" then
        def = def[ "style" ]
        if type( def ) == "table" then
            r = def[ frame.args[ 2 ] ]
        end
    end
    return r or ""
end -- p.style



p.failsafe = function ()
    return Tutorial.serial
end -- p.failsafe

return p