Module:String

From Commons Wiki
Jump to navigation Jump to search
--- String provides access to basic string functions.
--  This module allows simple text manipulation and is dozens of times
--  more efficient that its parser function counterparts. MediaWiki's
--  native string functions have a upper limit defined by 
--  `wgPFStringLengthLimit` as 1000 characters - this module lacks any
--  such limit.
--  
--  The default category name for errors is [{{fullurl:Category:Errors
--  reported by Module String}} "Errors reported by Module String"].
--  However, this can be overriden if your wiki configures
--  '''Module:String/category''' to return a different category name.
--  On this wiki, the category is [[Module:String/category|configured
--  to Scribunto's general error category]], and this configuration
--  corresponds to "{{int:scribunto-common-error-category}}" on wikis
--  written in your user language.
--  
--  The majority of template functions provided can be invoked with
--  named parameters, unnamed parameters, or a mixture.  If named
--  parameters are used, Mediawiki will automatically remove leading
--  or trailing whitespace from the parameter - see
--  @{str._getParameters}.
--  
--  Template options:
--   * `ignore_errors`: If set to `true` or `1`, any error condition will
--  result in an empty string being returned rather than an error
--  message.
--   * `error_category`: If an error occurs, overrides the name of a
--  category to include with the error message.
--   * `no_category`: If set to `true` or `1`, no category will be added
--  if an error is generated.
--  
--  @module             str
--  @release            stable
--  @author             [[User:Dessamator|Dessamator]]
--  @credit             [[wikipedia:User:Anomie|Anomie]] (Wikipedia)
--  @attribution        [[wikipedia:Module:String|Wikipedia]]
--  @see                [[wikipedia:Module:String/doc|Wikipedia documentation]]
--  @see                [[Module talk:String/testcases|Test suite for module]]
--  
--  <nowiki>

local str = {}

-- begin dev wiki change
local DEFAULT_CATEGORY = select(2, pcall(require, 'Module:String/category'))
if type(DEFAULT_CATEGORY) ~= 'string' then
    DEFAULT_CATEGORY = 'Errors reported by Module String'
end
-- end dev wiki change

--  Module dependencies.
local ustring, text = mw.ustring, mw.text
local entrypoint = require( 'Dev:Entrypoint' )
local yesno = require( 'Dev:Yesno' )

--  Template functions.

--- Computes the length of the target string.
--  @function           str.len
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param[opt]         {string} frame.args.s Target Unicode string.
--  @return             {number} Length of Unicode string.
--  @usage              {{string|len|target string}}
--  @usage              {{string|len|s=target string}}
function str.len( frame )
    local args = str._getParameters( frame.args, {'s'} )
    local s = args['s'] or ''
    return ustring.len( s )
end

--- Extracts a substring of the target string at specified indices.
--  Indexing is **1-based** for the target string to extract from. If
--  either `i` or `j` is a negative value, it is interpreted the same as
--  selecting a character by counting from the end of the string.
--  Hence, a value of `-1` is the same as selecting the last character
--  of the string.
--  
--  If the requested indices are out of range for the given string, an
--  error is reported.
--  
--  @function           str.sub
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.s The string to return a
--                      subset of.
--  @param              {string} frame.args.i The fist index of the substring
--                      to return; default: `1`.
--  @param              {string} frame.args.j: The last index of the string
--                      to return; default: `#s`.
--  @error[99]          {string} 'string subset index out of range'
--  @error[102]         {string} 'string subset indices out of order'
--  @return             Substring from `i`/`1` to `j`/`#s`.
--  @usage              {{string|sub|target string|start index|end index}}
--  @usage              {{string|sub|s= target string |i= start index |j = end index }}
function str.sub( frame )
    local args = str._getParameters( frame.args, { 's', 'i', 'j' } )
    local s = args['s'] or ''
    local i = tonumber( args['i'] ) or 1
    local j = tonumber( args['j'] ) or -1

    local len = ustring.len( s )

    -- Convert negatives for range checking
    if i < 0 then
        i = len + i + 1
    end
    if j < 0 then
        j = len + j + 1
    end

    if i > len or j > len or i < 1 or j < 1 then
        return str._error( 'string subset index out of range', frame )
    end
    if j < i then
        return str._error( 'string subset indices out of order', frame )
    end

    return ustring.sub( s, i, j )
end

--- Implements `{{str sub old}}`.
--  This function is kept in order to maintain these older templates.
--  Indexing is **0-based** for the substring start position.
--  
--  @function           str.sublength
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.s Source string to search.
--  @param              {string} frame.args.i Index to begin output
--                      substring at. Default: `0`.
--  @param              {string} frame.args.len Length of output substring.
--  @return             {string} Substring starting with `i`/`0` of
--                      length `len`.
--  @warning            This function is deprecated in favor of @{str.sub}.
function str.sublength( frame )
    local args = str._getParameters( frame.args, { 's', 'i', 'len' } )
    local i = tonumber( args['i'] ) or 0
    local len = tonumber( args['len'] )
    return ustring.sub( args['s'], i + 1, len and ( i + len ) )
end

--- Extracts a substring matching a pattern from the source string.
--  If `match` or `start` are out of range for the string being queried,
--  then this function generates an error.  An error is also generated if
--  no match is found. If one adds the parameter ignore_errors=true, then
--  the error will be suppressed and an empty string will be returned on
--  any failure.
--  
--  @function           str.match
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.s Target string to search.
--  @param              {string} frame.args.pattern The pattern or string
--                      to find within the string.
--  @param              {string} frame.args.start The index within the
--                      source string to start the search. The first
--                      character of the string has index 1. Default: `1`.
--  @param              {string} frame.args.match In some cases it may be
--                      possible to make multiple matches on a single
--                      string. This specifies which match to return,
--                      where the first match is `match = 1`. If a
--                      negative number is specified then a match is
--                      returned counting from the last match. Hence
--                      `match = -1` is the same as requesting the last
--                      match.  Default: `1`.
--  @param              {string} frame.args.plain A flag indicating that
--                      the pattern should be understood as a literal.
--                      Default: `false`.
--  @param              {string} frame.args.nomatch If no match is found,
--                      output the "nomatch" value rather than an error.
--  @error[192]         {string} 'target string is empty'
--  @error[195]         {string} 'pattern string is empty'
--  @error[198]         {string} 'requested start is out of range'
--  @error[201]         {string} 'match index is out of range'
--  @error[241]         {string} 'match not found'
--  @return             {string} Substring of the source string matching
--                      a pattern or string literal.
--  @usage
--      
--      {{string|match|source string|pattern string|start index|match number|plain flag|nomatch output}}
--      
--  @usage
--      
--      {{string|match
--      | s = source string
--      | pattern = pattern string
--      | start = start index
--      | match = match number
--      | plain = plain flag
--      | nomatch = nomatch output
--      }}
--      
--  @see                [[Lua reference manual/Standard libraries#Patterns|Native patterns]]
--  @see                [[Lua reference manual/Standard libraries#Patterns|Unicode patterns]]
function str.match( frame )
    local args = str._getParameters( frame.args, { 's', 'pattern', 'start', 'match', 'plain', 'nomatch' } )
    local s = args['s'] or ''
    local start = tonumber( args['start'] ) or 1
    local plain_flag = str._getBoolean( args['plain'] or false )
    local pattern = args['pattern'] or ''
    local match_index = math.floor( tonumber( args['match'] ) or 1 )
    local nomatch = args['nomatch']

    if s == '' then
        return str._error( 'target string is empty', frame )
    end
    if pattern == '' then
        return str._error( 'pattern string is empty', frame )
    end
    if math.abs( start ) < 1 or math.abs( start ) > ustring.len( s ) then
        return str._error( 'requested start is out of range', frame )
    end
    if match_index == 0 then
        return str._error( 'match index is out of range', frame )
    end
    if plain_flag then
        pattern = str._escapePattern( pattern )
    end

    local result
    if match_index == 1 then
        -- Find first match is simple case
        result = ustring.match( s, pattern, start )
    else
        if start > 1 then
            s = ustring.sub( s, start )
        end

        local iterator = ustring.gmatch( s, pattern )
        if match_index > 0 then
            -- Forward search
            for w in iterator do
                match_index = match_index - 1
                if match_index == 0 then
                    result = w
                    break
                end
            end
        else
            -- Reverse search
            local result_table = {}
            local count = 1
            for w in iterator do
                result_table[count] = w
                count = count + 1
            end

            result = result_table[ count + match_index ]
        end
    end

    if result == nil then
        return nomatch == nil
            and str._error( 'match not found', frame )
            or  nomatch
    else
        return result
    end
end

--- Returns a single character from the target string.
--  Indexing is **1-based** for the target string position.
--  
--  If one requests a negative value, this function will select a
--  character by counting backwards from the end of the string. In other
--  words, `pos = -1` is the same as asking for the last character.
--  
--  A requested value of zero, or a value greater than the length of the
--  string returns an error.
--  
--  @function           str.pos
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.target The string to search.
--  @param              {string} frame.args.pos The index for the character
--                      to return. Can be negative for reverse indexing.
--  @error[274]         {string} 'string index out of range'
--  @return             {string} Single character at position `pos`.
--  @usage              {{string|pos|target string|index value}}
--  @usage              {{string|pos|target=target string|pos = index value }}
function str.pos( frame )
    local args = str._getParameters( frame.args, { 'target', 'pos' } )
    local target = args['target'] or ''
    local pos = tonumber( args['pos'] ) or 0

    if pos == 0 or math.abs( pos ) > ustring.len( target ) then
        return str._error( 'string index out of range', frame )
    end

    return ustring.sub( target, pos, pos )
end

--- Searches for a target string/pattern in a string.
--  
--  This function should be safe for UTF-8 strings.
--  
--  @function           str.find
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source The string to search.
--  @param              {string} frame.args.target The string or pattern
--                      to find within the `source` string.
--  @param              {string} frame.args.start The index within the source
--                      string to start the search. Default: `1`.
--  @param              {string} frame.args.plain Boolean flag indicating
--                      that target should be understood as a literal and
--                      not as a Lua style regular expression. Default: `true`.
--  @return             First index >= `start`/`1` that `target` is found
--                      within `source`. Indexing is **1-based**. If `target`
--                      is not found, then this function returns 0. If
--                      either "source" or "target" are missing / empty, this
--                      function also returns 0.
--  @usage
--      
--      {{string|find|source string|target string|start index|plain flag}}
--      
--  @usage
--      
--      {{string|find
--      | source = source string
--      | target = target string
--      | start = start index
--      | plain = plain flag
--      }}
--      
function str.find( frame )
    local args = str._getParameters( frame.args, { 'source', 'target', 'start', 'plain' } )
    local source = args['source'] or ''
    local pattern = args['target'] or ''
    local start_pos = tonumber( args['start'] ) or 1
    local plain = args['plain'] or true

    if source == '' or pattern == '' then
        return 0
    end

    plain = str._getBoolean( plain )

    local start = ustring.find( source, pattern, start_pos, plain )

    return start or 0
end

--- Duplicates the behavior of `{{str find}}` including its quirks.
--  This is provided in order to support older templates.
--  
--  @function           str.str_find
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to find a
--                      match in.
--  @param              {string} frame.args.target Search string within `source`.
--  @return             The first index in `source` that is a match to
--                      `target`. Indexing is **1-based**, and the function
--                      returns `-1` if the `target` string is not present
--                      in `source`.
--  @warning            This function is deprecated in favour of @{str.find}.
--  @note               If the "target" string is empty / missing, this
--                      function returns a value of `'1'`, which is
--                      generally unexpected behavior, and must be
--                      accounted for separatetly.
function str.str_find( frame )
    local args = str._getParameters( frame.args, { 'source', 'target' } )
    local source = args['source'] or ''
    local target = args['target'] or ''

    if target == '' then
        return 1
    end

    local start = ustring.find( source, target, 1, true )

    return start or -1
end

--- Determines the presence of a prefix in a string.
--  @function           str.prefix
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to test.
--  @param              {string} frame.args.prefix Suffix to test for.
--  @return             {string} Boolean flag indicating prefix presence.
--  @usage              {{string|prefix|source string|prefix string}}
--  @usage              {{string|prefix|source= source string |prefix = prefix string }}
function str.prefix( frame )
    local args = str._getParameters( frame.args, { 'source', 'prefix' } )
    local source = args['source'] or ''
    local prefix = args['prefix'] or ''
    if prefix == '' then
        return 'yes' -- All strings end with the empty string.
    end
    return ustring.sub( source, 1, ustring.len(prefix) ) == prefix
        and 'yes'
        or  'no'
end

--- Determines the presence of a suffix in a string.
--  @function           str.suffix
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to test.
--  @param              {string} frame.args.suffix Suffix to test for.
--  @return             {string} Boolean flag indicating suffix presence.
--  @usage              {{string|suffix|source string|suffix string}}
--  @usage              {{string|suffix|source= source string |suffix = suffix string }}
function str.suffix( frame )
    local args = str._getParameters( frame.args, { 'source', 'suffix' } )
    local source = args['source'] or ''
    local suffix = args['suffix'] or ''
    if suffix == '' then
        return 'yes' -- All strings end with the empty string.
    end
    return ustring.sub( source, -ustring.len(suffix), -1 ) == suffix
        and 'yes'
        or  'no'
end

--- Counts the number of occurrences of one string in another.
--  @function           str.count
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to count
--                      occurences in.
--  @param              {string} frame.args.pattern Lua pattern or string
--                      to match against.
--  @param[opt]         {string} frame.args.plain Boolean flag indicating
--                      that pattern should be understood as a literal
--                      and not as a Lua style regular expression.
--                      Default: `'true'`.
--  @return             {number} Number of occurences in target string.
--  @usage
--      
--      {{string|count|source string|pattern string|plain flag}}
--      
--  @usage
--      
--      {{string|count
--      | source = source string
--      | pattern = pattern string
--      | plain = plain flag
--      }}
--      
function str.count( frame )
    local args = str._getParameters( frame.args, { 'source', 'pattern', 'plain' } )
    local source = args['source'] or ''
    local pattern = args['pattern'] or ''
    local plain = str._getBoolean( args.plain or 'true' )
    if plain then
        pattern = str._escapePattern( pattern )
    end
    return select( 2, ustring.gsub( source, pattern, '' ) )
end

--- Replaces a target string or pattern within another string.
--  
--  @function           str.replace
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source The string to search.
--  @param              {string} frame.args.pattern The string or pattern
--                      to find within the source.
--  @param              {string} frame.args.replace The replacement text
--  @param              {string} frame.args.count The number of occurences
--                      to replace. Defaults to all occurences.
--  @param              {string} frame.args.plain Boolean flag indicating
--                      that pattern should be understood as a literal
--                      and not as a Lua style regular expression.
--                      Default: `'true'`.
-- @usage
--     
--      {{string|replace|source string|pattern string|replace string|replacement count|plain flag}}
--     
-- @usage
--     
--     {{string|replace
--     | source = source_string
--     | pattern = pattern string
--     | replace = replace string
--     | count = replacement count
--     | plain = plain flag
--     }}
--     
function str.replace( frame )
    local args = str._getParameters( frame.args, { 'source', 'pattern', 'replace', 'count', 'plain' } )
    local source = args['source'] or ''
    local pattern = args['pattern'] or ''
    local replace = args['replace'] or ''
    local count = tonumber( args['count'] )
    local plain = args['plain'] or true

    if source == '' or pattern == '' then
        return source_str
    end
    plain = str._getBoolean( plain )

    if plain then
        pattern = str._escapePattern( pattern )
        --Only need to escape replacement sequences.
        replace = ustring.gsub( replace, "%%", "%%%%" );
    end

    if count ~= nil then
        return ( ustring.gsub( source, pattern, replace, count ) )
    else
        return ( ustring.gsub( source, pattern, replace ) )
    end
end

--- Repeats a string $$\{n \in \N \}$$ times.
--  A simple template pipe for the `string.rep` Lua function.
--  @function           str.rep
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to repeat.
--  @param              {string} frame.args.count Integer for number of
--                      output repetitions.
--  @return             {string} String with repeated copies of `source`.
--  @error[513]         {string} 'function rep expects a number as second
--                      parameter, received $count'
--  @usage              {{string|rep|hello|3}}
--  @usage              {{string|rep|source = hello |count = 3 }}
function str.rep( frame )
    local args = str._getParameters( frame.args, {'source', 'count' } )
    local repetitions = tonumber( args['count'] )
    if not repetitions then
        return str._error( 'function rep expects a number as second parameter, received "' .. ( args['count'] or 'nil' ) .. '"', frame )
    end
    return ustring.rep( args['source'] or '', repetitions )
end

--- Convert string to lowercase Unicode character sequence.
--  @function           str.lc
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to change case.
--  @return             Lowercase UTF-8 string.
--  @usage              {{string|lc|INPUT STRING}}
--  @usage              {{string|lc|source = INPUT STRING }}
function str.lc( frame )
    return ustring.lower( frame.args['source'] or frame.args[1] )
end

--- Convert string to uppercase Unicode character sequence.
--  @function           str.uc
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.str String of indeterminate case.
--  @return             Uppercase UTF-8 string.
--  @usage              {{string|uc|input string}}
--  @usage              {{string|uc|source = input string }}
function str.uc( frame )
    return ustring.upper( frame.args['source'] or frame.args[1] )
end

--- Convert string prefix to lowercase Unicode character.
--  @function           str.lcfirst
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source String of indeterminate case.
--  @return             UTF-8 string with lowercase prefix letter.
--  @usage              {{string|lcfirst|Input string}}
--  @usage              {{string|lcfirst|source = Input string }}
function str.lcfirst( frame )
    return ( ustring.gsub( frame.args['source'] or frame.args[1], '^%u', ustring.lower ) )
end

--- Convert string prefix to uppercase Unicode character.
--  @function           str.ucfirst
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to change case.
--  @return             UTF-8 string with uppercase prefix letter.
--  @usage              {{string|ucfirst|iNPUT STRING}}
--  @usage              {{string|ucfirst|source = iNPUT STRING }}
function str.ucfirst( frame )
    return ( ustring.gsub( frame.args['source'] or frame.args[1], '^%l', ustring.upper ) )
end

--- Pads beginning of a string with a character or whitespace.
--  @function           str.padleft
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.str Source string to pad.
--  @param              {string} frame.args.len Length of output string.
--  @param[opt]         {string} frame.args.char Start padding character.
--                      Default: `' '`.
--  @return             {string} String padded to the left.
--  @usage
--     
--     {{string|padleft|source string|expected length|pad character}}
--     
--  @usage
--     
--     {{string|padleft
--     | source = source string
--     | len = expected length
--     | char = pad character
--     }}
--     
function str.padleft( frame )
    local args = str._getParameters( frame.args, { 'source', 'len', 'char' } )
    local source = args['source'] or ''
    local len = tonumber( args['len'] ) or 0
    local char = ( args['char'] or ' ' ):sub( 1, 1 )
    return char:rep( len - #source ) .. source
end

--- Pads end of a string with a character or whitespace.
--  @function           str.padright
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to pad.
--  @param              {string} frame.args.len Length of output string.
--  @param[opt]         {string} frame.args.char End padding character.
--                      Default: `' '`.
--  @return             {string} String padded to the right.
--  @usage
--     
--     {{string|padright|source string|expected length|pad character}}
--     
--  @usage
--     
--     {{string|padright
--     | source = source string
--     | len = expected length
--     | char = pad character
--     }}
--     
function str.padright( frame )
    local args = str._getParameters( frame.args, { 'source', 'len', 'char' } )
    local source = args['source'] or ''
    local len = tonumber( args['len'] ) or 0
    local char = ( args['char'] or ' ' ):sub( 1, 1 )
    return source .. char:rep( len - #source )
end

--- Return delimited string piece, like PHP's `explode()`.
--  Indexing is **0-based** to match the behavior of `{{#explode}}`
--  parser function.
--  
--  @function           str.explode
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Delimited string to split.
--  @param              {string} frame.args.dlm Symbol or character to split
--                      with.
--  @param[opt]         {string} frame.args.pos Initial piece position.
--                      Default: `0`.
--  @param[opt]         {string} frame.args.lim Maximum number of pieces to
--                      append. Default: `1`.
--  @return             {string} Percent-encoded string.
--  @usage
--     
--     {{string|explode|source string|delimiter|position 0-index|piece limit}}
--     
--  @usage
--     
--     {{string|explode
--     | source = source string
--     | dlm = delimiter
--     | pos = position 0-index
--     | lim = piece limit
--     }}
--     
function str.explode( frame )
    local args = str._getParameters( frame.args, { 'source', 'dlm', 'pos', 'lim' } )
    local source = args['source'] or ''
    local delim = str._escapePattern( args['dlm'] or ' ' )
    local pos = ( tonumber( args['pos'] or '0' ) + 1 )
    local pieces = text.split(source, delim)
    local limit = tonumber( args['lim'] ) or #pieces
    local dividers = {}
    for div in ustring.gmatch(source, delim) do
        table.insert(dividers, div)
    end
    if limit < #pieces then
        for index, value in ipairs(pieces) do
            if index > limit then
                pieces[limit] = pieces[limit] .. dividers[index-1] .. value
            end
        end
        for index, value in ipairs(pieces) do
            if index > limit then pieces[index] = nil end
        end
    end
    if pos < 1 then
        pos = #pieces + pos
    end
    return pieces[pos] or ''
end

--- Percent-encoding for strings.
--  @function           str.urlencode
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to encode.
--  @param              {string} frame.args.code Encoding type (`QUERY`, `PATH`, `WIKI`).
--  @return             {string} Percent-encoded string.
--  @usage              {{string|urlencode|unencoded string}}
--  @usage              {{string|urlencode|source = unencoded string |code = encoding type }}
function str.urlencode( frame )
    local args = str._getParameters( frame.args, { 'source', 'code' } )
    local code = args['code'] or 'QUERY'
    return mw.uri.encode( args['source'], code )
end

--- URL decoding for strings.
--  @function           str.urldecode
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.source Source string to decode.
--  @param              {string} frame.args.code Encoding type (`QUERY`, `PATH`, `WIKI`).
--  @return             {string} Percent-decoded string.
--  @usage              {{string|urldecode|percent-encoded string}}
--  @usage              {{string|urldecode|source = percent-encoded string |code = encoding type }}
function str.urldecode( frame )
    local args = str._getParameters( frame.args, { 'source', 'code' } )
    local code = args['code'] or 'QUERY'
    return mw.uri.decode( args['source'], code )
end

--- Replaces characters in a string with HTML entities.
--  @function           str.htmlencode
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.s Source string to encode.
--  @param              {string} frame.args.charset A string as appropriate to go inside brackets in a Ustring pattern.
--  @return             {string} HTML-encoded string.
--  @usage              {{string|htmlencode|unencoded string}}
--  @usage              {{string|htmlencode|s = unencoded string |code = encoding type }}
function str.htmlencode( frame )
    local args = str._getParameters( frame.args, { 's', 'charset' } )
    return mw.text.encode( args['s'], args['charset'] )
end

--- Replaces HTML entities in the string with the corresponding characters.
--  @function           str.htmldecode
--  @param              {table} frame Invocation frame object.
--  @param              {table} frame.args Invocation/template arguments.
--  @param              {string} frame.args.s Source string to decode.
--  @param              {string} frame.args.decodeNamedEntities If decodeNamedEntities is omitted or false, the only named entities recognized are '&lt;', '&gt;', '&amp;', and '&quot;'. Otherwise, the list of HTML5 named entities to recognize is loaded from PHP's get_html_translation_table function.
--  @return             {string} Decoded string.
--  @usage              {{string|htmldecode|encoded string}}
--  @usage              {{string|htmldecode|s = encoded string |decodeNamedEntities = something }}
function str.htmldecode( frame )
    local args = str._getParameters( frame.args, { 's', 'decodeNamedEntities' } )
    return mw.text.decode( args['s'], args['decodeNamedEntities'] )
end

--- Helper functions.
--  @section            str.utils

--- Populates an argument list with both named/unnamed parameters.
--  This is relevant because named parameters are not identical to unnamed
--  parameters due to string trimming, and when dealing with strings we
--  sometimes want to either preserve or remove that whitespace
--  depending on the application.
--  @function           str._getParameters
--  @param              {table} frame_args Table of sequential and named arguments.
--  @param              {table} arg_list Array of parameter names.
--  @return             {table} Map of named arguments corresponding to `arg_list`.
function str._getParameters( frame_args, arg_list )
    local new_args = {}
    local index = 1
    local value

    for i,arg in ipairs( arg_list ) do
        value = frame_args[arg]
        if value == nil then
            value = frame_args[index]
            index = index + 1
        end
        new_args[arg] = value
    end

    return new_args
end

--- Helper function to handle error messages.
--  @function           str._error
--  @param              {string} exception Error string to display to user.
--  @param              {table} frame Current frame object (from string template or module).
--  @return             {string} Optional error message, with or without categorisation.
function str._error( exception, frame )
    -- begin dev wiki change
    local category = frame.args['error_category'] or DEFAULT_CATEGORY
    -- end dev wiki change
    local silent = frame.args['ignore_errors'] or false
    local anonymous = frame.args['no_category'] or false

    if str._getBoolean( silent ) then
        return ''
    end

    local exception = '<strong class="error">String Module Error: ' .. exception .. '</strong>'
    -- begin dev wiki change
    if type(category) == 'string' and #category > 0 and not str._getBoolean( anonymous ) then
    -- end dev wiki change
        exception = '[[Category:' .. category .. ']]' .. exception
    end

    return exception
end

--- Helper function to interpret boolean strings.
--  @function           str._getBoolean
--  @param              {string} str Boolean-like wikitext string.
--  @return             {boolean} Boolean value corresponding to `str`.
function str._getBoolean( str )
    return yesno( str, true ) or false
end

--- Helper function that escapes all pattern characters.
--  This allows patterns to be treated as plain text.
--  @function           str._escapePattern
--  @param              {string} pattern_str Lua pattern string with special characters.
--  @return             {string} Escaped Lua pattern string for literal string matches.
function str._escapePattern( pattern_str )
    return ustring.gsub( pattern_str, "([%(%)%.%%%+%-%*%?%[%^%$%]])", "%%%1" )
end

--- Wrapper function for string template.
--  @function           str.main
--  @usage              {{string|function|...}}
str.main = entrypoint( str )

return str