Documentación del módulo
Por favor, añade las categorías a la subpágina de documentación.
(subpáginas - enlaces)
--[[  
Este módulo está destinado a proporcionar acceso a las funciones de cadena (string) básicas.
]]

local str = {}

--[[
len

Parametros
    s: La cadena a encontrar su longitud 

]]
function str.len( frame )
    local new_args = str._getParameters( frame.args, {'s'} );
    local s = new_args['s'] or '';
    return mw.ustring.len( s )
end

--[[
sub

Parametros
    s: La cadena donde extraer la subcadena 
    i: La cadena donde extraer la subcadena.
    j: Índice final de la subcadena, por defecto la longitud total, hasta el último carácter.
    
]]
function str.sub( frame )
    local new_args = str._getParameters( frame.args, { 's', 'i', 'j' } );
    local s = new_args['s'] or '';
    local i = tonumber( new_args['i'] ) or 1;
    local j = tonumber( new_args['j'] ) or -1;
    
    local len = mw.ustring.len( s );

    -- Convertir negativos para la comprobación de rango
    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( 'Índice fuera del rango de la cadena' );
    end
    if j < i then
        return str._error( 'Índices de la cadena no ordenados' );
    end
    
    return mw.ustring.sub( s, i, j )
end

--[[
match

Parametros
   s: cadena donde se hace la búsqueda 
   pattern: patrón o cadena a buscar. 
   start: índice de la cadena dónde empezar a buscar, por defecto 1, el primer carácter. 
   match: si se encuentran múltiples coincidencias, especifica cuál de ellas devolver. Por defecto es 1, l
       la primera coincidencia encontrada. Un número negativo cuenta desde el final, por lo tanto 
       match = -1 es la última coincidencia. 
   plain: indica si el patrón debe interpretarse como texto limpio, por defecto 'false'. nomatch: en caso de 
       no encontrar ninguna coincidencia, devuelve el valor de "nomatch" en lugar de un error. 
       Si el número match o el índice start están fuera del rango de la cadena, entonces la función genera un error. 
       También genera un error si no encuentra ninguna coincidencia. 
       Con el parámetro global ignore_errors = true se suprime el    
       error y devuelve una cadena vacía.
]]
function str.match( frame )
    local new_args = str._getParameters( frame.args, {'s', 'pattern', 'start', 'match', 'plain', 'nomatch'} );
    local s = new_args['s'] or '';
    local start = tonumber( new_args['start'] ) or 1;
    local plain_flag = str._getBoolean( new_args['plain'] or false );
    local pattern = new_args['pattern'] or '';
    local match_index = math.floor( tonumber(new_args['match']) or 1 );
    local nomatch = new_args['nomatch'];
    
    if s == '' then
        return str._error( 'La cadena donde buscar está vacía' );
    end
    if pattern == '' then
        return str._error( 'La cadena de búsqueda está vacía ' );
    end
    if math.abs(start) < 1 or math.abs(start) > mw.ustring.len( s ) then
        return str._error( 'Índice d\'inicio fuera del rango de la cadena ' );
    end
    if match_index == 0 then
        return str._error( 'Número de coincidencias fuera de rango' );
    end
    if plain_flag then
        pattern = str._escapePattern( pattern );
    end
    
    local result
    if match_index == 1 then
        -- Encontrar la primera coincidencia es un caso sencillo.
        result = mw.ustring.match( s, pattern, start )
    else
        if start > 1 then
            s = mw.ustring.sub( s, start );
        end
        
        local iterator = mw.ustring.gmatch(s, pattern);
        if match_index > 0 then
            -- Búsqueda hacia adelante
            for w in iterator do
                match_index = match_index - 1;
                if match_index == 0 then
                    result = w;
                    break;
                end
            end    
        else
            -- Invierte búsqueda
            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
        if nomatch == nil then
            return str._error( 'Ninguna coincidencia encontrada' );
        else
            return nomatch;
        end
    else
        return result;
    end
end

--[[
pos

Parámetros
   target: Cadena donde buscar. 
   pos: Índice del carácter a devolver. 
]]
function str.pos( frame )
    local new_args = str._getParameters( frame.args, {'target', 'pos'} );
    local target_str = new_args['target'] or '';
    local pos = tonumber( new_args['pos'] ) or 0;

    if pos == 0 or math.abs(pos) > mw.ustring.len( target_str ) then
        return str._error( 'Índice fuera del rango de la cadena' );
    end    
    
    return mw.ustring.sub( target_str, pos, pos );
end

--[[
find

Parametros
    source: Cadena donde buscar. 
    target: Cadena a buscar o patrón de búsqueda. 
    start: Índice de la cadena fuente donde empezar a buscar, por defecto 1, el primer carácter. 
    plain: Indica si la búsqueda debe interpretarse como texto limpio, de lo contrario como patrón Lua. 
        Por defecto es 'true'.
]]
function str.find( frame )
    local new_args = str._getParameters( frame.args, {'source', 'target', 'start', 'plain' } ); 
    local source_str = new_args['source'] or '';
    local pattern = new_args['target'] or '';
    local start_pos = tonumber(new_args['start']) or 1;
    local plain = new_args['plain'] or true;
        
    if source_str == '' or pattern == '' then
        return 0;
    end    
    
    plain = str._getBoolean( plain );

    local start = mw.ustring.find( source_str, pattern, start_pos, plain )
    if start == nil then
        start = 0
    end
    
    return start
end

--[[
replace

Parámetros
    source: Cadena donde buscar 
    pattern: Cadena de búsqueda o patrón a buscar 
    replace: Texto de reemplazo 
    count: Número de ocurrencias a reemplazar, por defecto todas. 
    plain: Indica si la búsqueda debe interpretarse como texto limpio, de lo contrario como patrón Lua. Por 
        defecto es 'true' 
]]
function str.replace( frame )
    local new_args = str._getParameters( frame.args, {'source', 'pattern', 'replace', 'count', 'plain' } ); 
    local source_str = new_args['source'] or '';
    local pattern = new_args['pattern'] or '';
    local replace = new_args['replace'] or '';
    local count = tonumber( new_args['count'] );
    local plain = new_args['plain'] or true;
        
    if source_str == '' or pattern == '' then
        return source_str;
    end    
    plain = str._getBoolean( plain );

    if plain then
        pattern = str._escapePattern( pattern );
        replace = mw.ustring.gsub( replace, "%%", "%%%%" ); --Sólo es necesario secuencias de escape.
    end
    
    local result;

    if count ~= nil then
        result = mw.ustring.gsub( source_str, pattern, replace, count );
    else
        result = mw.ustring.gsub( source_str, pattern, replace );
    end        

    return result;
end

function str.mayuscula(frame) -- Convierte en mayúsculas la primera letra que aparece en la edición de una cadena
    local s = frame.args[1] or '';
    if s  ~= '' then        
        local cambio = {};
        local modo = {};
        if string.find(s, '|') ~= nil then -- Enlaces con etiqueta
            modo = string.upper(string.match(s,'(|%a)'));
            cambio = string.gsub(s,'|%a', modo,1);
        elseif string.find(s, '[[]') ~= nil then -- Enlaces sin etiqueta
            modo = string.upper(string.match(s,'^(..%a)'));
            cambio = string.gsub(s,'^..%a', modo,1);
        elseif string.match(s,'^%a') ~= nil then -- Sin enlace
           modo = string.upper(string.match(s,'^(%a)'));
           cambio = string.gsub(s,'^%a', modo, 1);
        else
           cambio = s;
        end
        return cambio;
    end
end

--[[
Función de ayuda que rellena la lista de argumentos, para que el usuario pueda utilizar una combinación de
 parámetros con nombre y sin nombre. Esto es importante porque los parámetros con nombre no funcionan igual 
 que los parámetros sin nombre cuando se encadenan recortes, y cuando se trata de cadenas
 a veces se debe conservar o quitar espacios en blanco dependiendo de la aplicación. 
]]
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        

--[[
Función de ayuda para controlar los mensajes de error.
]]
function str._error( error_str )
    local frame = mw.getCurrentFrame();
    local error_category = frame.args.error_category or 'Errores detectados por el módulo String';
    local ignore_errors = frame.args.ignore_errors or false;
    local no_category = frame.args.no_category or false;
    
    if str._getBoolean(ignore_errors) then
        return '';
    end
    
    local error_str = '<strong class="error">Error del módulo String: ' .. error_str .. '</strong>';
    if error_category ~= '' and not str._getBoolean( no_category ) then
        error_str = '[[Categoría:Wikisource:' .. error_category .. ']]' .. error_str;
    end        
    
    return error_str;
end

--[[
Función de ayuda para interpretar cadenas booleanas.
]]
function str._getBoolean( boolean_str )
    local boolean_value;
    
    if type( boolean_str ) == 'string' then
        boolean_str = boolean_str:lower();
        if boolean_str == 'false' or boolean_str == 'no' or boolean_str == '0' 
                or boolean_str == '' then
            boolean_value = false;
        else
            boolean_value = true;
        end    
    elseif type( boolean_str ) == 'boolean' then
        boolean_value = boolean_str;
    else
        error( 'Ningún valor booleano encontrado' );
    end    
    return boolean_value
end

--[[
Función de ayuda que escapa a todos los caracteres de patrón para que puedan ser tratados
como texto sin formato.
]]
function str._escapePattern( pattern_str )
    return mw.ustring.gsub( pattern_str, "([%(%)%.%%%+%-%*%?%[%^%$%]])", "%%%1" );
end

-- Selección del caracter de una página que sea válido para la categoría ES-x (generalmente el primero)
function str.CaracterParaOrdenar(frame)
    local frase
    if type(frame) == 'string' then
    	frase = frame
    else
    	frase = frame.args[1]
    end
    local PrimerCaracter = ''
	local CaracterEspecial = {
		["¿"] = true,
		["?"] = true,
		["!"] = true,
		[" "] = true,
		["."] = true,
		["¡"] = true, 
		["​"] = true, --bizarro error donde el primer caracter se toma como uno en blanco
		["("] = true,
		["«"] = true }
    
    PrimerCaracter = mw.ustring.sub( frase , 1 , 1)
    while (CaracterEspecial[PrimerCaracter]) do --bizarro error donde el primer caracter se toma como uno en blanco
        frase = mw.ustring.sub( frase , 2 ) --Segundo caracter
    	PrimerCaracter = mw.ustring.sub( frase , 1 , 1)
    end
    
    if (PrimerCaracter == "Á") then       PrimerCaracter = 'A'
    elseif (PrimerCaracter == "É") then   PrimerCaracter = 'E'
    elseif (PrimerCaracter == "Í") then   PrimerCaracter = 'I'
    elseif (PrimerCaracter == "Ó") then   PrimerCaracter = 'O'
    elseif (PrimerCaracter == "Ú") then   PrimerCaracter = 'U'
    else
    end

    return PrimerCaracter 

end


return str