Zeile 1: |
Zeile 1: |
− | --[=[ 2015-04-04 | + | local Shortcuts = { suite = "Shortcuts", |
− | Module:Shortcuts
| + | serial = "2019-08-01", |
| + | item = 0 } |
| + | --[=[ |
| + | Support shortcut redirects |
| ]=] | | ]=] |
− | | + | local Failsafe = Shortcuts |
| | | |
| | | |
| -- local globals | | -- local globals |
− | local Config
| + | local Current = { rule = { } } |
− | local Current = { } | |
| local Errors = false | | local Errors = false |
| local Sort = false | | local Sort = false |
Zeile 69: |
Zeile 71: |
| return r | | return r |
| end -- facet() | | end -- facet() |
| + | |
| + | |
| + | |
| + | local function facilitated( attempt ) |
| + | -- Check whether Config string is a text formatting pattern |
| + | -- attempt -- string, with format pattern |
| + | -- Throws error with message, else does nothing |
| + | local s = Config[ attempt ] |
| + | if type( s ) ~= "string" or |
| + | not s:find( "%s", 1, true ) then |
| + | error( string.format( "/config '%s' invalid", attempt ) ) |
| + | end |
| + | end -- facilitated() |
| | | |
| | | |
Zeile 105: |
Zeile 120: |
| | | |
| | | |
− | local function failsafe( attempt ) | + | local function faculty( adjust ) |
− | -- Check whether Config string may is a text formatting pattern | + | -- Test template arg for boolean |
− | -- attempt -- string, with format pattern | + | -- adjust -- string or nil |
− | -- Throws error with message, else does nothing | + | -- Returns boolean |
− | local s = Config[ attempt ] | + | local s = type( adjust ) |
− | if type( s ) ~= "string" or | + | local r |
− | not s:find( "%s", 1, true ) then
| + | if s == "string" then |
− | error( string.format( "/config '%s' invalid", attempt ) ) | + | 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 failure( assigned, about ) |
| + | -- Add one message to Errors |
| + | -- assigned -- string, with shortcut page name |
| + | -- about -- string, with error keyword |
| + | if type( Errors ) ~= "table" then |
| + | Errors = { } |
| end | | end |
− | end -- failsafe() | + | Errors[ assigned ] = about |
| + | end -- failure() |
| | | |
| | | |
Zeile 133: |
Zeile 166: |
| return mw.text.trim( r ) | | return mw.text.trim( r ) |
| end -- fair() | | end -- fair() |
| + | |
| + | |
| + | |
| + | local function fallback( access, alt ) |
| + | -- Retrieve message text |
| + | -- access -- string, with message ID |
| + | -- alt -- string, with plain message, if no access |
| + | -- Returns string, with any message |
| + | local r = Config[ access ] |
| + | if type( r ) ~= "string" then |
| + | r = alt or "***** UNKNOWN MESSAGE " .. access .. " *****" |
| + | end |
| + | return r |
| + | end -- fallback() |
| | | |
| | | |
Zeile 140: |
Zeile 187: |
| -- alert -- string, with message, or other data | | -- alert -- string, with message, or other data |
| -- Returns message string with markup | | -- Returns message string with markup |
| + | local elem = mw.html.create( "span" ) |
| + | :addClass( "error" ) |
| local ecat = mw.message.new( "Scribunto-common-error-category" ) | | local ecat = mw.message.new( "Scribunto-common-error-category" ) |
− | local r = type( alert ) | + | local r = type( alert ) |
| if r == "string" then | | if r == "string" then |
| r = alert | | r = alert |
Zeile 147: |
Zeile 196: |
| r = "???? " .. r | | r = "???? " .. r |
| end | | end |
− | if ecat:isBlank() then | + | elem:wikitext( string.format( "FATAL LUA ERROR %s", r ) ) |
− | ecat = ""
| + | r = tostring( elem ) |
− | else
| + | if not ecat:isBlank() then |
− | ecat = string.format( "[[Category:%s]]", ecat:plain() ) | + | ecat = string.format( "%s[[Category:%s]]", r, ecat:plain() ) |
| end | | end |
− | r = string.format( "<span class=\"error\">FATAL LUA ERROR %s</span>",
| |
− | r )
| |
− | .. ecat
| |
| return r | | return r |
| end -- fatal() | | end -- fatal() |
Zeile 165: |
Zeile 211: |
| -- absent -- boolean, hide message, trigger category | | -- absent -- boolean, hide message, trigger category |
| -- Returns message with markup | | -- Returns message with markup |
− | local r = string.format( "<span class=\"error\">%s</span>", | + | local e = mw.html.create( "span" ) |
− | alert or "???" )
| + | :addClass( "error" ) |
| + | :wikitext( alert or "???" ) |
| + | local r = tostring( e ) |
| if absent and type( Config ) == "table" then | | if absent and type( Config ) == "table" then |
| if type( Config.suppress ) == "string" then | | if type( Config.suppress ) == "string" then |
− | failsafe( "suppress" ) | + | facilitated( "suppress" ) |
| r = string.format( Config.suppress, r ) | | r = string.format( Config.suppress, r ) |
| end | | end |
Zeile 178: |
Zeile 226: |
| return r | | return r |
| end -- fault() | | end -- fault() |
− |
| |
− |
| |
− |
| |
− | local function failure( assigned, about )
| |
− | -- Add one message to Errors
| |
− | -- assigned -- string, with shortcut page name
| |
− | -- about -- string, with error keyword
| |
− | if type( Errors ) ~= "table" then
| |
− | Errors = { }
| |
− | end
| |
− | Errors[ assigned ] = about
| |
− | end -- failure()
| |
| | | |
| | | |
Zeile 233: |
Zeile 269: |
| if page.exists then | | if page.exists then |
| if page.isRedirect then | | if page.isRedirect then |
− | local redirect, say
| |
| local story = page:getContent() | | local story = page:getContent() |
| local shift = story:match( "^#[^%[]+%[%[([^%]\n]+)%]%]" ) | | local shift = story:match( "^#[^%[]+%[%[([^%]\n]+)%]%]" ) |
− | if not shift or shift:match( "%%%x%x" ) then | + | local redirect |
| + | if not shift or |
| + | shift:match( "%%%x%x" ) then |
| redirect = false | | redirect = false |
| else | | else |
Zeile 242: |
Zeile 279: |
| end | | end |
| if not redirect then | | if not redirect then |
− | if type( Config.sayBadLink ) == "string" then | + | failure( alone, |
− | say = Config.sayBadLink
| + | fallback( "sayBadLink", "bad link encoding" ) ) |
− | else
| |
− | say = "bad link encoding"
| |
− | end
| |
− | failure( alone, say )
| |
| elseif mw.title.equals( Current.page, redirect ) then | | elseif mw.title.equals( Current.page, redirect ) then |
| for k, v in pairs( achieved ) do | | for k, v in pairs( achieved ) do |
| if mw.title.equals( page, v ) then | | if mw.title.equals( page, v ) then |
− | if type( Config.sayDuplicated ) == "string" then | + | failure( alone, |
− | say = Config.sayDuplicated
| + | fallback( "sayDuplicated", |
− | else
| + | "duplicated" ) ) |
− | say = "duplicated"
| |
− | end
| |
− | failure( alone, say )
| |
| page = false | | page = false |
| break -- for k, v | | break -- for k, v |
Zeile 265: |
Zeile 295: |
| end | | end |
| if Current.nsns == Current.nsn | | if Current.nsns == Current.nsn |
| + | and not Current.leave |
| and Config.signature | | and Config.signature |
| and not story:find( Config.signature, 15 ) then | | and not story:find( Config.signature, 15 ) then |
− | if type( Config.signal ) == "string" then | + | failure( alone, |
− | say = Config.signal
| + | fallback( "signal", |
− | else
| + | ".signature (category) missing" ) |
− | say = ".signature (category) missing"
| + | ) |
− | end
| |
− | failure( alone, say )
| |
| end | | end |
| else | | else |
− | if type( Config.sayTarget ) == "string" then | + | failure( alone, |
− | say = Config.sayTarget
| + | fallback( "sayTarget", "wrong target" ) ) |
− | else
| |
− | say = "wrong target"
| |
− | end
| |
− | failure( alone, say )
| |
| end | | end |
| else | | else |
− | if type( Config.sayRegular ) == "string" then | + | failure( alone, fallback( "sayRegular", "regular page" ) ) |
− | say = Config.sayRegular
| |
− | else
| |
− | say = "regular page"
| |
− | end
| |
− | failure( alone, say )
| |
| end | | end |
| else | | else |
− | if type( Config.sayMissing ) == "string" then | + | failure( alone, fallback( "sayMissing", "missing" ) ) |
− | say = Config.sayMissing
| |
− | else
| |
− | say = "missing"
| |
− | end
| |
− | failure( alone, say )
| |
| end | | end |
| end -- flag() | | end -- flag() |
Zeile 309: |
Zeile 324: |
| local r = "\n|}" | | local r = "\n|}" |
| local collect = { } | | local collect = { } |
− | local n | + | local n, previous |
| if type( alone ) == "number" then | | if type( alone ) == "number" then |
| n = factory( account, alone, collect ) | | n = factory( account, alone, collect ) |
Zeile 333: |
Zeile 348: |
| shortcut = string.format( "[[%s]]", v.shortcut ) | | shortcut = string.format( "[[%s]]", v.shortcut ) |
| second = string.format( "[[:%s]]", v.shift ) | | second = string.format( "[[:%s]]", v.shift ) |
− | r = fiat( shortcut, second ) .. r | + | if v.shortcut == previous then |
| + | failure( previous, |
| + | fallback( "sayDuplicated", "duplicated" ) ) |
| + | end |
| + | previous = v.shortcut |
| + | r = fiat( shortcut, second ) .. r |
| end -- for k, v | | end -- for k, v |
| end | | end |
Zeile 359: |
Zeile 379: |
| end | | end |
| if n == 0 then | | if n == 0 then |
− | if type( Config.sayNoPage ) == "string" then | + | r = fallback( "sayNoPage", "No target page" ) |
− | r = Config.sayNoPage
| |
− | else
| |
− | r = "No target page"
| |
− | end
| |
| else | | else |
| if type( arglist.space ) == "string" then | | if type( arglist.space ) == "string" then |
Zeile 380: |
Zeile 396: |
| end | | end |
| else | | else |
− | if type( Config.sayNoNamespace ) == "string" then | + | r = fallback( "sayNoNamespace", "No target namespace" ) |
− | r = Config.sayNoNamespace
| |
− | else
| |
− | r = "No target namespace"
| |
− | end
| |
| end | | end |
| end | | end |
Zeile 410: |
Zeile 422: |
| end | | end |
| if type( t ) ~= "table" then | | if type( t ) ~= "table" then |
− | if type( Config.sayNamespaceOff ) == "string" then
| + | r = string.format( "%d (%s) * %s", |
− | r = Config.sayNamespaceOff
| + | nsn, |
− | else
| + | space, |
− | r = "Namespace not configured"
| + | fallback( "sayNamespaceOff", |
− | end
| + | "Namespace not configured" ) |
− | r = string.format( "%d (%s) * %s", nsn, space, r ) | + | ) |
| end | | end |
| end | | end |
Zeile 451: |
Zeile 463: |
| end | | end |
| else | | else |
− | local say | + | shortcuts = fault( fallback( "sayUnregistered", |
− | if type( Config.sayUnregistered ) == "string" then
| + | "no shortcuts registered" ), |
− | say = Config.sayUnregistered
| + | false ) |
− | else
| |
− | say = "no shortcuts registered"
| |
− | end
| |
− | shortcuts = fault( say, false )
| |
| end | | end |
| r = r .. fiat( s, shortcuts ) | | r = r .. fiat( s, shortcuts ) |
Zeile 475: |
Zeile 483: |
| local pages = { } | | local pages = { } |
| local shortcuts = face( arglist.shortcuts ) | | local shortcuts = face( arglist.shortcuts ) |
| + | local style = Config.style |
| local got, r | | local got, r |
| if Config.show then | | if Config.show then |
− | failsafe( "show" ) | + | facilitated( "show" ) |
| r = string.format( Config.show, shortcuts ) | | r = string.format( Config.show, shortcuts ) |
| end | | end |
− | if Config.style and not Config.light then | + | if Current.style and Config[ Current.style ] then |
− | failsafe( "style" )
| + | style = Config[ Current.style ] |
− | r = string.format( Config.style, r )
| + | elseif Current.rule then |
− | if r:find( "###", 16, true ) then
| + | got = type( Current.rule.styling ) |
− | local s = shortcuts
| + | if got == "boolean" then |
− | local k = s:find( ",", 3, true )
| + | style = false |
− | if k then
| + | elseif got == "string" then |
− | s = s:sub( 1, k - 1 )
| + | style = Config[ Current.rule.styling ] |
| + | end |
| + | end |
| + | if style then |
| + | if not Config.light then |
| + | local s |
| + | facilitated( "style" ) |
| + | if style:find( ".sub.", 8, true ) then |
| + | style = style:gsub( "%.sub%.", "-sub" ) |
| + | end |
| + | if style:find( "-shortcut:", 16, true ) then |
| + | if Current.page.isSubpage then |
| + | s = "%1line-height: 0; top: -2.5em%2" |
| + | else |
| + | s = "%1top: -1em%2" |
| + | end |
| + | style = style:gsub( "([; '])%-shortcut:%s*top([; '])", |
| + | s ) |
| + | s = "%sdata-shortcut-clear-right='1'" |
| + | if style:find( s ) then |
| + | e = mw.html.create( "div" ) |
| + | e:css( { ["clear"] = "right", |
| + | ["height"] = "0" } ) |
| + | style = style:gsub( s, "" ) .. tostring( e ) |
| + | end |
| + | end |
| + | r = string.format( style, r ) |
| + | if r:find( "###", 16, true ) then |
| + | local k = shortcuts:find( ",", 3, true ) |
| + | s = shortcuts |
| + | if k then |
| + | s = s:sub( 1, k - 1 ) |
| + | end |
| + | r = r:gsub( "###", s, 1 ) |
| end | | end |
− | r = r:gsub( "###", s, 1 )
| |
| end | | end |
| + | else |
| + | r = "" |
| end | | end |
| got = shortcuts:gsub( "<s>[^<]+</s>", "" ) | | got = shortcuts:gsub( "<s>[^<]+</s>", "" ) |
Zeile 502: |
Zeile 545: |
| if Errors then | | if Errors then |
| local s = "Shortcuts * " | | local s = "Shortcuts * " |
| + | local t |
| for k, v in pairs( Errors ) do | | for k, v in pairs( Errors ) do |
− | s = string.format( "%s <u>[[%s]]</u>: %s", s, k, v ) | + | t = mw.title.new( k ) |
| + | s = string.format( "%s <u>[%s %s]</u>: %s", |
| + | s, |
| + | t:fullUrl( { redirect = "no" } ), |
| + | k, |
| + | v ) |
| end -- for k, v | | end -- for k, v |
| r = r .. fault( s, true ) | | r = r .. fault( s, true ) |
Zeile 517: |
Zeile 566: |
| -- .self -- string, target page | | -- .self -- string, target page |
| -- .shortcuts -- string, comma separated list of shortcuts | | -- .shortcuts -- string, comma separated list of shortcuts |
| + | -- .style -- string, particular style ID |
| + | -- .loose -- boolean, ignore undefined shortcuts |
| -- Throws error with message, else returns string with text | | -- Throws error with message, else returns string with text |
| local l, t, sub | | local l, t, sub |
Zeile 576: |
Zeile 627: |
| end -- for k, v | | end -- for k, v |
| end | | end |
− | if type( t ) == "table" then | + | if args.service == "template" then |
− | sub = Current.page.text
| + | if type( Config.rules ) == "table" then |
− | if type( t[ sub ] ) == "string" then
| + | local rules = Config.rules[ Current.nsn ] |
− | if args.shortcuts and not Config.locally then
| + | if type( rules ) == "table" then |
| + | local seek = Current.page.text |
| + | local scope |
| + | for k, v in pairs( rules ) do |
| + | if type( v ) == "table" then |
| + | if type( v.sub ) ~= "string" |
| + | or mw.ustring.match( seek, |
| + | v.sub ) then |
| + | Current.rule.styling = v.styling |
| + | Current.rule.locally = v.locally |
| + | end |
| + | end |
| + | end -- for k, v |
| + | end |
| + | end |
| + | if type( t ) == "table" then |
| + | sub = Current.page.text |
| + | if type( t[ sub ] ) == "string" then |
| + | if args.shortcuts and |
| + | not Config.locally then |
| + | args.shortcuts = false |
| + | r = fallback( "sayNoLocals", |
| + | "'1=' not permitted" ) |
| + | else |
| + | args.shortcuts = t[ sub ] |
| + | if Current.nsn == Current.nsnt then |
| + | args.shortcuts |
| + | = fair( args.shortcuts ) |
| + | end |
| + | end |
| + | elseif Current.rule.locally then |
| args.shortcuts = false | | args.shortcuts = false |
− | r = "'1=' not permitted"
| |
− | else
| |
− | args.shortcuts = t[ sub ]
| |
− | if Current.nsn == Current.nsnt then
| |
− | args.shortcuts = fair( args.shortcuts )
| |
− | end
| |
| end | | end |
| + | elseif Current.rule.locally then |
| + | args.shortcuts = false |
| end | | end |
− | end
| |
− | if args.service == "template" then
| |
| if type( args.shortcuts ) == "string" then | | if type( args.shortcuts ) == "string" then |
| + | local suitable = Config.patternSuitable or "" |
| + | local syntactic = "[_#%{%}|]" |
| + | suitable = string.format( "^[ -~%s]+$", |
| + | suitable ) |
| args.shortcuts = mw.text.trim( args.shortcuts ) | | args.shortcuts = mw.text.trim( args.shortcuts ) |
| if args.shortcuts == "" then | | if args.shortcuts == "" then |
− | r = "no shortcuts" | + | r = fallback( "sayNoShortcuts", |
− | elseif ( Current.nsn == 2 or Current.nsn == 3 ) | + | "no shortcuts" ) |
− | and not args.shortcuts:find( "/" ) then | + | elseif Current.leave and |
| + | not args.shortcuts:find( "/" ) then |
| leave = true | | leave = true |
| + | elseif mw.ustring.match( args.shortcuts, |
| + | suitable ) and |
| + | not args.shortcuts:match( syntactic ) then |
| + | lucky = true |
| else | | else |
− | lucky = true | + | r = fallback( "sayInvalidChar", |
| + | "shortcut with invalid character" ) |
| end | | end |
| elseif Current.page.prefixedText == Config.skip then | | elseif Current.page.prefixedText == Config.skip then |
Zeile 605: |
Zeile 690: |
| args.leave = true | | args.leave = true |
| lucky = true | | lucky = true |
| + | elseif args.loose then |
| + | leave = true |
| elseif Config.locally then | | elseif Config.locally then |
− | if type( Config.sayUnknown ) == "string" then | + | r = fallback( "sayUnknown", |
− | r = Config.sayUnknown
| + | "Shortcuts template:" |
− | else
| + | .. " page not registered," |
− | r = "Shortcuts template:"
| + | .. " '1=' missing" ) |
− | .. " page not registered, '1=' missing"
| |
− | end
| |
| end | | end |
− | if Current.page.id == 6167366 then | + | if not lucky and Current.rule.locally then |
| + | lucky = true |
| leave = true | | leave = true |
| end | | end |
Zeile 626: |
Zeile 712: |
| r = "" -- NOOP | | r = "" -- NOOP |
| elseif args.service == "template" then | | elseif args.service == "template" then |
| + | Current.style = args.style |
| r = follow( args ) | | r = follow( args ) |
| elseif args.service == "trows" then | | elseif args.service == "trows" then |
Zeile 654: |
Zeile 741: |
| local pars = frame:getParent().args | | local pars = frame:getParent().args |
| if params.service == "template" then | | if params.service == "template" then |
| + | params.loose = faculty( pars.loose ) |
| params.shortcuts = pars[ 1 ] | | params.shortcuts = pars[ 1 ] |
− | if pars.light then | + | if faculty( pars.light ) and |
− | params.light = ( pars.light == "1" )
| + | params.light and |
− | if params.light and frame.args.shortcut then
| + | frame.args.shortcut then |
− | params.shortcuts = frame.args.shortcut
| + | params.shortcuts = frame.args.shortcut |
| + | end |
| + | if params.shortcuts then |
| + | params.shortcuts = mw.text.trim( params.shortcuts ) |
| + | if params.shortcuts == "" then |
| + | params.shortcuts = false |
| end | | end |
| end | | end |
| + | params.style = pars.style |
| elseif params.service == "trows" then | | elseif params.service == "trows" then |
| local k, v, s | | local k, v, s |
Zeile 693: |
Zeile 787: |
| return r | | return r |
| end -- framed() | | end -- framed() |
| + | |
| + | |
| + | |
| + | Failsafe.failsafe = function ( atleast ) |
| + | -- Retrieve versioning and check for compliance |
| + | -- Precondition: |
| + | -- atleast -- string, with required version or "wikidata" or "~" |
| + | -- or false |
| + | -- Postcondition: |
| + | -- Returns string -- with queried version, also if problem |
| + | -- false -- if appropriate |
| + | local last = ( atleast == "~" ) |
| + | local since = atleast |
| + | local r |
| + | if last or since == "wikidata" then |
| + | local item = Failsafe.item |
| + | since = false |
| + | if type( item ) == "number" and item > 0 then |
| + | local entity = mw.wikibase.getEntity( string.format( "Q%d", |
| + | item ) ) |
| + | if type( entity ) == "table" then |
| + | local vsn = entity:formatPropertyValues( "P348" ) |
| + | if type( vsn ) == "table" and |
| + | type( vsn.value ) == "string" and |
| + | vsn.value ~= "" then |
| + | if last and vsn.value == Failsafe.serial then |
| + | r = false |
| + | else |
| + | r = vsn.value |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| + | if type( r ) == "nil" then |
| + | if not since or since <= Failsafe.serial then |
| + | r = Failsafe.serial |
| + | else |
| + | r = false |
| + | end |
| + | end |
| + | return r |
| + | end -- Failsafe.failsafe() |
| | | |
| | | |
Zeile 701: |
Zeile 838: |
| function p.template( frame ) | | function p.template( frame ) |
| return framed( frame, "template" ) or "" | | return framed( frame, "template" ) or "" |
− | end -- p.template | + | end -- .template |
| | | |
| function p.total( frame ) | | function p.total( frame ) |
| return framed( frame, "total" ) | | return framed( frame, "total" ) |
− | end -- p.total | + | end -- .total |
| | | |
| function p.trows( frame ) | | function p.trows( frame ) |
| return framed( frame, "trows" ) | | return framed( frame, "trows" ) |
− | end -- p.trows | + | end -- .trows |
| | | |
| function p.twoletters( frame ) | | function p.twoletters( frame ) |
| return framed( frame, "twoletters" ) | | return framed( frame, "twoletters" ) |
− | end -- p.twoletters | + | end -- .twoletters |
| | | |
| function p.test( args ) | | function p.test( args ) |
Zeile 724: |
Zeile 861: |
| local lucky, r = pcall( forward, args ) | | local lucky, r = pcall( forward, args ) |
| return r | | return r |
− | end -- p.test() | + | end -- .test() |
| + | |
| + | 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 -- .failsafe |
| + | |
| + | p.Shortcuts = function () |
| + | -- Module interface |
| + | return Shortcuts |
| + | end -- .Shortcuts |
| | | |
| return p | | return p |