Crear regex con JavaScript y SuperExpressive

Crear regular expressions o regex con JavaScript y SuperExpressive, una librería que permite crear regex con lenguaje natural.

https://github.com/francisrstokes/super-expressive 🦜 Super Expressive is a zero-dependency JavaScript library for building regular expressions in (almost) natural language.”

En la segunda parte de éste post, he creado un tutorial práctico para ver cómo implementar la librería. Haz click en la siguiente imagen para ver el resultado final en una ventana nueva:

Ejercicio con SuperExpressive
Ejercicio con SuperExpressive, haz click sobre la imagen para ver el ejercicio terminado.

¿Quieres aprender a hacer este ejercicio? Saltar directamente al tutorial

Existe una segunda parte de este exercicio, donde implementamos un medidor de fuerza para el campo contraseña. Si quieres saltar a esa seguna parte te dejo el enlace a continuación:

Crear medidor de fuerza para contraseñas con JavaScript y CheckPasswordStrength

Regex la herramienta para detectar patrones

En ocasiones, es necesario detectar patrones en determinados textos de aplicaciones web.

Algunos de los patrones comunes a detectar son formatos de email, urls, números de teléfono o fechas. Y precisamente para detectar formatos de fechas como inputs, la librería DayJs utiliza regex. Puedes aprender más sobre DayJs en este vistazo que hicimos.

¿Cómo manejar datetimes y fechas con DayJs?

Por poner un ejemplo, supongamos que tenemos que crear un formulario de registro. En él se le pide al usuario su correo electrónico a través de un campo de texto.

Sin embargo, antes de que se envíe el formulario, debemos asegurarnos de que el correo electrónico introducido tiene un formato correcto.

HTML5 incluye inputs de tipo email, pero para este ejemplo, vamos a obviar esto.

Las expresiones regulares, o «regex«, son una herramienta excelente para estos casos.

Mediante una serie de instrucciones, se definen restricciones como el orden, la cantidad o el tipo de carácteres que debe contener un «string».

Sin embargo, la forma como lo hace, puede resultar extremadamente críptica, incluso para programadores expertos.

/^((?:\*|[0-5]?[0-9](?:(?:-[0-5]?[0-9])|(?:,[0-5]?[0-9])+)?)(?:\/[0-9]+)?)\s+((?:\*|(?:1?[0-9]|2[0-3])(?:(?:-(?:1?[0-9]|2[0-3]))|(?:,(?:1?[0-9]|2[0-3]))+)?)(?:\/[0-9]+)?)\s+((?:\*|(?:[1-9]|[1-2][0-9]|3[0-1])(?:(?:-(?:[1-9]|[1-2][0-9]|3[0-1]))|(?:,(?:[1-9]|[1-2][0-9]|3[0-1]))+)?)(?:\/[0-9]+)?)\s+((?:\*|(?:[1-9]|1[0-2])(?:(?:-(?:[1-9]|1[0-2]))|(?:,(?:[1-9]|1[0-2]))+)?)(?:\/[0-9]+)?)\s+((?:\*|[0-7](?:-[0-7]|(?:,[0-7])+)?)(?:\/[0-9]+)?)$/gm

Viendo ejemplos como este, cualquiera diría que el gato ha estado paseándose por el teclado.

¿Regex o gato paseando por el teclado?

Si te interesa aprender más sobre regex, al final del artículo te dejo enlaces a más recursos. 

Sin embargo, la librería Super Expressive, fue creada para facilitar la vida, a la hora de crear “regex”.

Crear regex con lenguaje natural con JavaScript y SuperExpressive

El objetivo de Super Expressive, es ofrecer al desarrollador una API, a través del cual, pueda construir expresiones regulares, con un lenguaje (casi) natural.

Mediante la concatenación de métodos, se pueden crear instrucciones que parecen más o menos a frases escritas en inglés. En última instancia, Super Expressive, devolverá la expresión regular a partir de dichas instrucciones.

Por poner un ejemplo, te propongo tratar de descubrir el patrón que detecta la siguiente instancia de Super Expressive.

const myRegex = SuperExpressive().startOfInput.optional.string('0x').capture.exactly(4).anyOf.range('A', 'F').range('a', 'f').range('0', '9').end().end().endOfInput.toRegex();

¿Lo has resuelto?

Efectivamente, estas instrucciones reconocen y capturan números en hexadecimales de 16-bits, como por ejemplo “0xC0D3”.

La expresión regular que genera es la siguiente:

/^(?:0x)?([A-Fa-f0-9]{4})$/

Super Expressive ocupa menos de 4kb, y se puede instalar a través del comando “npm i super-expressive”. Se trata de una herramienta ampliamente usada, ya que está valorada con más de 4.4K estrellas. Actualmente tiene unas 1607 descargas semanales en NPM.

Tal y como hemos visto en el ejemplo, trabajar con Super Expressive es relativamente sencillo. El primer paso es ejecutar la función “SuperExpressive”, e inmediatamente enlazar métodos y propiedades que describen el patrón.

Como siempre, la mejor forma de ver en detalle las opciones que ofrece la librería, es a través de su documentación. Sin embargo, aquí solo veremos algunos de los métodos más interesantes. Y, por supuesto, los implementaremos en un sencillo ejercicio.

Métodos y propiedades descriptivas

La API de SuperExpressive expone métodos y propiedades para todas las reglas válidas de regex. Empezaremos viendo cómo se indican los “flags”. Los flags, son reglas genéricas que se incluyen al final del regex en forma de letras. Estas reglas se aplican al conjunto de carácteres del texto.

Entre los distintos flags existen algunos como “g”, el cual sirve para encontrar más de una coincidencia con el patrón en la cadena de texto. SuperExpressive, permite incluir este flag llamando a la propiedad “allowMultipleMatches”.

Otro exemplo de flag es “i”, que se puede activar llamando a la propiedad “caseInsensitive”. Ésta, sirve para que no se tenga la diferencia entre mayúsculas y minúsculas.

El resto de propiedades para flags son “lineByLine”, “sticky”, “unicode” y “singleLine”. Como se puede ver, esta librería trata de ser lo más descriptiva posible.

Una vez decididos los flags, pasamos a definir el patrón. Para ello se puede especificar que se desea hacerlo desde el inicio de la cadena de texto. Para este propósito existe la propiedad “startOfInput”.

Seguidamente se irán concatenando métodos y propiedades como por ejemplo, “digit”, para definir un valor numérico. O “word”, si se trata de un carácter alfanumérico. E incluso “anyChar”, si se desea permitir cualquier carácter.

Si en cambio, lo que se busca es encontrar un carácter dentro de un rango de posibilidades, también existe el método “range()”.

Adicionalmente, la librería también permite encapsular grupos de reglas mediante propiedades como “anyOf”. Gracias a esta propiedad, se puede especificar se cumpla cualquier regla declarada dentro de un conjunto. La propiedad “anyOf” debe acabar ejecutando el método “end()” para indicar hasta dónde llega el conjunto de reglas.

Otro método destacable es “between(x,y)”. Tal como su nombre sugiere, permite declarar que el siguiente elemento definido, se cumpla entre “x” y “y” veces.

A pesar de que existen muchas otras instrucciones, con esta pequeña muestra, es fácil hacerse a la idea de la versatilidad de la API, a la hora de construir patrones regex.

Finalmente, se puede obtener la expresión regular ejecutando el método “toRegex()”.

Supervisa el formato de los campos con SuperExpressive

Seguidamente implementaremos un ejercicio donde aparecerán tres “regex” construidos con SuperExpressive.

En este ejercicio crearemos un formulario de registro, donde el usuario podrá introducir un nombre de usuario, un correo electrónico y una contraseña.

Cada uno de estos campos deberá cumplir un conjunto de restricciones. Por supuesto, comprobaremos que se cumplan dichas restricciones a través de expresiones regulares creadas con SuperExpressive.

Empezaremos creando la estructura base en HTML. Esta se compone de un gran formulario. Este formulario contendrá tres campos de texto, y cada uno irá acompañado de un elemento “label”. Así como de un listado con información del patrón que debe cumplir, y un contenedor con un mensaje de error. Ese mensaje solo lo mostraremos en caso de que se cumpla la expresión regular.

    <form class="main-form">
      <img src="imgs/blogging.svg" alt="" />
      <label for="username">Nombre de usuario</label>
      <input id="username" name="username" type="text" />
      <ul>
        <li>
          Debe tener entre 3 y 5 carácteres
        </li>
        <li>
          No puede tener mayúsculas
        </li>
        <li>
          Solo puede tener letras, números o los carácteres "_" y "-"
        </li>
      </ul>
      <div class="error-messages error-username">El nombre de usuario no tiene
        el formato adecuado</div>
      <label for="email">Email</label>
      <input id="email" name="email" type="text" />
      <ul>
        <li>
          Debe tener un formato de email válido
        </li>
      </ul>
      <div class="error-messages error-email">El correo electrónico no tiene el
        formato adecuado</div>
      <label for="password">Contraseña</label>
      <input id="password" name="password" type="password" />
      <ul>
        <li>
          Al menos 8 carácteres.
        </li>
        <li>
          Debe contener al menos una letra en mayúscula, una en minúscula, y un
          número.
        </li>
        <li>
          Puede contener carácteres especiales.
        </li>
      </ul>
      <div class="error-messages error-password">El password tiene el formato
        válido</div>
      <div>
        <button type="submit" class="btn">ENVIAR</button>
      </div>
    </form>

Cerraremos el formulario con un botón de envío.

Seguidamente, daremos un poco de estilos al formulario con el siguiente código SCSS. Es importante destacar que los mensajes de error deben tener un estado de “display:none”, por defecto.

@import url("https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,300;0,400;0,700;1,300&display=swap");
$mainColor: #7655cb;
$secondColor: #ec615a;
* {
  box-sizing: border-box;
  &:before,
  &:after {
    box-sizing: border-box;
  }
}
body {
  margin: 0;
  padding: 0;
  background-color: rgb(252, 252, 252);
  font-family: "Lato", sans-serif;
  font-weight: 300;
  background-color: #f8f5fe;
}
ul {
  list-style: none;
  margin: 0 0 20px 0;
  padding: 0 0 20px 0;
  color: rgb(131, 131, 131);
  border-bottom: solid 1px rgb(220, 220, 220);
  font-style: italic;
}
form.main-form {
  max-width: 600px;
  padding: 10px;
  margin: 20px auto 50px auto;
  label {
    font-weight: 900;
    color: $mainColor;
    font-size: 0.9rem;
    display: block;
    margin-bottom: 10px;
  }
  input {
    display: block;
    margin: 0 0 20px 0;
    width: 100%;
    height: auto;
    padding: 10px;
    border: 2px solid $mainColor;
    border-radius: 3px;
    box-shadow: 0 3px 1px rgba(0, 0, 0, 0.2);
  }
}
.btn {
  background-color: $mainColor;
  color: #fff;
  padding: 20px 40px;
  border-radius: 3px;
  border: none;
  display: block;
  margin: 0 auto;
  width: auto;
  cursor: pointer;
  font-weight: 700;
  box-shadow: 0 3px 1px rgba(0, 0, 0, 0.2);
}
.error-messages {
  padding: 20px;
  margin: 20px 0 20px 0;
  border-radius: 5px;
  background-color: $secondColor;
  color: #590a06;
  box-shadow: 0 3px 1px rgba(0, 0, 0, 0.2);
  display: none;
  &.show {
    display: block;
  }
}

A continuación crearemos el script con JS con el que controlaremos el comportamiento.

Iniciaremos el script importando librerías JavaScript y estilos CSS. Además de SuperExpressive, también importaremos la librería canvas-confetti.

import SuperExpressive from "super-expressive";
import confetti from "canvas-confetti";
import "./SCSS/index.scss";

Utilizaremos Canvas Confetti para lanzar un efecto de confeti cuando el usuario pueda mandar el formulario sin errores.

En su momento ya vimos cómo implementar esta librería, si te interesa revisarlo, a continuación tienes un enlace para verla más en detalle.

Crear efecto confeti con JavaScript y CanvasConfetti

Siguiendo con nuestro ejercicio, guardaremos una referencia al formulario en una variable llamada form.

const form = document.querySelector(".main-form");

El primer regex que vamos a crear va a ser el del campo del nombre de usuario. Este debe contener entre 3 y 5 carácteres. Estos carácteres pueden ser cualquiera de los definidos a continuación:

Una letra minúscula entre la “a” y la “z”, un número entre el 0 y el 9, el carácter “_” o el carácter “-”.

const userNameRegex = SuperExpressive()
  .startOfInput.between(3, 5)
  .anyOf.range("a", "z")
  .range("0", "9")
  .string("_")
  .string("-")
  .end()
  .endOfInput.toRegex();

La segunda expresión regular, es más compleja. No servirá para comprobar el formato del correo electrónico, definiendo qué carácteres son válidos antes y después de la “@”.

//^(?:[A-Za-z0-9][\._]{0,1})+[A-Za-z0-9]@(?:[A-Za-z0-9])+(?:\.{0,1}[A-Za-z0-9]){2}\.[a-z]{2,3}$/
const emailRegex = SuperExpressive()
  .startOfInput.oneOrMore.group.anyOf.range("A", "Z")
  .range("a", "z")
  .range("0", "9")
  .end() //end anyOf
  .between(0, 1)
  .anyOf.string(".")
  .string("_")
  .end() //end anyOf
  .end() //end group
  .anyOf.range("A", "Z")
  .range("a", "z")
  .range("0", "9")
  .end()
  .string("@")
  .oneOrMore.group.anyOf.range("A", "Z")
  .range("a", "z")
  .range("0", "9")
  .end() // end anyOf
  .end() // end group
  .exactly(2)
  .group.between(0, 1)
  .string(".")
  .anyOf.range("A", "Z")
  .range("a", "z")
  .range("0", "9")
  .end()
  .end()
  .string(".")
  .between(2, 3)
  .range("a", "z")
  .endOfInput.toRegex();

Por último, construimos un regex para forzar una contraseña robusta. Para ello, nos aseguramos que el usuario escriba al menos 8 carácteres. De estos 8, al menos debe contener un número y una mayúscula.

//^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$
const paswordRegex = SuperExpressive()
  .startOfInput.assertAhead.zeroOrMore.anyChar.digit.end()
  .assertAhead.zeroOrMore.anyChar.range("a", "z")
  .end()
  .assertAhead.zeroOrMore.anyChar.range("A", "Z")
  .end()
  .assertAhead.zeroOrMore.anyChar.anyOf.range("a", "z")
  .range("A", "Z")
  .end()
  .end()
  .atLeast(8)
  .anyChar.endOfInput.toRegex();

Guardamos las expresiones regulares en un objeto con los nombres de los campos.

const allRegex = {
  username: userNameRegex,
  email: emailRegex,
  password: paswordRegex,
};

Declaramos la función “checkInputByName” para comprobar dinámicamente si un campo cumple con su expresión. Para ello le pasamos el nombre del campo y el valor. En esta misma función actualizamos la visualización del mensaje de error si es preciso.

const checkInputByName = (name, value) => {
  const errorContainet = document.querySelector(`.error-${name}`);
  errorContainet.classList.remove("show");
  const passed = allRegex[name].test(value);
  if (!passed) {
    errorContainet.classList.add("show");
  }
  return passed;
};

Seleccionamos y recorremos todos los campos para asignar la escucha del evento “keyup”. En el callback de este evento, llamamos a la función “checkInputByName” pasándole el nombre y el valor del campo.

document.querySelectorAll("input").forEach((input) => {
  input.addEventListener("keyup", (e) => {
    const inputName = e.currentTarget.name;
    checkInputByName(inputName, e.currentTarget.value);
  });
});

Finalmente, capturamos el evento “submit” del formulario. En la función de “callback”, detenemos el comportamiento por defecto y comprobamos de nuevo todos los campos con “checkInputByName”. Si todos los campos han sido validados, ejecutamos “confetti”.

form.addEventListener("submit", (e) => {
  e.preventDefault();
  let passed = true;
  document.querySelectorAll("input").forEach((input) => {
    passed = checkInputByName(input.name, input.value) && passed;
  });
  if (!passed) {
    return;
  }
  confetti({
    colors: ["#7655cb", "#ec615a"],
    origin: { x: 0 },
    spread: 55,
    angle: 60,
  });
  confetti({
    colors: ["#7655cb", "#ec615a"],
    origin: { x: 1 },
    spread: 55,
    angle: 120,
  });
});

Si quieres ampliar este formulario con un medidor de contraseñas, a continuación te dejo el enlace a la segunda parte de este ejerecicio:

Crear medidor de fuerza para contraseñas con JavaScript y CheckPasswordStrength

Aprender RegEx sigue siendo necesario

A pesar de que SuperExpressive genera una capa de abstracción más inteligible, definir expresiones regulares sigue siendo algo complejo.

Crear regex con JavaScript y SuperExpressive es posible, pero sigue siendo necesario conocer las reglas a bajo nivel. A pesar de que SuperExpressive, puede ser una gran herramienta, creo que sigue siendo necesario entender regex de base, para poderla utilizar correctamente. 

Este ha sido un pequeño vistazo a la librería SuperExpressive. Como siempre, encontrarás el ejercicio subido al repositorio libreriasjs y algunos recursos adicionales a continuación:

Nos vemos pronto, un abrazo desarrolladores!

Deja un comentario