XRegExp:un constructor Regex de JavaScript extendido

Actualización: Esta versión de XRegExp está desactualizada. Ver XRegExp.com para la última y mejor versión.

Uso expresiones regulares en JavaScript con bastante frecuencia, y aunque el método exec() es genial y me encanta la capacidad de usar una función para generar el reemplazo en el método replace(), las expresiones regulares de JavaScript carecen de algunas características muy importantes disponibles en muchos otros idiomas. . Una de mis mayores molestias es la falta de soporte para s y x banderas, que deberían habilitar el modo "punto coincide con todo" (también conocido como una sola línea) y el modo "espacio libre y comentarios", respectivamente. Estos modificadores están disponibles en casi todas las demás bibliotecas de expresiones regulares modernas.

Para remediar esto, he creado un script (muy pequeño) que extiende JavaScript RegExp constructor, habilitando las banderas antes mencionadas. Básicamente, te da un constructor llamado XRegExp que funciona exactamente como el RegExp constructor excepto que también acepta s y x como banderas, además del ya soportado g (global), i (sin distinción entre mayúsculas y minúsculas) y m (multilínea, es decir, ^ y $ coincidencia en los saltos de línea). Como beneficio adicional, XRegExp también mejora la consistencia de la sintaxis de expresiones regulares entre navegadores.

Así es como funciona:

var regex = new XRegExp("te?", "gi");
var value = "Test".replace(regex, "z"); // value is now "zsz"

¿Parecer familiar? Si ha usado expresiones regulares en JavaScript anteriormente, debería hacerlo:es exactamente como usar RegExp . Sin embargo, hasta ahora no hemos hecho nada que no se pueda lograr usando el RegExp nativo constructor. El s flag se explica por sí mismo (los detalles específicos se pueden encontrar en las preguntas frecuentes, a continuación), así que aquí hay un ejemplo con el x bandera:

// Turn email addresses into links
var email = new XRegExp(
    "\\b                                      " +
    "# Capture the address to $1            \n" +
    "(                                        " +
    "  \\w[-.\\w]*               # username \n" +
    "  @                                      " +
    "  [-.a-z0-9]+\\.(?:com|net) # hostname \n" +
    ")                                        " +
    "\\b                                      ", "gix");

value = value.replace(email, "<a href=\"mailto:$1\">$1</a>");

¡Eso es ciertamente diferente! Un par de notas:

  • Al usar XRegExp , las reglas de escape de cadenas normales (precediendo a los caracteres especiales con "\ ") son necesarios, al igual que con RegExp . Por lo tanto, las tres instancias de \n son metasecuencias dentro del propio literal de cadena. JavaScript los convierte en caracteres de nueva línea (que terminan los comentarios) antes de XRegExp ve la cadena.
  • La expresión regular del correo electrónico es demasiado simplista y solo tiene fines demostrativos.

Eso es bastante ingenioso, pero podemos hacerlo aún más fácil. Si ejecuta la siguiente línea de código:

XRE.overrideNative();

…Como magia, el RegExp el propio constructor soportará el s y x banderas desde ese punto en adelante. La compensación es que ya no podrá acceder a la información sobre la última coincidencia como propiedades del RegExp global objeto. Sin embargo, todas esas propiedades están oficialmente obsoletas de todos modos, y puede acceder a la misma información a través de una combinación de propiedades en instancias de expresiones regulares y el uso de exec() método.

Aquí hay una pregunta frecuente rápida. Para las dos primeras preguntas, tomé prestadas partes de las explicaciones de Mastering Regular Expressions, 3 rd de O'Reilly. Edición .

¿Qué hace exactamente el s bandera hacer?
Por lo general, el punto no coincide con una nueva línea. Sin embargo, un modo en el que el punto coincide con una nueva línea puede ser tan útil como uno en el que el punto no lo hace. El s flag permite seleccionar el modo por expresión regular. Tenga en cuenta que los puntos dentro de las clases de caracteres (p. ej., [.a-z] ) son siempre equivalentes a puntos literales.

En cuanto a qué se considera exactamente un carácter de nueva línea (y, por lo tanto, no coincide con puntos fuera de las clases de caracteres a menos que se use el s bandera), según el Centro de desarrolladores de Mozilla, incluye los cuatro caracteres que coinciden con la siguiente expresión regular:[\n\r\u2028\u2029]
¿Qué hace exactamente la x bandera hacer?
Primero, hace que la mayoría de los espacios en blanco se ignoren, por lo que puede "formatear libremente" la expresión para facilitar la lectura. En segundo lugar, permite comentarios con un # inicial. .

Específicamente, convierte la mayoría de los espacios en blanco en un metacarácter "ignorarme", y # en un metacarácter "ignorarme, y todo lo demás hasta la siguiente nueva línea". No se toman como metacaracteres dentro de una clase de carácter (lo que significa que las clases no formato libre, incluso con x ), y al igual que con otros metacaracteres, puede escapar espacios en blanco y # que quieres que te tomen literalmente. Por supuesto, siempre puedes usar \s para coincidir con espacios en blanco, como en new XRegExp("<a \\s+ href=…>", "x") . Tenga en cuenta que describir los espacios en blanco y los comentarios como metacaracteres de ignorarme no es del todo exacto; sería mejor pensar en ellos como metapersonajes que no hacen nada. Esta distinción es importante con algo como \12 3 , que con el x la bandera se toma como \12 seguido de 3 , y no \123 . Finalmente, no siga inmediatamente un espacio en blanco o un comentario con un cuantificador (por ejemplo, * o ? ), o cuantificará el metacarácter de no hacer nada.

En cuanto a qué es exactamente el espacio en blanco, según el Centro de desarrolladores de Mozilla, es equivalente a todos los caracteres que coinciden con la siguiente expresión regular:
[\t\n\v\f\r \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000]
¿Pueden los s y x ¿Se pueden usar las banderas juntas?
Sí. Puede combinar todas las banderas admitidas (g , i , m , s , x ) en cualquier orden.
¿Qué sintaxis de expresiones regulares admite XRegExp?
Lo que su navegador admita de forma nativa.
¿Mencionó algo sobre la mejora de la consistencia de la sintaxis de expresiones regulares entre navegadores?
Al usar XRegExp , un ] inicial y sin escape dentro de una clase de carácter se considera un carácter literal y, por lo tanto, no finaliza la clase. Esto es consistente con otros motores de expresiones regulares que he usado, y es cierto en Internet Explorer de forma nativa. Sin embargo, en Firefox se pueden experimentar las siguientes peculiaridades (¿errores?):
  • [^] es equivalente a [\S\s] , aunque debería arrojar un error.
  • [^]] es equivalente a [\S\s]] , aunque debería ser equivalente a [^\]] o (?!])[\S\s] .
  • [] es equivalente a (?!) (que nunca coincidirá), aunque debería arrojar un error.
  • []] es equivalente a (?!)] (que nunca coincidirá), aunque debería ser equivalente a [\]] o ] .
Al usar XRegExp (o RegExp con XRE.overrideNative() ), no tienes que preocuparte por cómo los diferentes navegadores manejan esto, como un ] líder dentro de una clase de personaje nunca terminará la clase.
¿Qué métodos relacionados con expresiones regulares admite XRegExp?
Todos ellos.
¿Las expresiones regulares creadas con XRegExp son más lentas de lo que serían de otro modo?
No.
¿Se tarda más en construir expresiones regulares usando XRegExp de lo que sería de otra manera?
Sí, por una pequeña cantidad. Según las pruebas personales, la creación de expresiones regulares con XRegExp suele tardar menos de un milisegundo más de lo que sería de otra manera. Esto es especialmente trivial dado que las expresiones regulares no deberían necesitar construirse dentro de los bucles. En su lugar, se debe asignar una expresión regular a una variable antes de ingresar a un ciclo, para evitar reconstruirla durante cada iteración del ciclo.
¿Con qué navegadores se ha probado?
Firefox 2, Internet Explorer 5.5–7 y Opera 9.
¿Qué tamaño tiene el archivo de script?
Minimizado, ocupa menos de 1 KB. Gzipping lo reduce aún más.
¿Bajo qué licencia se publica?
La Licencia MIT.
¿XregExp afecta los literales de expresiones regulares?
No. Incluso cuando se usa XRE.overrideNative() , literales de expresiones regulares estilo Perl (por ejemplo, /pattern/gi ) no se ven afectados.
Encontré un error. ¿Por qué apestas tanto?
¿Está seguro de que el error está en XRegExp? La sintaxis de las expresiones regulares es algo compleja y, a menudo, cambia su significado según el contexto. Además, las metasecuencias dentro de los literales de cadena de JavaScript pueden cambiar las cosas antes de que XRegExp vea su expresión regular. En cualquier caso, ya sea que esté seguro o no de saber cuál es la causa del problema, no dude en dejar un comentario y lo investigaré lo antes posible.

Aquí está el guión, con comentarios:

/*----------------------------------------------------------------------
XRegExp 0.1, by Steven Levithan <http://stevenlevithan.com>
MIT-style license
------------------------------------------------------------------------
Adds support for the following regular expression features:
  - The "s" flag: Dot matches all (a.k.a, single-line) mode.
  - The "x" flag: Free-spacing and comments mode.

XRegExp also offers consistent, cross-browser handling when "]" is used
as the first character within a character class (e.g., "[]]" or "[^]]").
----------------------------------------------------------------------*/

var XRegExp = function(pattern, flags){
	if(!flags) flags = "";
	
	/* If the "free-spacing and comments" modifier (x) is enabled, replace unescaped whitespace as well as unescaped pound
	signs (#) and any following characters up to and including the next newline character (\n) with "(?:)". Using "(?:)"
	instead of just removing matches altogether prevents, e.g., "\1 0" from becoming "\10" (which has different meanings
	depending on context). None of this applies within character classes, which are unaffected even when they contain
	whitespace or pound signs (which is consistent with pretty much every library except java.util.regex). */
	if(flags.indexOf("x") !== -1){
		pattern = pattern.replace(XRE.re.xMod, function($0, $1, $2){
			// If $2 is an empty string or its first character is "[", return the match unviolated (an effective no-op).
			return (/[^[]/.test($2.charAt(0)) ? $1 + "(?:)" : $0);
		});
	}
	
	/* Two steps (the order is not important):
	
	1. Since a regex literal will be used to return the final regex function/object, replace characters which are not
	   allowed within regex literals (carriage return, line feed) with the metasequences which represent them (\r, \n),
	   accounting for both escaped and unescaped literal characters within pattern. This step is only necessary to support
	   the XRE.overrideNative() method, since otherwise the RegExp constructor could be used to return the final regex.
	
	2. When "]" is the first character within a character class, convert it to "\]", for consistent, cross-browser handling.
	   This is included to workaround the following Firefox quirks (bugs?):
	     - "[^]" is equivalent to "[\S\s]", although it should throw an error.
	     - "[^]]" is equivalent to "[\S\s]]", although it should be equivalent to "[^\]]" or "(?!])[\S\s]".
	     - "[]" is equivalent to "(?!)" (which will never match), although it should throw an error.
	     - "[]]" is equivalent to "(?!)]" (which will never match), although it should be equvialent to "[\]]" or "]".
	   
	   Note that this step is not just an extra feature. It is in fact required in order to maintain correctness without
	   the aid of browser sniffing when constructing the regexes which deal with character classes (XRE.re.chrClass and
	   XRE.re.xMod). They treat a leading "]" within a character class as a non-terminating, literal character. */
	pattern = pattern.replace(XRE.re.badChr, function($0, $1, $2){
			return $1 + $2.replace(/\r/, "\\r").replace(/\n/, "\\n");
		}).
		replace(XRE.re.chrClass, function($0, $1, $2){
			return $1 + $2.replace(/^(\[\^?)]/, "$1\\]");
		});
	
	// If the "dot matches all" modifier (s) is enabled, replace unescaped dots outside of character classes with [\S\s]
	if(flags.indexOf("s") !== -1){
		pattern = pattern.replace(XRE.re.chrClass, function($0, $1, $2){
			return $1.replace(XRE.re.sMod, function($0, $1, $2){
					return $1 + ($2 === "." ? "[\\S\\s]" : "");
				}) + $2;
		});
	}
	
	// Use an evaluated regex literal to return the regular expression, in order to support the XRE.overrideNative() method.
	return eval("/" + pattern + "/" + flags.replace(/[sx]+/g, ""));
},
XRE = {
	overrideNative: function(){
		/* Override the global RegExp constructor/object with the enhanced XRegExp constructor. This precludes accessing
		properties of the last match via the global RegExp object. However, those properties are deprecated as of
		JavaScript 1.5, and the values are available on RegExp instances or via the exec() method. */
		RegExp = XRegExp;
	},
	re: {
		chrClass: /((?:[^[\\]+|\\(?:[\S\s]|$))*)((?:\[\^?]?(?:[^\\\]]+|\\(?:[\S\s]|$))*]?)?)/g,
		xMod: /((?:[^[#\s\\]+|\\(?:[\S\s]|$))*)((?:\[\^?]?(?:[^\\\]]+|\\(?:[\S\s]|$))*]?|\s*#[^\n\r]*[\n\r]?\s*|\s+)?)/g,
		sMod: /((?:[^\\.]+|\\(?:[\S\s]|$))*)(\.?)/g,
		badChr: /((?:[^\\\r\n]+|\\(?:[^\r\n]|$(?!\s)))*)\\?([\r\n]?)/g
	}
};
/* XRE.re is used to cache the more complex regexes so they don't have to be recompiled every time XRegExp runs. Note that
the included regexes match anything (if they didn't, they would have to be rewritten to avoid catastrophic backtracking on
failure). It's the backreferences, as well as where one match ends and the next begins, that's important. Also note that
the regexes are exactly as they are in order to account for all kinds of caveats involved in interpreting and working with
JavaScript regexes. Do not modify them! */

Descárgalo aquí .

Actualización: Esta versión de XRegExp está desactualizada. Ver XRegExp.com para la última y mejor versión.