EWW, el navegador de Emacs que subestimas

Read in English

La primera vez que abrí una página web dentro de Emacs pensé que era un truco, un parche, un juguete... al estudiarlo a fondo vi que me equivocaba. EWW es un navegador escrito 100% en Emacs Lisp, sin ningún motor externo detrás (nada de WebKit, Blink o Gecko). Es ligero hasta lo absurdo, está inteligentemente diseñado y, casi sin querer, te regala una plataforma para hacer scraping o automatizar tareas web con unas pocas líneas. Una herramienta con un potencial enorme que muchos desconocen, incluso dentro de la propia comunidad Emacs.

Y lo mejor es que ya lo tienes instalado. Viene por defecto con Emacs. Para lanzarlo basta con M-x eww y escribir una URL o un término de búsqueda (se abrirá DuckDuckGo).

¿Es un sustituto de Chrome o Firefox? No, ni lo pretende. Juega en otra liga.

Dos piezas: EWW y SHR

Lo que llamamos "el navegador de Emacs" son en realidad dos piezas que trabajan juntas.

  • EWW (eww.el) es la capa de navegador: URLs, historial, marcadores, formularios, cookies, descargas y sesiones.
  • SHR, o Simple HTML Renderer (shr.el), es el motor que convierte el HTML en texto dentro de un buffer. Y no lo usa solo EWW: también lo comparten Gnus para el correo, elfeed para los feeds, y unos cuantos paquetes más.

Aquí está la clave: SHR no dibuja una página, la traduce. Coge el HTML y lo pinta como texto de Emacs, con sus faces y sus propiedades. ¿Qué entiende por el camino? Bastante más de lo que imaginas:

  • Texto enriquecido: b, i, em, strong, u, s, code, tt, mark, ins, del, sup, sub, abbr, bdo/bdi.
  • Estructura: h1..h6, p, div, blockquote, pre, hr, ul/ol/li, dl/dt/dd.
  • Enlaces y tablas.
  • Imágenes: entiende data: URIs (base64), srcset (elige la resolución), cid: (correo), escalado con shr-max-image-proportion, animación y zoom. Y si un src está roto, cae a su texto alt, como debe ser.
  • MathML: se queda con la anotación TeX, no renderiza la fórmula.

Los formularios son un caso curioso: no los pone SHR, los añade EWW por encima vía shr-external-rendering-functions. Gracias a eso puedes enviar formularios GET y POST, e incluso multipart/form-data para subir ficheros.

En la lista nos faltan 2 detalles: JavaScript y CSS.

Lo que EWW no hace (y por qué está bien)

EWW no está pensado para ejecutar aplicaciones web modernas. Sus limitaciones no son un descuido, son la razón de que sea tan rápido y tan ligero. Pero conviene que las tengas claras antes de frustrarte.

Sin JavaScript. Esto descarta de un plumazo cualquier SPA (React, Vue, Angular), el infinite scroll, el contenido que llega por fetch o XHR y la mayoría de la web actual. Si una página necesita JS para pintarse, en EWW verás poco o nada.

Sin CSS. El propio código lo confiesa en su cabecera: "It does not do CSS, JavaScript or anything advanced". En la práctica:

  • Las hojas <style> y los <link rel=stylesheet> se ignoran por completo. Solo se lee el atributo style inline, y solo si contiene color, display (en concreto none) o border-collapse. Todo lo demás (font-size, margin, padding, float, flex, grid, text-align...) se tira a la basura.
  • Los colores exigen (display-color-cells) >= 88. Y el sistema de contraste es sorprendentemente serio: convierte a CIE Lab y usa distancia CIE DE2000 para garantizar que el texto se lea.
  • No hay selectores por clase o id, ni cascada, ni especificidad. Nada.

El parser no es HTML5-conforme. Usa libxml2, que es tolerante pero no sigue el algoritmo de parseo de HTML5 al pie de la letra. Se aplican parches manuales para tapar los agujeros.

EWW no es para SPAs, banca online, paneles con JS, formularios dinámicos, cualquier cosa que te suelte un "activa JavaScript para continuar", vídeo o audio embebido, o layouts que solo son legibles gracias al CSS. No es su terreno, y forzarlo es perder el tiempo.

Scraping de serie

EWW se apoya en libxml-parse-html-region, que te devuelve el DOM como una S-expression. Y Emacs incluye dom.el para recorrerlo. Eso hace que hacer scraping es trivial y ni siquiera necesitas abrir EWW.

Mira este script. Extrae los titulares de la portada de Hacker News:

(require 'dom)
(require 'url)
(require 'cl-lib)

(defun demo-scrape (url)
  "Descarga URL y devuelve los titulares de Hacker News como lista de conses.
Cada elemento es (TITULO . HREF)."
  (with-current-buffer (url-retrieve-synchronously url t t 30)
    (goto-char (point-min))
    ;; Saltar cabeceras HTTP hasta la primera linea en blanco.
    (re-search-forward "\r?\n\r?\n" nil t)
    (let* ((dom (libxml-parse-html-region (point) (point-max)))
           ;; En HN cada titular es <span class="titleline"><a>...</a>.
           (titles (dom-by-class dom "titleline")))
      (mapcar (lambda (node)
                (let ((a (dom-child-by-tag node 'a)))
                  (cons (string-trim (dom-texts a))   ; texto del enlace
                        (dom-attr a 'href))))          ; destino
              titles))))

(defun demo-scrape-hn ()
  "Descarga la portada de Hacker News y muestra los titulares en un buffer."
  (interactive)
  (let ((items (demo-scrape "https://news.ycombinator.com/")))
    (with-output-to-temp-buffer "*HN titulares*"
      (princ (format "Titulares encontrados: %d\n\n" (length items)))
      (cl-loop for (title . href) in items
               for i from 1
               do (princ (format "%2d. %s\n    %s\n\n" i title href))))))

;; Al evaluar el buffer (M-x eval-buffer) se ejecuta directamente:
(demo-scrape-hn)

Evalúalo con M-x eval-buffer y aparecerá un buffer temporal con la lista de titulares y sus enlaces. Sin librerías externas ni instalar nada.

Esto es posible porque dom.el te da un puñado de funciones que hacen el trabajo pesado: dom-by-tag, dom-by-class, dom-by-id, dom-child-by-tag, dom-attr, dom-text y dom-texts. Y si en vez del HTML crudo quieres el texto ya renderizado (por ejemplo, para indexar la versión "legible" de una página), puedes pasar el DOM por shr-insert-document en un buffer temporal y quedarte con buffer-string. Esta es, literalmente, la base sobre la que se construyen elfeed o mu4e.

Diseñar "para EWW" es diseñar bien

Voy a cambiar de perspectiva. Hasta ahora hemos hablado de EWW como lector. Pero ¿y si eres tú quien publica? ¿Y si quieres que tu web se vea impecable ahí dentro?

La buena noticia es que diseñar para EWW no es aprender un dialecto raro. Es volver a los principios del HTML semántico, esos que nunca deberían haberse abandonado. EWW renderiza el HTML como un documento estructurado, no como un lienzo pintado con CSS. Sigue estas ideas y, de paso, tu web será más accesible en todas partes.

El orden del DOM es el orden en pantalla

SHR recorre el árbol en orden de documento e inserta el texto tal cual lo va encontrando. No hay reordenación por CSS: float, flex, grid, order y position no existen. Lo que pongas primero en el HTML, aparece primero.

Así que coloca el contenido principal lo antes posible en el DOM, o al menos nada más abrir el <body>. Los <nav> largos y los pies de página, al final.

Marca la estructura con etiquetas, no con estilos

SHR da faces propias a h1..h6, b/strong, i/em, u, code, pre, blockquote, listas, mark y del/ins. Úsalas por lo que significan, no por cómo se ven:

  • Encabezados reales <h1>..<h6> para la jerarquía. Nunca un <div class="big">.
  • <ul>/<ol>/<li> para listas y <dl>/<dt>/<dd> para definiciones.
  • <blockquote> para citas (se indenta) y <pre> para bloques preformateados o arte ASCII (desactiva el reflow).
  • <code> para código en línea (face de ancho fijo).

Ojo con los elementos semánticos de HTML5 (article, section, nav, header, footer, main, aside, figure): no tienen render propio. Se tratan como contenedores transparentes y solo aportan su contenido textual. Están bien para dar sentido al documento, pero no esperes que se "vean".

No dependas de CSS para nada esencial

Ya lo sabes: solo se lee el atributo style inline, y solo color, background-color, display y border-collapse. El resto se ignora. De ahí salen tres reglas que conviene tatuarse:

  • Tu página debe ser legible con el CSS totalmente desactivado. Si no lo es, no es un problema de EWW, es un problema de tu HTML.
  • No ocultes contenido con una clase display:none. SHR no la aplica y ese contenido aparecerá igualmente. Si de verdad necesitas ocultar algo tendría que ser style="display:none" inline, pero es mala práctica. Mejor no meter ese contenido y punto.
  • No transmitas información solo con color. Aunque color inline funciona, exige contraste suficiente (ese filtro CIE DE2000 tan estricto) y en terminales pobres ni se aplica. Un "campo obligatorio en rojo" o un "verde = correcto" se evaporan. Acompaña siempre con texto o símbolos.

Y una consecuencia que se cuela: el espaciado (margin, padding, line-height) no existe. La separación la dan los saltos de párrafo. Estructura con <p> de verdad, no con <br> sueltos ni divs vacíos.

Tablas: solo para datos, jamás para maquetar

SHR dibuja <table> como una rejilla ASCII sorprendentemente buena: mide columnas, reparte anchos y hasta emula colspan y rowspan. Pero tiene sus normas:

  • Úsalas solo para datos reales. Una tabla de layout produce una rejilla absurda e ilegible.
  • Vigila el ancho. Si la suma de columnas supera el frame, EWW se limita a activar truncate-lines y la experiencia se degrada. Menos columnas y celdas cortas rinden mucho mejor.
  • Las imágenes dentro de celdas no se incrustan en la celda (limitación del buffer): se insertan después de la tabla. Si el orden importa, evita imágenes ahí dentro.

Imágenes con alt y srcset

Sí, Emacs muestra imágenes en buffers gráficos. Pero no te confíes:

  • Da siempre un alt descriptivo. Es lo que se ve si la imagen está bloqueada, rota o si el usuario navega sin imágenes. En muchos flujos de EWW, el alt es el contenido.
  • srcset está soportado: EWW elige la resolución adecuada al ancho del frame, así que ofrécele varias.
  • Las imágenes cargan de forma asíncrona sobre un placeholder, no bloquean el render del texto. No dependas de una imagen para comunicar algo crítico.
  • Las data: URIs (incluida base64) funcionan, cómodas para iconos pequeños incrustados.

Formularios que funcionan de verdad

Los formularios van bien si son HTML puro con action y method (GET o POST, incluido multipart/form-data para ficheros):

  • Nada de envíos por JavaScript (onclick, fetch). Usa un <form> de verdad con su <input type="submit"> o su <button>.
  • Pon name en todos los campos y value para los valores por defecto. EWW recolecta por name.
  • Los type de texto reconocidos (text, password, email, number, date, color y el propio textarea...) se pintan como campos editables. El resto degrada a texto. checkbox, radio y select funcionan.
  • Asocia <label> a cada campo: la navegación por teclado te lo agradecerá.

Ayuda al modo legible

Si quieres que tu artículo se vea perfecto con eww-readable, conoce a tu enemigo. Su heurística puntúa cada nodo por número de palabras, penaliza los enlaces (les resta palabras), bonifica las imágenes y se queda con el nodo de más de 100 palabras y mayor puntuación.

¿La traducción práctica? Envuelve el cuerpo del artículo en un único contenedor con mucho texto seguido y no lo trocees en mil divs pequeños llenos de enlaces. Un <article> o un <main> con párrafos largos gana. Una maraña de <div> con navegación pierde.

Cabeceras y metadatos que sí cuentan

Cuatro detalles pequeños que marcan la diferencia:

  • <title>: se muestra en la header line de EWW. Ponlo siempre y que sea descriptivo.
  • <meta charset>: EWW lo usa como fallback de codificación. Declara UTF-8.
  • <base>: se respeta, útil para resolver enlaces relativos.
  • HTTPS con certificado válido: EWW colorea el título de la header line según el estado TLS. Sirve tu sitio por HTTPS.

Entonces, ¿para qué sirve?

EWW es un lector de documentos HTML excelente y un navegador web deliberadamente incompleto. Y en cuanto asumes esa dualidad, encaja como un guante en unos cuantos escenarios:

  • Leer sin salir de Emacs: documentación, blogs, artículos, wikis, man pages en HTML... con todas tus teclas de siempre y con isearch.
  • Lectura enfocada: el modo legible, con su scoring por densidad de palabras, deja el texto y nada más. Sin ruido.
  • Bajo ancho de banda y cero distracciones: sin anuncios, sin JS, sin pop-ups, sin telemetría. Accesibilidad de serie.
  • Feeds y correo con HTML: recuerda que SHR es lo que usan Gnus, elfeed y mu4e por debajo.
  • Un flujo único: buscar y abrir enlaces desde el propio Emacs, con marcadores, historial, varios buffers o pestañas y sesiones.
  • Scriptable: parseas el DOM de libxml-parse-html-region directamente y automatizas lo que quieras.

Su filosofía es la contraria a la de un navegador moderno. En lugar de emular un motor de renderizado gráfico, traduce el HTML semántico a texto de Emacs. Todo lo que sea "documento" funciona muy bien y muy rápido. Todo lo que sea "aplicación web" no funciona en absoluto. Y esa es la gracia: no pasará el test Acid3, pero no por deficiencia, sino por diseño.

Este trabajo está bajo una licencia Attribution-NonCommercial-NoDerivatives 4.0 International.

¿Me invitas a un café?

Así sigo escribiendo sin publicidad ni muros de pago.

Comentarios

Todavía no hay ningún comentario.

Sigue leyendo