local URIutil = { suite  = "URIutil",
                  serial = "2022-06-04",
                  item   = 19644443 };
--[=[
Utilities for URI etc.
* coreISSN()
* formatISBN()
* formatISSN()
* formatLCCN()
* isDNBvalid()
* isDOI()
* isEscValid()
* isGTINvalid()
* isHandle()
* isISBN()
* isISBNvalid()
* isISSNvalid()
* isLCCN()
* linkDNBopac()
* linkDOI()
* linkHandle()
* linkISBN()
* linkISSN()
* linkLCCN()
* linkPMID()
* linkURN()
* mayDOI()
* mayHandle()
* mayISBN()
* mayISSN()
* mayLCCN()
* mayURI()
* mayURN()
* plainISBN()
* targetISSN()
* uriDOI()
* uriHandle()
* uriURN()
* failsafe()
* URIutil()
loadData: URIutil/config URIutil/isbn URIutil/urn
]=]
local Failsafe = URIutil;
local CurrentPageName;



local factory = function ( attempt, allowX )
    -- Retrieve plain digits of attempt
    -- Precondition:
    --     attempt  -- string; with digits (+xX) and hyphens, not trimmed
    --     allowX   -- number; of (last) position for permitted xX
    --                 boolean; xX at last position permitted
    -- Postcondition:
    --     Returns   table; success
    --                      [1]...[8]/[10]...[13]  -- digits 0...9
    --                                                10 at last position
    --                      .hyphens  -- number of hyphens
    --                      .type     -- number of digits
    --               number; no string or bad length or data
    --                        0  -- no string
    --                       >0  -- unexpected char at position (trimmed)
    local r;
    if type( attempt ) == "string" then
        local c, i;
        local j = 0;
        local k = 1;
        local s = mw.text.trim( attempt );
        local n = mw.ustring.len( s );
        r = { hyphens = 0 };
        for i = 1, n do
            c = mw.ustring.codepoint( s,  i,  i + 1 );
            if c >= 48  and  c <= 57 then
                j      = j + 1;
                r[ j ] = c - 48;
                k      = false;
            elseif c == 45 then    -- hyphen
                if i > 1  and  i < n then
                    r.hyphens = r.hyphens + 1;
                    k         = i;
                else
                    r = j;
                    break;
                end
            elseif c == 88  or  c == 120 then    -- X x
                j = j + 1;
                if allowX  and  i == n then
                    if allowX == true  or  allowX == j then
                        r[ j ] = 10;
                    else
                        r = j;
                    end
                else
                    r = j;
                end
                break;
            else
                r = j;
                break;
            end
        end -- for i
        if type( r ) == "table" then
            r.type = j;
        end
    else
        r = 0;
    end
    return r;
end -- factory()



local faculty = function ( ask, auto )
    -- Evaluate possible string as boolean signal, if brief
    -- Precondition:
    --    ask   -- trimmed string or nil or other
    --    auto  -- fallback value if nil
    -- Postcondition:
    --     Returns appropriate value, or ask
    local r;
    if type( ask ) == "string" then
        if ask == "1"  or  ask == "" then
            r = true;
        elseif ask == "0"  or  ask == "-" then
            r = false;
        else
            r = ask;
        end
    elseif ask == nil then
        r = auto;
    else
        r = ask;
    end
    return r;
end -- faculty()



local fair = function ( assert )
    -- Compute check digit (11 minus modulo 11) for descending factor
    -- Precondition:
    --     assert  -- table; as of factory()
    --                .type  -- number of digits including check digit
    -- Postcondition:
    --     Returns checksum
    local i;
    local n = assert.type;
    local k = n;
    local r = 0;
    for i = 1,  n - 1 do
        r = r  +  k * assert[ i ];
        k = k - 1;
    end -- for i
    return  ( 11  -  r % 11 );
end -- fair()



local fault = function ( alert )
    -- Format error message by class=error
    -- Parameter:
    --     alert  -- string, error message
    -- Returns:
    --     string, HTML span
    return tostring( mw.html.create( "span" )
                            :addClass( "error" )
                            :wikitext( alert ) );
end -- fault()



local fetch = function ( acquire )
    -- Load data submodule
    -- Precondition:
    --     acquire  -- string, one of
    --                  "config"
    --                  "isbn"
    --                  "urn"
    -- Postcondition:
    --     Returns any table
    local r;
    if URIutil.data then
        r = URIutil.data[ acquire ];
    else
        URIutil.data = { };
    end
    if not r then
        local lucky, storage;
        storage = "Module:URIutil/" .. acquire;
        lucky, r = pcall( mw.loadData, storage );
        if not lucky then
            if not URIutil.suited  and
               type( URIutil.item ) == "number" then
                local suited = string.format( "Q%d", URIutil.item );
                URIutil.suited = mw.wikibase.getSitelink( suited )
                                 or  true;
            end
            if type( URIutil.suited ) == "string" then
                storage = string.format( "%s/%s",
                                         URIutil.suited, acquire );
                lucky, r = pcall( mw.loadData, storage );
            end
        end
        if type( r ) ~= "table" then
            r = { };
        end
        URIutil.data[ acquire ] = r;
    end
    return r;
end -- fetch()



local flop = function ( alert )
    -- Create link to (maintenance) category
    -- Precondition:
    --     alert  -- trimmed string with title, not empty, or nil
    -- Postcondition:
    --     Returns  link, or false
    local r;
    if type( alert ) == "string"  and
       alert ~= "-"  and
       alert ~= "" then
        r = string.format( "[[Category:%s]]", alert );
    end
    return r;
end -- flop()



local format = function ( assigned, ahead, amount )
    -- Convert part of digit sequence into string
    -- Precondition:
    --     assigned  -- table; as of factory()
    --     ahead     -- index of first digit
    --     amount    -- number of digits to append
    -- Postcondition:
    --     Returns  string with digits and hyphens
    local i, k;
    local r = "";
    for i = ahead,  ahead + amount - 1 do
        k = assigned[ i ];
        if k == 10 then
            r = r .. "X";
        else
            r = r .. tostring( k );
        end
    end -- for i
    return r;
end -- format()



local fullPageName = function ()
    -- Retrieve current page name
    -- Postcondition:
    --     Returns  string: current page name
    if not CurrentPageName then
        CurrentPageName = mw.title.getCurrentTitle().fullText;
    end
    return CurrentPageName;
end -- fullPageName()



local DNBfaith = function ( assert, ancestor )
    -- Compute DNB (also GND, ZDB) check digit and verify
    -- Precondition:
    --     assert    -- table; as of factory()
    --                  .type  -- until 11 including check digit
    --     ancestor  -- true: 2011 mode
    -- Postcondition:
    --     Returns  true: check digit matches
    -- 2013-09-01
    local k = fair( assert )  %  11;
    if ancestor then
        k  =  11 - k;
    end
    return  ( k == assert[ assert.type ] );
end -- DNBfaith()



local GTINfair = function ( assert )
    -- Compute GTIN check digit
    -- Precondition:
    --     assert  -- table; ~ 13 digits
    --                       .type  -- 13 ...
    -- Postcondition:
    --     Returns  number 0...9
    local i, k;
    local lead = true;
    local r    = 0;
    for i = 1,  assert.type - 1 do
        k   = assert[ i ];
        r = r + k;
        if lead then    -- odd
            lead = false;
        else    -- even
            r    = r + k + k;
            lead = true;
        end
    end -- for i
    r = (10  -  r % 10)   %   10;
    return  r;
end -- GTINfair()



local GTINfaith = function ( assert )
    -- Compute GTIN check digit and verify
    -- Precondition:
    --     assert  -- table; ~ 13 digits
    --                       .type  -- 13 ...
    -- Postcondition:
    --     Returns  true: check digit matches
    return  ( GTINfair( assert )  ==  assert[ assert.type ] );
end -- GTINfaith()



local ISBNfactory = function ( attempt )
    -- Retrieve plain digits of ISBN attempt
    -- Precondition:
    --     attempt  -- string with digits (+xX) and hyphens, not trimmed
    -- Postcondition:
    --     Returns   table; success
    --                      [1]...[13]  -- digits 0...9
    --                                     10 at ISBN-10 last position
    --                      .type       -- 10 or 13
    --                      .hyphens    -- 0... number of hyphens
    --               number; no string or bad length or data
    --                        0  -- no string
    --                       >0  -- unexpected char at position (trimmed)
    --                       -1  -- bad digit count
    --                       -2  -- bad bookland
    local r;
    if type( attempt ) == "string" then
        local s;
        r = factory( attempt, 10 );
        s = type( r );
        if s == "number"  and  r ~= 10  and  r > 6  and  r < 13 then
            r = factory( attempt, r );
            s = type( r );
        end
        if s == "table" then
            if r.type == 13 then
                if r[1] ~= 9  or
                   r[2] ~= 7  or
                   r[3] < 8 then
                    r = -2;
                end
            elseif r.type ~= 10 then
                r = -1;
            end
        end
    else
        r = 0;
    end
    return r;
end -- ISBNfactory()



local ISBNfaith = function ( assert )
    -- Compute ISBN check digit and verify
    -- Precondition:
    --     assert  -- table; as of ISBNfactory()
    --                       .type  -- 10 or 13
    -- Postcondition:
    --     Returns  true: check digit matches
    local r;
    if assert.type == 10 then
        local i;
        local k = 0;
        for i = 1, 9 do
            k = k  +  i * assert[ i ];
        end -- for i
        k = k % 11;
        r = ( k == assert[ 10 ] );
    elseif assert.type == 13 then
        r = GTINfaith( assert );
    else
        r = false;
    end
    return r;
end -- ISBNfaith()



local ISBNflat = function ( assigned )
    -- Plain digits of attempt ISBN
    -- Precondition:
    --     assigned  -- table; as of ISBNfactory()
    -- Postcondition:
    --     Returns  string with digits; ISBN-10 with 'X' at last position
    local i;
    local r = "";
    local n = assigned.type;
    if n == 10 then
        if assigned[ assigned.type ] == 10 then
            n = 9;
        end
    end
    for i = 1, n do
        r = r .. tostring( assigned[ i ] );
    end -- for i
    if n == 9 then
        r = r .. "X";
    end
    return r;
end -- ISBNflat()



local ISBNfold = function ( assigned, apply, allocate, already )
    -- Retrieve number of digits for ISBN publisher/group
    -- Precondition:
    --     assigned  -- table; as of ISBNfactory()
    --     apply     -- number; of bookland (978 or 979)
    --     allocate  -- number; of country
    --     already   -- number; position in assigned to inspect
    -- Postcondition:
    --     Returns  number of digits, at least 0
    local r        = 0;
    local def      = fetch( "isbn" );
    local bookland = def[ apply ];
    if type( bookland ) == "table"  then
        local country = bookland[ allocate ];
        if type( country ) == "table" then
            local e, i, j, k, m, v;
            for i = 1, 4 do
                v = country[ i ];
                if type( v ) == "table" then
                    m  =  tonumber( format( assigned, already, i ) );
                    for k, e in pairs( v ) do
                        if m >= e[ 1 ]  and  m <= e[ 2 ] then
                            r = e[ 3 ];
                            break; -- for k
                        end
                    end -- for k
                end
                if r > 0 then
                    break; -- for i
                end
            end -- for i
        end
    end
    return r;
end -- ISBNfold()



local ISBNformat = function ( attempt, assigned )
    -- Hyphen formatting; at least try minimum
    -- Precondition:
    --     attempt   -- string with presumable ISBN
    --     assigned  -- table; as of ISBNfactory()
    --                         .type     -- 10 or 13
    --                         .hyphens  -- 0...4
    -- Postcondition:
    --     Returns  string with digits and hyphens
    local r = false;
    local j, k, m, n;
    if assigned.type == 10 then
        m = 978;
        r = "";
        j = 1;
    else
        m = 970 + assigned[ 3 ];
        r = tostring( m ) .. "-";
        j = 4;
    end
    if assigned[ j ] < 8 then
        k = 1;
    else
        k = 2;
        if assigned[ j ] == 9  and
           assigned[ j + 1 ] > 4 then
            k = 3;
            if assigned[ j + 1 ] > 8 then
                k = 4;
                if assigned[ j + 2 ] > 8 then
                    k = 5;
                end
            end
        end
    end
    if k then
        n = format( assigned, j, k );
        r = string.format( "%s%s-", r, n );
        j = j + k;
        n = ISBNfold( assigned,  m,  tonumber( n ),  j );
        if n > 0 then
            r = string.format( "%s%s-",  r,  format( assigned, j, n ) );
            j = j + n;
        end
    end
    r = r .. format( assigned,  j,  assigned.type - j );
    if assigned[ assigned.type ] == 10 then
        r = r .. "-X";
    else
        r = string.format( "%s-%s",
                           r,
                           tostring( assigned[ assigned.type ] ) );
    end
    if not r then
        r = mw.ustring.upper( mw.text.trim( attempt ) );
    end
    return r;
end -- ISBNformat()



local ISSNfactory = function ( attempt )
    -- Retrieve plain digits of ISSN attempt
    -- Precondition:
    --     attempt  -- string with digits (+xX) and hyphens, not trimmed
    -- Postcondition:
    --     Returns   table; success
    --                      [1]...[13]  -- digits 0...9
    --                                     10 at ISSN-8 last position
    --                      .type       -- 8 or 13
    --                      .hyphens    -- 0... number of hyphens
    --               number; no string or bad length or data
    --                        0  -- no string
    --                       >0  -- unexpected char at position (trimmed)
    --                       -1  -- bad digit count
    --                       -2  -- bad issnland
    local r;
    if type( attempt ) == "string" then
        r = factory( attempt, 8 );
        if type( r ) == "table" then
            if r.type == 13 then
                if r[1] ~= 9  or
                   r[2] ~= 7  or
                   r[3] ~= 7 then
                    r = -2;
                end
            elseif r.type ~= 8 then
                r = -1;
            end
        end
    else
        r = 0;
    end
    return r;
end -- ISSNfactory()



local ISSNfaith = function ( assert )
    -- Compute ISSN check digit and verify
    -- Precondition:
    --     assert  -- table; as of ISSNfactory()
    --                       .type  -- 8 or 13
    -- Postcondition:
    --     Returns  true: check digit matches
    local r;
    if assert.type == 8 then
        local k = fair( assert );
        if k == 11 then
            r = ( assert[ 8 ]  ==  0 );
        else
            r = ( assert[ 8 ]  ==  k );
        end
    elseif assert.type == 13 then
        r = GTINfaith( assert );
    else
        r = false;
    end
    return r;
end -- ISSNfaith()



local ISSNformat = function ( assigned, achieve )
    -- Hyphen formatting of ISSN
    -- Precondition:
    --     assigned  -- table; as of ISSNfactory(), and valid
    --     achieve   -- 8 or 13
    -- Postcondition:
    --     Returns  string with digits and hyphens
    local r;
    if achieve == 8 then
        local x;
        if assigned.type == 8 then
            r = string.format( "%s-%s",
                               format( assigned, 1, 4 ),
                               format( assigned, 5, 3 ) );
            x = assigned[ 8 ];
        elseif assigned.type == 13 then
            r = string.format( "%s-%s",
                               format( assigned, 4, 4 ),
                               format( assigned, 8, 3 ) );
            x = fair( assigned );
        end
        if x == 10 then
            r = r .. "X";
        else
            r = r .. tostring( x );
        end
    elseif achieve == 13 then
        if assigned.type == 8 then
            r = string.format( "977-%s-00-%s",
                               format( assigned, 1, 7 ),
                               GTINfair( assigned ) );
        elseif assigned.type == 13 then
            r = string.format( "977-%s%s-%s",
                               format( assigned, 4, 7 ),
                               format( assigned, 10, 2 ),
                               tostring( assigned[ 13 ] ) );
        end
    end
    return r;
end -- ISSNformat()



local LCCNfactory = function ( attempt, allow )
    -- Retrieve segments of LCCN attempt (format since 2001)
    -- Precondition:
    --     attempt  -- string with presumable LCCN
    --     allow    -- false or string: "/"
    -- Postcondition:
    --     Returns  table; success
    --              false if not correct, bad data
    -- 2014-12-28
    local r   = false;
    local pat = "^%s*(%a*)(/?)(%d%S+)%s*$";
    local pre, sep, s = attempt:match( pat );
    if pre and s then
        local year, serial;
        if pre == "" then
            pre = false;
            if sep ~= "" then
                s = false;
            end
        elseif #pre > 3 then
            s = false;
        else
            pre = pre:lower();
        end
        if s then
            if allow ~= "/"  or  sep == "/" then
                if sep == "/" then
                    year, serial = s:match( "^(%d+)/(%d.+)$" );
                elseif s:find( "-", 2, true ) then
                    year, serial = s:match( "^(%d+)%-(%d.+)$" );
                else
                    year = s:match( "^([%d]+)" );
                    if year then
                        if #year <= 8 then
                            year   = s:sub( 1, 2 );
                            serial = s:sub( 3 );
                        elseif #year <= 10 then
                            year   = s:sub( 1, 4 );
                            serial = s:sub( 5 );
                        else
                            year   = false;
                            serial = s;
                        end
                    elseif tonumber( s ) then
                        serial = s;
                    end
                end
            end
            if year then
                if #year == 4 then
                    local n = tonumber( year );
                    if n <= 2000 then
                        -- 2000 -> "00"
                        serial = false;
                    elseif n > tonumber( os.date( "%Y" ) ) then
                        serial = false;
                    end
                elseif #year ~= 2 then
                    serial = false;
                end
            end
            if serial then
                r = { pre = pre, serial = serial };
                if year then
                    r.year = year;
                end
                if serial:find( "/", 2, true ) then
                    local q;
                    serial, q = serial:lower()
                                      :match( "^(%d+)/([a-z]+)$" );
                    if q == "dc" or
                       q == "mads" or
                       q == "marcxml" or
                       q == "mods" then
                        r.serial    = serial;
                        r.qualifier = q;
                    end
                end
                if serial then
                    serial = serial:match( "^0*([1-9]%d*)$" );
                end
                if not serial then
                    r = false;
                elseif #serial < 6 then
                    r.serial = string.format( "%06d",
                                              tonumber( serial ) );
                elseif #serial > 6 then
                    r = false;
                end
            end
        end
    end
    return r;
end -- LCCNfactory()



local LCCNformat = function ( assigned, achieve )
    -- Standard or hyphen or slash formatting of LCCN
    -- Precondition:
    --     assigned  -- table; as of LCCNfactory(), and valid
    --     achieve   -- additional formatting desires, like "-" or "/"
    -- Postcondition:
    --     Returns  string with letters, digits and hyphens
    -- 2013-07-14
    local r;
    if assigned.pre then
        r = assigned.pre;
    else
        r = "";
    end
    if assigned.year then
        if achieve == "/"  and  r ~= "" then
            r = r .. "/";
        end
        r = r .. assigned.year;
        if achieve then
            r = r .. achieve;
        end
    end
    if assigned.serial then
        r = r .. assigned.serial;
    end
    if assigned.qualifier then
        r = string.format( "%s/%s", r, assigned.qualifier );
    end
    return r;
end -- LCCNformat()



local LCCNforward = function ( attempt, achieve )
    -- Retrieve bracketed titled external LCCN permalink
    -- Precondition:
    --     attempt  -- string with presumable LCCN
    --     achieve  -- additional title formatting desires, like "-"
    -- Postcondition:
    --     Returns  link, or plain attempt if bad LCCN
    -- 2015-08-10
    local lccn = LCCNfactory( attempt );
    local r;
    if lccn then
        r = LCCNformat( lccn, false );
        if r then
            local e, s;
            if achieve then
                s = LCCNformat( lccn, achieve );
                if s:find( "-", 2, true ) then
                    e = mw.html.create( "span" )
                               :css( "white-space", "nowrap" );
                end            
            else
                s = r;
            end            
            s = string.format( "[https://lccn.loc.gov/%s %s]", r, s );
            if e then
                r = tostring( e:wikitext( s ) );
            else
                r = s;
            end            
        end
    else
        r = attempt;
    end
    return r;
end -- LCCNforward()



local URNnamespace = function ( area, acquire )
    -- Are these parts of a correct URN?
    -- Precondition:
    --     area     -- string with lowercase namespace
    --     acquire  -- string with identification
    -- Postcondition:
    --     Returns  false if no problem detected
    --              string with violation
    local r;
    if area == "urn" then
        r = "urn:";
    else
        local s = fetch( "urn" ).sns;
        if type( s ) == "string" then
            r = string.format( ":%s:", area );
            if s:match( r ) then
                s = "[^%w%(%)%+,%-%.:=/@;%$_!%*'].*$";
                r = acquire:match( s );
            else
                r = string.format( "&#58;%s:", area );
            end
            if not r then
                r = false;
                if area == "isbn" then
                    if not URIutil.isISBNvalid( acquire ) then
                        r = acquire;
                    end
                elseif area == "issn" then
                    if not URIutil.isISSNvalid( acquire ) then
                        r = acquire;
                    end
                end
            end
        end
    end
    return r;
end -- URNnamespace()



local URNresolve = function ( assigned, ask, alter )
    -- Resolve URN within space
    -- Precondition:
    --     assigned  -- table with resolvers for this space
    --     ask       -- string with ID within this space
    --     alter     -- string with alternative resolver, or not
    -- Postcondition:
    --     Returns
    --         1.    URL of resolver, or nil
    --         2.    modified ask
    local resolver = assigned;
    local sign     = ask;
    local subset   = assigned[ ":" ];
    local r;
    if subset then
        local s = sign:match( subset );
        if s then
            s    = s:lower();
            sign = s .. sign:sub( #s + 1 )
            if assigned[ s ] then
                resolver = assigned[ s ];
            end
        end
    end
    if alter then
        r = resolver[ alter ];
    end
    if not r then
        r = resolver[ "*" ];
    end
    return r, sign;
end -- URNresolve()



function URIutil.coreISSN( attempt )
    -- Fetch significant ISSN
    -- Precondition:
    --     attempt  -- string with presumable ISSN
    -- Postcondition:
    --     Returns   string with 7 digits, without check digit nor GTIN
    --               unmodified input if wrong
    local r;
    local issn = ISSNfactory( attempt );
    if type( issn ) == "table" then
        if issn.type == 8 then
            r = format( issn, 1, 7 );
        elseif issn.type == 13 then
            r = format( issn, 4, 7 );
        end
    else
        r = mw.ustring.upper( mw.text.trim( attempt ) );
    end
    return r;
end -- URIutil.coreISSN()



function URIutil.formatISBN( attempt, assigned )
    -- Format ISBN, if no hyphens present
    -- Precondition:
    --     attempt   -- string with presumable ISBN
    --     assigned  -- table or false; as of ISBNfactory()
    -- Postcondition:
    --     Returns   string with some hyphens, if not yet
    --               unmodified input if already hyphens or wrong
    local r;
    local isbn;
    if type( assigned ) == "table" then
        isbn = assigned;
    else
        isbn = ISBNfactory( attempt );
    end
    if type( isbn ) == "table" then
        r = ISBNformat( attempt, isbn );
    else
        r = mw.ustring.upper( mw.text.trim( attempt ) );
    end
    return r;
end -- URIutil.formatISBN()



function URIutil.formatISSN( attempt, achieve )
    -- Format ISSN
    -- Precondition:
    --     attempt  -- string with presumable ISSN
    --     achieve  -- false or 8 or 13; requested presentation
    -- Postcondition:
    --     Returns   string with some hyphens, if not yet
    --               unmodified input if already hyphens or wrong
    local r    = false;
    local issn = ISSNfactory( attempt );
    if type( issn ) == "table" then
        if ISSNfaith( issn ) then
            local k, m;
            if type( achieve ) == "string" then
                m = tonumber( achieve );
            else
                m = achieve;
            end
            if m == 8  or m == 13 then
                k = m;
            else
                k = issn.type;
            end
            r = ISSNformat( issn, k );
        end
    end
    if not r then
        r = mw.ustring.upper( mw.text.trim( attempt ) );
    end
    return r;
end -- URIutil.formatISSN()



function URIutil.formatLCCN( attempt, achieve )
    -- Standard or hyphen formatting of LCCN
    -- Precondition:
    --     attempt  -- string with presumable LCCN
    --     achieve   -- additional formatting desires, like "-"
    -- Postcondition:
    --     Returns  string with letters, digits and hyphens
    --              unmodified input if wrong
    local r = LCCNfactory( attempt );
    if r then
        r = LCCNformat( r, achieve );
    end
    return r;
end -- URIutil.formatLCCN()



function URIutil.isDNBvalid( attempt, also )
    -- Is this DNB (also GND, ZDB) formally correct (check digit)?
    -- Precondition:
    --     attempt  -- string with any presumable DNB code
    --     also     -- string or nil; optional requirement DMA GND SWD
    --                 "ZDB"  -- permit hyphen, but use >2011 rule
    --                 DMA starting with 3 and no hyphen
    --                 GND not DNB2011
    --                 SWD DNB2011 starting with 4 or 7 and no X check
    -- Postcondition:
    --     Returns  number of digits or 2011, if valid
    --              false if not correct, bad data or check digit wrong
    local s = mw.text.trim( attempt );
    local j = s:find( "/", 5, true );
    local r = false;
    local dnb;
    if j then
        s = attempt:sub( 1,  j - 1 );
    end
    j = s:find( "-", 2, true );
    if j then
        if j > 3  and  j <= 8 then
            if s:match( "^[0-9]+-[0-9xX]$" ) then
                dnb = factory( s, true );
            end
        end
    elseif #s > 6 then
        if s:match( "^[0-9]+[0-9xX]$" ) then
            dnb = factory( s, #s );
        end
    end
    if type( dnb ) == "table" then
        if j then
            if DNBfaith( dnb, true ) then
                r = 2011;
            elseif type( also ) == "string" then
                s = mw.text.trim( also );
                if s == "ZDB" then
                    if DNBfaith( dnb, false ) then
                        r = dnb.type;
                    end
                end
            end
        else
            if DNBfaith( dnb, false ) then
                r = dnb.type;
            elseif type( also ) == "string" then
                s = mw.text.trim( also );
                if s == "ZDB" then
                    if DNBfaith( dnb, true ) then
                        r = dnb.type;
                    end
                end
            end
        end
    end
    return r;
end -- URIutil.isDNBvalid()



function URIutil.isDOI( attempt )
    -- Is this a syntactically correct DOI?
    -- Precondition:
    --     attempt  -- string with presumable DOI code
    -- Postcondition:
    --     Returns  number of organization, if valid
    --              false if not correct, bad character or syntax
    local r = false;
    local k, s = attempt:match( "^%s*10%.([1-9][0-9]+)/(.+)%s*$" );
    if k then
        k = tonumber( k );
        if k >= 1000  and  k < 100000000 then
            local pc = "^[0-9A-Za-z%(%[<%./]"
                       .. "[%-0-9/A-Z%.a-z%(%)_%[%];,:<>%+]*"
                       .. "[0-9A-Za-z%)%]>%+#]$"
            s = mw.uri.decode( mw.text.decode( s ), "PATH" );
            if s:match( pc )  or  s:match( "^%w$" ) then
                r = k;
            end
        end
    end
    return r;
end -- URIutil.isDOI()



function URIutil.isEscValid( attempt )
    -- Are bad percent escapings in attempt?
    -- Precondition:
    --     attempt  -- string with possible percent escapings
    -- Postcondition:
    --     Returns  string with violating sequence
    --              false if correct
    local i = 0;
    local r = false;
    local h, s;
    while i do
        i = attempt:find( "%", i, true );
        if i then
            s = attempt:sub( i + 1,  i + 2 );
            h = s:match( "%x%x" );
            if h then
                if h == "00" then
                    r = "%00";
                    break; -- while i
                end
                i = i + 2;
            else
                r = "%" .. s;
                break; -- while i
            end
        end
    end -- while i
    return r;
end -- URIutil.isEscValid()



function URIutil.isGTINvalid( attempt )
    -- Is this GTIN (EAN) formally correct (check digit)?
    -- Precondition:
    --     attempt  -- string with presumable GTIN
    -- Postcondition:
    --     Returns  GTIN length
    --              false if not correct, bad data or check digit wrong
    local r;
    local gtin = factory( attempt, false );
    if type( gtin ) == "table" then
        if gtin.type == 13 then
            if GTINfaith( gtin ) then
                r = gtin.type;
            end
        else
            r = false;
        end
    else
        r = false;
    end
    return r;
end -- URIutil.isGTINvalid()



function URIutil.isHandle( attempt )
    -- Is this a meaningful handle for handle.net?
    -- Precondition:
    --     attempt  -- string with presumable handle code
    -- Postcondition:
    --     Returns  number of primary authority, if valid
    --              false if not correct, bad character or syntax
    local r = attempt:match( "^%s*([^/%s]+)/%S+%s*$" );
    if r then
        local k = r:find( ".", 1, true );
        if k then
            if k == 1  or  r:match( "%.$" ) then
                r = false;
            else
                r = r:sub( 1,  k - 1 );
            end
        end
        if r then
            if r:match( "^[1-9][0-9]+$" ) then
                r = tonumber( r );
            else
                r = false;
            end
        end
    else
        r = false;
    end
    return r;
end -- URIutil.isHandle()



function URIutil.isISBN( attempt )
    -- Is this a syntactically correct ISBN?
    -- Precondition:
    --     attempt  -- string with presumable ISBN
    -- Postcondition:
    --     Returns
    --        1  -- 10 if 10 digits and hyphens; also X at end of ISBN-10
    --              13 if 13 digits and hyphens; beginning with bookland
    --              false if not an ISBN
    --        2  -- internal table, if (1)
    --              number; no string or bad length or data
    --                       0  -- no string
    --                      >0  -- unexpected char at position (trimmed)
    --                      -1  -- bad digit count
    --                      -2  -- bad bookland
    local r;
    local isbn = ISBNfactory( attempt );
    if type( isbn ) == "table" then
        r = isbn.type;
    else
        r = false;
    end
    return r, isbn;
end -- URIutil.isISBN()



function URIutil.isISBNvalid( attempt )
    -- Is this ISBN formally correct (check digit)?
    -- Precondition:
    --     attempt  -- string with presumable ISBN
    -- Postcondition:
    --     Returns
    --        1  -- 10 if 10 digits and hyphens; also X at end of ISBN-10
    --              13 if 13 digits and hyphens; beginning with bookland
    --              false if not correct, bad data or check digit wrong
    --        2  -- internal table, if (1)
    --              number; no string or bad length or data
    --                       0  -- no string
    --                      >0  -- unexpected char at position (trimmed)
    --                      -1  -- bad digit count
    --                      -2  -- bad bookland
    local r    = false;
    local isbn = ISBNfactory( attempt );
    if type( isbn ) == "table" then
        if ISBNfaith( isbn ) then
            r = isbn.type;
        end
    end
    return r, isbn;
end -- URIutil.isISBNvalid()



function URIutil.isISSNvalid( attempt )
    -- Is this ISSN formally correct (check digit)?
    -- Precondition:
    --     attempt  -- string with presumable ISSN
    -- Postcondition:
    --     Returns  8 if 8 digits and up to 1 hyphen; also X at end
    --              13 if 13 digits and hyphens; beginning with 977
    --              false if not correct, bad data or check digit wrong
    local r    = false;
    local issn = ISSNfactory( attempt );
    if type( issn ) == "table" then
        if ISSNfaith( issn ) then
            r = issn.type;
        end
    end
    return r;
end -- URIutil.isISSNvalid()



function URIutil.isLCCN( attempt, allow )
    -- Is this a syntactically correct LCCN?
    -- Precondition:
    --     attempt  -- string with presumable LCCN
    --     allow    -- false or string: "/"
    -- Postcondition:
    --     Returns  string with LCCN formatted aa9999-99999999
    --              false if not correct, bad data
    local r    = false;
    local lccn = LCCNfactory( attempt, allow );
    if lccn then
        r = LCCNformat( lccn, "-" );
    end
    return r;
end -- URIutil.isLCCN()



function URIutil.linkDNBopac( attempt, about, allow, abbr, alert )
    -- Retrieve bracketed titled external DNB opac link
    -- Precondition:
    --     attempt  -- string with presumable DNB ID
    --     about    -- title, or false
    --     allow    -- true: permit invalid ID
    --     abbr     -- true: link DNB abbreviation
    --     alert    -- string with title of maintenance category, or nil
    -- Postcondition:
    --     Returns  link, or plain string if bad DNB
    local r = allow  or  URIutil.isDNBvalid( attempt );
    local s = "DNB";
    if abbr  and  not about then
        local cnf = fetch( "config" );
        if cnf.supportDNB   and  cnf.supportDNB ~= fullPageName() then
            s = string.format( "[[%s|DNB]]", cnf.supportDNB );
        end
    end
    if r then
        if about then
            r = about;
        else
            r = attempt;
        end
        r = string.format( "%s&nbsp;[%s%s%s%s%s %s]",
                           s,
                           "https://portal.dnb.de/opac.htm",
                           "?referrer=Wikipedia",
                           "&method=simpleSearch&cqlMode=true",
                           "&query=idn%3D",
                           attempt,
                           r );
    else
        r = string.format( "%s&nbsp;%s", s, attempt );
        if about then
            r = string.format( "%s %s", r, about );
        end
        if alert then
            r = r .. flop( alert );
        end
    end
    return r;
end -- URIutil.linkDNBopac()



function URIutil.linkDOI( attempt, any1, any2, any3, alert )
    -- Retrieve bracketed titled external link on DOI resolver
    -- Precondition:
    --     attempt  -- string with presumable DOI
    --     any1     -- intentionally dummy parameter
    --     any2     -- intentionally dummy parameter
    --     any3     -- intentionally dummy parameter
    --     alert    -- string with title of maintenance category, or nil
    -- Postcondition:
    --     Returns  external link, or false
    local r = URIutil.isDOI( attempt );
    if r then
        local e = mw.html.create( "span" )
                         :addClass( "uri-handle" )
                         :css( "white-space", "nowrap" );
        local s;
        s, r = attempt:match( "^%s*(10%.[1-9][0-9]+/)(.+)%s*$" );
        r = mw.text.decode( r, "PATH" );
        r = string.format( "[%s/%s%s %s%s]",
                           "https://doi.org",
                           s,
                           mw.uri.encode( r ),
                           s,
                           mw.text.encode( r, "<>&%]" ) );
        r = tostring( e:wikitext( r ) );
    else
        r = flop( alert );
    end
    return r;
end -- URIutil.linkDOI()



function URIutil.linkHandle( attempt, any1, any2, any3, alert )
    -- Retrieve bracketed titled external link on handle resolver
    -- Precondition:
    --     attempt  -- string with presumable handle
    --     any1     -- intentionally dummy parameter
    --     any2     -- intentionally dummy parameter
    --     any3     -- intentionally dummy parameter
    --     alert    -- string with title of maintenance category, or nil
    -- Postcondition:
    --     Returns  external link, or false
    local r = URIutil.isHandle( attempt );
    if r then
        local e = mw.html.create( "span" )
                         :addClass( "uri-handle" )
                         :css( "white-space", "nowrap" );
        r = mw.text.decode( mw.text.trim( attempt ),  "PATH" );
        r = string.format( "[%s%s %s]",
                           "https://hdl.handle.net/",
                           mw.uri.encode( r ),
                           mw.text.encode( r, "<>&%]" ) );
        r = tostring( e:wikitext( r ) );
    else
        r = flop( alert );
    end
    return r;
end -- URIutil.linkHandle()



function URIutil.linkISBN( attempt, allow, abbr, adhere, alert )
    -- Retrieve bracketed titled wikilink on booksources page with "ISBN"
    -- Precondition:
    --     attempt  -- string with presumable ISBN
    --     allow    -- true: permit invalid check digit or digit count
    --     abbr     -- true or string: link ISBN abbreviation
    --     adhere   -- true: use &nbsp;  else: use simple space
    --     alert    -- string with title of maintenance category, or nil
    -- Postcondition:
    --     Returns  link
    local e      = mw.html.create( "span" );
    local source = mw.text.trim( attempt );
    local r      = string.format( "[[Special:Booksources/%s|", source );
    local isbn   = ISBNfactory( source );
    local lapsus;
    if type( isbn ) == "table" then
        local lenient;
        if type( allow ) == "string" then
            lenient = ( allow ~= "0" );
        else
            lenient = allow;
        end
        if lenient then
            lapsus = false;
        else
            lapsus = ( not ISBNfaith( isbn ) );
        end
        r = r .. ISBNformat( attempt, isbn );
    else
        lapsus = not allow;
        r      = r .. source;
    end
    r = r .. "]]";
    e:css( "white-space", "nowrap" );
    if lapsus then
        r = r .. fault( "(?!?!)" );
        e:addClass( "invalid-ISBN" )
         :wikitext( r );
        r = tostring( e );
        if alert then
            r = r .. flop( alert );
        end
    else
        e:wikitext( r );
        r = tostring( e );
    end
    if adhere then
        r = "&nbsp;" .. r;
    else
        r = " " .. r;
    end
    if abbr then
        local cnf = fetch( "config" );
        local s   = cnf.supportISBN;
        if s then
            if type( s ) ~= "string"
               or  s == "-"
               or  s == "" then
                s = false;
            end
        else
            s = "International Standard Book Number";
        end
        if s  and  s ~= fullPageName() then
            s = string.format( "[[%s|ISBN]]", s );
        else
            s = "ISBN";
        end
        r = string.format( "%s&#160;%s", s, r );
    else
        r = "ISBN" .. r;
    end
    return r;
end -- URIutil.linkISBN()



function URIutil.linkISSN( attempt, allow, abbr, adhere, alert )
    -- Retrieve bracketed titled external link on ISSN DB with "ISSN"
    -- Precondition:
    --     attempt  -- string with presumable ISSN
    --     allow    -- true: permit invalid check digit
    --     abbr     -- true: link ISSN abbreviation
    --     adhere   -- true: use &nbsp;  else: use simple space;
    --     alert    -- string with title of maintenance category, or nil
    -- Postcondition:
    --     Returns  link
    local r = URIutil.targetISSN( attempt, allow, nil, nil, alert );
    if adhere then
        r = "&#160;" .. r;
    else
        r = " " .. r;
    end
    if abbr then
        local cnf  = fetch( "config" );
        local s = cnf.supportISSN;
        if s then
            if type( s ) ~= "string"
               or  s == "-"
               or  s == "" then
                s = false;
            end
        else
            s = "International Standard Serial Number";
        end
        if s  and  s ~= fullPageName() then
            if s == "ISSN" then
                s = "[[ISSN]]";
            else
                s = string.format( "[[%s|ISSN]]", s );
            end
        else
            s = "ISSN";
        end
        r = string.format( "%s%s", s, r );
    else
        r = "ISSN" .. r;
    end
    return r;
end -- URIutil.linkISSN()



function URIutil.linkLCCN( attempt, achieve, any1, any2, alert )
    -- Retrieve bracketed titled external LCCN permalink
    -- Precondition:
    --     attempt  -- string with presumable LCCN
    --     achieve  -- additional title formatting desires, like "-"
    --     any1     -- intentionally dummy parameter
    --     any2     -- intentionally dummy parameter
    --     alert    -- string with title of maintenance category, or nil
    -- Postcondition:
    --     Returns  link, or false if bad LCCN
    local r = LCCNforward( attempt, achieve );
    if not r then
        r = flop( alert );
    end
    return r;
end -- URIutil.linkLCCN()



function URIutil.linkPMID( attempt, any, abbr, adhere, alert )
    -- Retrieve external PMID link with "PMID"
    -- Precondition:
    --     attempt  -- string or number with presumable PMID
    --     any      -- intentionally dummy parameter
    --     abbr     -- true: link PMID abbreviation
    --     adhere   -- true: use &nbsp;  else: use simple space;
    --     alert    -- string with title of maintenance category, or nil
    -- Postcondition:
    --     Returns  link
    local s = type( attempt );
    local legal, r;
    if s == "string" then
        r = mw.text.trim( attempt );
        if r == "" then
            r = "PMID";
            s = "empty";
        end
    elseif s == "number" then
        r = tostring( attempt );
        s = "string";
    else
        r = "PMID";
    end
    if s == "string" then
        legal = r:match( "^[1-9]%d*$" );
        if legal then
            local cnf = fetch( "config" );
            if cnf then
                s = cnf.pmid;
                if type( s ) == "string"  and
                   s ~= "-"  and
                   s ~= "" then
                    local sep;
                    if s:find( "$1", 5, true ) then
                        s = s:gsub( "%$1", r );
                    else
                        s = s .. r;
                    end
                    r = string.format( "[%s %s]", s, r );
                    if adhere then
                        sep = "&#160;";
                    else
                        sep = " ";
                    end
                    if abbr then
                        s = cnf.supportPMID;
                        if type( s ) == "string" and
                           s ~= "-"  and
                           s ~= ""  and
                           s ~= fullPageName() then
                            if s == "PMID" then
                                s = "[[PMID]]";
                            else
                                s = string.format( "[[%s|PMID]]", s );
                            end
                        else
                            s = "PMID";
                        end
                    else
                        s = "PMID";
                    end
                    r = string.format( "%s%s%s", s, sep, r );
                else
                    cnf = false;
                end
            end
            if not cnf then
                r = string.format( "PMID %s", r );
            end
        else
            r = string.format( "PMID %s", r );
        end
    end
    if not legal then
        local e = mw.html.create( "span" )
                         :addClass( "invalid-PMID" )
                         :wikitext( r );
        r = tostring( e ) .. fault( "(?!?!)" );
        if alert then
            r = r .. flop( alert );
        end
    end
    return r;
end -- URIutil.linkPMID()



function URIutil.linkURN( attempt, alter, any1, any2, alert, at, alone )
    -- Retrieve bracketed titled external URN link
    -- Precondition:
    --     attempt  -- string, with presumable URN, starting with "urn:"
    --     alter    -- alternative handler
    --     any1     -- intentionally dummy parameter
    --     any2     -- intentionally dummy parameter
    --     alert    -- string, with title of maintenance category, or nil
    --     at       -- fragment, or nil
    --     alone    -- true, if link text not preceded by "urn:"
    -- Postcondition:
    --     Returns
    --         1.    linked ID, or plain string if bad URN
    --         2.    true, if to be preceded by "urn:"
    local r2 = true;
    local r;
    if URIutil.mayURN( attempt ) then
        local e = mw.html.create( "span" )
                         :addClass( "invalid-URN" );
        local serial  = attempt;
        local suffix  = flop( alert );
        if serial:sub( 1, 4 ):lower() == "urn:" then
            serial = serial:sub( 5 );
        end
        e:wikitext( serial );
        r = tostring( e ) .. fault( "(?!?!)" );
        if suffix then
            r = r .. suffix;
        end
    else
        local s = attempt:match( "^%s*[uU][rR][nN]:(%S+)%s*$" );
        if s then
            local space, sign = s:match( "^(%w+):(.+)$" );
            if space then
                local defs = fetch( "urn" );
                if type( defs ) == "table" then
                    local resolver = defs.resolver;
                    space    = space:lower();
                    resolver = resolver[ space ];
                    r2       = ( resolver ~= true );
                    if type( resolver ) == "table" then
                        r, sign = URNresolve( resolver, sign, alter );
                        s = string.format( "%s:%s", space, sign );
                        if r then
                            r = r:gsub( "%$1",  "urn:" .. s );
                            if at then
                                r = string.format( "%s#%s", r, at );
                            end
                            if alone then
                                r2 = true;
                            else
                                s = "urn:" .. s;
                            end
                            r = string.format( "[%s %s]", r, s );
                        end
                    elseif r2 then
                        if type( defs.sns ) == "string" then
                            s = string.format( ":%s:", space );
                            if not defs.sns:find( s, 1, true ) then
                                s = false;
                            end
                        else
                            s = false;
                        end
                        if s then
                            r = string.format( "%s:%s", space, sign );
                        else
                            r = string.format( "%s&#58;%s",
                                               space, sign );
                        end
                        r2 = not alone;
                    else
                        s = "link" .. space:upper();
                        r = URIutil[ s ]( sign, alter, nil, nil, alert );
                    end
                else
                    r =  fault( "Bad structure in Module:URIutil/urn" );
                end
            end
        end
    end
    if not r then
        if alert then
            r = flop( alert )  or  "";
            if attempt then
                r = attempt .. r;
            end
        else
            r = mw.text.trim( attempt );
        end
        r2 = false;
    end
    return r, r2;
end -- URIutil.linkURN()



function URIutil.mayDOI( attempt )
    -- Is this a syntactically correct DOI, or empty?
    -- Precondition:
    --     attempt  -- string with presumable DOI
    -- Postcondition:
    --     Returns  number of organization
    --              0  if empty
    --              false if not empty and not a DOI
    local r;
    if type( attempt ) == "string" then
        local s = mw.text.trim( attempt );
        if #s >= 10 then
            r = URIutil.isDOI( attempt );
        elseif #s == 0 then
            r = 0;
        else
            r = false;
        end
    else
        r = false;
    end
    return r;
end -- URIutil.mayDOI()



function URIutil.mayHandle( attempt )
    -- Is this a meaningful handle, or empty?
    -- Precondition:
    --     attempt  -- string with presumable handle
    -- Postcondition:
    --     Returns  number of organization
    --              0  if empty
    --              false if not empty and not a DOI
    local r;
    if type( attempt ) == "string" then
        local s = mw.text.trim( attempt );
        if #s > 5 then
            r = URIutil.isHandle( attempt );
        elseif #s == 0 then
            r = 0;
        else
            r = false;
        end
    else
        r = false;
    end
    return r;
end -- URIutil.mayHandle()



function URIutil.mayISBN( attempt )
    -- Is this a syntactically correct ISBN, or empty?
    -- Precondition:
    --     attempt  -- string with presumable ISBN
    -- Postcondition:
    --     Returns  10 if 10 digits and hyphens; also X at end of ISBN-10
    --              13 if 13 digits and hyphens; beginning with bookland
    --              0  if empty
    --              false if not empty and not an ISBN
    local r;
    if type( attempt ) == "string" then
        local s = mw.text.trim( attempt );
        if #s >= 10 then
            r = URIutil.isISBN( attempt );
        elseif #s == 0 then
            r = 0;
        else
            r = false;
        end
    else
        r = false;
    end
    return r;
end -- URIutil.mayISBN()



function URIutil.mayISSN( attempt )
    -- Is this a correct ISSN, or empty?
    -- Precondition:
    --     attempt  -- string with presumable ISSN
    -- Postcondition:
    --     Returns  8 if 8 digits and hyphens; also X at end
    --              13 if 13 digits and hyphens; beginning with issnland
    --              0  if empty
    --              false if not empty and not an ISSN
    local r;
    if type( attempt ) == "string" then
        local s = mw.text.trim( attempt );
        if #s >= 8 then
            r = URIutil.isISSNvalid( attempt );
        elseif #s == 0 then
            r = 0;
        else
            r = false;
        end
    else
        r = false;
    end
    return r;
end -- URIutil.mayISSN()



function URIutil.mayLCCN( attempt )
    -- Is this a syntactically correct LCCN?
    -- Precondition:
    --     attempt  -- string with presumable LCCN
    -- Postcondition:
    --     Returns  string with LCCN formatted aa9999-99999999
    --              0  if empty
    --              false if not recognized
    local r;
    if type( attempt ) == "string" then
        local s = mw.text.trim( attempt );
        if  s == "" then
            r = 0;
        else
            r = URIutil.isLCCN( s );
        end
    else
        r = false;
    end
    return r;
end -- URIutil.mayLCCN()



function URIutil.mayURI( attempt, ascii )
    -- Is this a syntactically correct URI, or empty?
    -- Precondition:
    --     attempt  -- string with presumable URI
    --     ascii    -- limit to ASCII (no IRI)
    -- Postcondition:
    --     Returns  false if no problem
    --              string with violation
    local r = URIutil.isEscValid( attempt );
    if not r then
        local s = mw.text.trim( attempt );
        r = s:match( "%s(.+)$" );
        if not r then
            r = s:match( "#(.*)$" );
            if r then
                r = "&#35;" .. r;
            elseif ascii then
                local p = string.format( "[%s].+$",
                                         mw.ustring.char( 128,45,255 ) );
                r = mw.ustring.match( s, p );
            end
        end
    end
    return r;
end -- URIutil.mayURI()



function URIutil.mayURN( attempt )
    -- Is this a syntactically correct URN, or empty?
    -- Precondition:
    --     attempt  -- string with presumable URN, starting with "urn:"
    -- Postcondition:
    --     Returns  false if no problem
    --              string with violation
    local r = URIutil.mayURI( attempt, true );
    if not r then
        local s = attempt:match( "^%s*[uU][rR][nN]:(.+)$" );
        if s then
            local space, id = s:match( "^(%w+):(.+)$" );
            if space then
                r = URNnamespace( space:lower(), id );
            else
                r = s;
            end
        else
            s = mw.text.trim( attempt );
            if s == "" then
                r = false;
            elseif s:match( "^https?://" ) then
                r = "http:";
            else
                r = "urn:";
            end
        end
    end
    return r;
end -- URIutil.mayURN()



function URIutil.plainISBN( attempt )
    -- Format ISBN as digits (and 'X') only string
    -- Precondition:
    --     attempt  -- string with presumable ISBN
    -- Postcondition:
    --     Returns  string with 10 or 13 chars
    --              false if not empty and not an ISBN
    local r;
    local isbn = ISBNfactory( attempt );
    if type( isbn ) == "table" then
        r = ISBNflat( isbn );
    else
        r = false;
    end
    return r;
end -- URIutil.plainISBN()



function URIutil.targetISSN( attempt, allow, any1, any2, alert )
    -- Retrieve bracketed titled external link on ISSN DB without "ISSN"
    -- Precondition:
    --     attempt  -- string with presumable ISSN
    --     allow    -- true: permit invalid check digit
    --     any1     -- intentionally dummy parameter
    --     any2     -- intentionally dummy parameter
    --     alert    -- string with title of maintenance category, or nil
    -- Postcondition:
    --     Returns  link
    local e = mw.html.create( "span" );
    local cnf  = fetch( "config" );
    local issn = ISSNfactory( attempt );
    local lapsus, r;
    if type( issn ) == "table" then
        local lenient;
        if type( allow ) == "string" then
            lenient = ( allow ~= "0" );
        else
            lenient = allow;
        end
        if lenient then
            lapsus = false;
        else
            lapsus = ( not ISSNfaith( issn ) );
        end
        r = ISSNformat( issn, issn.type );
        if type( cnf.issn ) == "string" then
            local s = cnf.issn
            if s:find( "$1", 5, true ) then
                s = s:gsub( "%$1", r );
            else
                s = s .. r;
            end
            r = string.format( "[%s %s]", s, r );
        end
    else
        lapsus = true;
        r      = attempt;
    end
    e:css( "white-space", "nowrap" )
     :wikitext( r );
    if lapsus then
        e:addClass( "invalid-ISSN" );
        r = tostring( e ) .. fault( "(?!?!)" );
        if alert then
            r = r .. flop( alert );
        end
    else
        r = tostring( e );
    end
    return r;
end -- URIutil.targetISSN()



function URIutil.uriDOI( attempt, anything, abbr )
    -- Retrieve linked URI on DOI resolver
    -- Precondition:
    --     attempt   -- string with presumable DOI
    --     anything  -- intentionally dummy parameter
    --     abbr      -- true or string: link doi: abbreviation
    local r = URIutil.linkDOI( attempt );
    if r then
        if abbr then
            local s;
            if type( abbr ) == "string" then
                s = abbr;
            else
                s = "Digital Object Identifier";
            end
            if s ~= fullPageName() then
                 r = string.format( "[[%s|doi]]:%s", s, r );
            else
                r = "doi:" .. r;
            end
        else
            r = "doi:" .. r;
        end
    end
    return r;
end -- URIutil.uriDOI()



function URIutil.uriHandle( attempt, anything, abbr )
    -- Retrieve linked URI on handle resolver
    -- Precondition:
    --     attempt   -- string with presumable handle
    --     anything  -- intentionally dummy parameter
    --     abbr      -- true or string: link hdl: abbreviation
    -- Postcondition:
    --     Returns  combined linked URI, or false
    local r = URIutil.linkHandle( attempt );
    if r then
        local s;
        if type( abbr ) == "string" then
            s = abbr;
        else
            local cnf = fetch( "config" );
            if cnf then
                s = cnf.supportHandle;
                if type( s ) ~= "string"  or
                   s == "-"  or
                   s == ""  or
                   s == fullPageName() then
                    s = false;
                end
            end
        end
        if s then
            r = string.format( "[[%s|hdl]]:%s", s, r );
        else
            r = "hdl:" .. r;
        end
    end
    return r;
end -- URIutil.uriHandle()



function URIutil.uriURN( attempt, anything, alter, alert, at )
    -- Retrieve linked URI on URN resolver
    -- Precondition:
    --     attempt   -- string with presumable URN, starting with "urn:"
    --     anything  -- intentionally dummy parameter
    --     alter     -- string with alternative handler, or nil
    --     alert     -- string with title of maintenance category, or nil
    --     at        -- fragment, or nil
    -- Postcondition:
    --     Returns  link, or plain string if bad URN
    local r, l =
        URIutil.linkURN( attempt, alter, false, false, alert, at, true );
    if l then
        local s = fetch( "config" ).supportURN;
        if s then
            if type( s ) ~= "string"
               or  s == "-"
               or  s == "" then
                s = false;
            end
        else
            s = "Uniform Resource Name";
        end
        if s  and  s ~= fullPageName() then
            r = string.format( "[[%s|urn]]:%s", s, r );
        else
            r = "urn:" .. r;
        end
    end
    return r;
end -- URIutil.uriURN()



Failsafe.failsafe = function ( atleast )
    -- Retrieve versioning and check for compliance
    -- Precondition:
    --     atleast  -- string, with required version
    --                         or wikidata|item|~|@ or false
    -- Postcondition:
    --     Returns  string  -- with queried version/item, also if problem
    --              false   -- if appropriate
    -- 2020-08-17
    local since = atleast
    local last    = ( since == "~" )
    local linked  = ( since == "@" )
    local link    = ( since == "item" )
    local r
    if last  or  link  or  linked  or  since == "wikidata" then
        local item = Failsafe.item
        since = false
        if type( item ) == "number"  and  item > 0 then
            local suited = string.format( "Q%d", item )
            if link then
                r = suited
            else
                local entity = mw.wikibase.getEntity( suited )
                if type( entity ) == "table" then
                    local seek = Failsafe.serialProperty or "P348"
                    local vsn  = entity:formatPropertyValues( seek )
                    if type( vsn ) == "table"  and
                       type( vsn.value ) == "string"  and
                       vsn.value ~= "" then
                        if last  and  vsn.value == Failsafe.serial then
                            r = false
                        elseif linked then
                            if mw.title.getCurrentTitle().prefixedText
                               ==  mw.wikibase.getSitelink( suited ) then
                                r = false
                            else
                                r = suited
                            end
                        else
                            r = vsn.value
                        end
                    end
                end
            end
        end
    end
    if type( r ) == "nil" then
        if not since  or  since <= Failsafe.serial then
            r = Failsafe.serial
        else
            r = false
        end
    end
    return r
end -- Failsafe.failsafe()



local Template = function ( frame, action )
    -- Retrieve library result for template access
    -- Precondition:
    --     frame   -- object
    --     action  -- string; function name
    -- Postcondition:
    --     Returns  appropriate string, or error message (development)
    local lucky, r = pcall( URIutil[ action ],
                            frame.args[ 1 ] or "",
                            frame.args[ 2 ],
                            faculty( frame.args.link, true ),
                            faculty( frame.args.nbsp, true ),
                            frame.args.cat,
                            frame.args.fragment );
    if lucky then
        if r then
            r = tostring( r );
        else
            r = "";
        end
    else
        r = fault( r );
    end
    return r;
end -- Template()



-- Provide template access and expose URIutil table to require()

local p = {};

function p.coreISSN( frame )
    return Template( frame, "coreISSN" );
end
function p.formatISBN( frame )
    return Template( frame, "formatISBN" );
end
function p.formatISSN( frame )
    return Template( frame, "formatISSN" );
end
function p.formatLCCN( frame )
    return Template( frame, "formatLCCN" );
end
function p.isDNBvalid( frame )
    return Template( frame, "isDNBvalid" );
end
function p.isDOI( frame )
    return Template( frame, "isDOI" );
end
function p.isEscValid( frame )
    return Template( frame, "isEscValid" );
end
function p.isGTINvalid( frame )
    return Template( frame, "isGTINvalid" );
end
function p.isHandle( frame )
    return Template( frame, "isHandle" );
end
function p.isISBN( frame )
    return Template( frame, "isISBN" );
end
function p.isISBNvalid( frame )
    return Template( frame, "isISBNvalid" );
end
function p.isISSNvalid( frame )
    return Template( frame, "isISSNvalid" );
end
function p.isLCCN( frame )
    return Template( frame, "isLCCN" );
end
function p.linkDNBopac( frame )
    return Template( frame, "linkDNBopac" );
end
function p.linkDOI( frame )
    return Template( frame, "linkDOI" );
end
function p.linkHandle( frame )
    return Template( frame, "linkHandle" );
end
function p.linkISBN( frame )
    return Template( frame, "linkISBN" );
end
function p.linkISSN( frame )
    return Template( frame, "linkISSN" );
end
function p.linkLCCN( frame )
    return Template( frame, "linkLCCN" );
end
function p.linkPMID( frame )
    return Template( frame, "linkPMID" );
end
function p.linkURN( frame )
    return Template( frame, "linkURN" );
end
function p.mayDOI( frame )
    return Template( frame, "mayDOI" );
end
function p.mayHandle( frame )
    return Template( frame, "mayHandle" );
end
function p.mayISBN( frame )
    return Template( frame, "mayISBN" );
end
function p.mayISSN( frame )
    return Template( frame, "mayISSN" );
end
function p.mayLCCN( frame )
    return Template( frame, "mayLCCN" );
end
function p.mayURI( frame )
    return Template( frame, "mayURI" );
end
function p.mayURN( frame )
    return Template( frame, "mayURN" );
end
function p.plainISBN( frame )
    return Template( frame, "plainISBN" );
end
function p.targetISSN( frame )
    return Template( frame, "targetISSN" );
end
function p.uriDOI( frame )
    return Template( frame, "uriDOI" );
end
function p.uriHandle( frame )
    return Template( frame, "uriHandle" );
end
function p.uriURN( frame )
    return Template( frame, "uriURN" );
end
p.failsafe = function ( frame )
    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()
function p.URIutil( arg )
    local r;
    if arg then
        r = "";
    else
        r = URIutil;
    end
    return r;
end

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

return p;