EWW, the Emacs browser you underestimate

andros1 pts0 comments

EWW, the Emacs browser you underestimate | Andros Fenollosa

Skip to content

The first time I opened a web page inside Emacs I thought it was a trick, a hack, a toy... studying it in depth I saw I was wrong. EWW is a browser written 100% in Emacs Lisp, with no external engine behind it (no WebKit, Blink or Gecko). It is absurdly lightweight, intelligently designed and, almost without meaning to, it hands you a platform to do scraping or automate web tasks in a few lines. A tool with enormous potential that many people don't know about, even within the Emacs community itself.

And the best part is that you already have it installed. It ships with Emacs by default. To launch it just run M-x eww and type a URL or a search term (it will open DuckDuckGo).

Is it a replacement for Chrome or Firefox? No, and it doesn't try to be. It plays in a different league.

Two pieces: EWW and SHR

What we call "the Emacs browser" is really two pieces working together.

EWW (eww.el) is the browser layer: URLs, history, bookmarks, forms, cookies, downloads and sessions.

SHR , or Simple HTML Renderer (shr.el), is the engine that turns HTML into text inside a buffer. And EWW is not the only one using it: Gnus for mail, elfeed for feeds, and quite a few other packages share it too.

Here's the key: SHR doesn't draw a page, it translates it. It takes the HTML and paints it as Emacs text, with its faces and its properties. What does it understand along the way? Quite a bit more than you'd imagine:

Rich text : b, i, em, strong, u, s, code, tt, mark, ins, del, sup, sub, abbr, bdo/bdi.

Structure : h1..h6, p, div, blockquote, pre, hr, ul/ol/li, dl/dt/dd.

Links and tables .

Images : it understands data: URIs (base64), srcset (it picks the resolution), cid: (mail), scaling with shr-max-image-proportion, animation and zoom. And if a src is broken, it falls back to its alt text, as it should.

MathML : it keeps the TeX annotation, it does not render the formula.

Forms are a curious case: SHR doesn't add them, EWW layers them on top via shr-external-rendering-functions. Thanks to that you can submit GET and POST forms, and even multipart/form-data to upload files.

Two things are missing from the list: JavaScript and CSS.

What EWW doesn't do (and why that's fine)

EWW is not meant to run modern web applications. Its limitations aren't an oversight, they are the reason it's so fast and so lightweight. But you'd better be clear about them before you get frustrated.

No JavaScript. This rules out, in one stroke, any SPA (React, Vue, Angular), infinite scroll, content that arrives via fetch or XHR, and most of today's web. If a page needs JS to paint itself, in EWW you'll see little or nothing.

No CSS. The code itself confesses it in its header: "It does not do CSS, JavaScript or anything advanced". In practice:

sheets and are ignored completely. Only the inline style attribute is read, and only if it contains color, display (specifically none) or border-collapse. Everything else (font-size, margin, padding, float, flex, grid, text-align...) is thrown in the bin.

Colors require (display-color-cells) >= 88. And the contrast system is surprisingly serious: it converts to CIE Lab and uses CIE DE2000 distance to make sure the text is readable.

There are no class or id selectors, no cascade, no specificity. Nothing.

The parser is not HTML5-conformant. It uses libxml2, which is tolerant but does not follow the HTML5 parsing algorithm to the letter. Manual patches are applied to plug the holes.

EWW is not for SPAs, online banking, JS dashboards, dynamic forms, anything that throws a "enable JavaScript to continue" at you, embedded video or audio, or layouts that are only legible thanks to CSS. It's not its turf, and forcing it is a waste of time.

Scraping out of the box

EWW leans on libxml-parse-html-region, which gives you back the DOM as an S-expression. And Emacs includes dom.el to walk it. That makes scraping trivial, and you don't even need to open EWW.

Look at this script. It extracts the headlines from the Hacker News front page:

(require 'dom)<br>(require 'url)<br>(require 'cl-lib)

(defun demo-scrape (url)<br>"Download URL and return the Hacker News headlines as a list of conses.<br>Each element is (TITLE . HREF)."<br>(with-current-buffer (url-retrieve-synchronously url t t 30)<br>(goto-char (point-min))<br>;; Skip HTTP headers until the first blank line.<br>(re-search-forward "\r?\n\r?\n" nil t)<br>(let* ((dom (libxml-parse-html-region (point) (point-max)))<br>;; On HN each headline is ....<br>(titles (dom-by-class dom "titleline")))<br>(mapcar (lambda (node)<br>(let ((a (dom-child-by-tag node 'a)))<br>(cons (string-trim (dom-texts a)) ; link text<br>(dom-attr a 'href)))) ; destination<br>titles))))

(defun demo-scrape-hn ()<br>"Download the Hacker News front page and show the headlines in a buffer."<br>(interactive)<br>(let ((items (demo-scrape "https://news.ycombinator.com/")))<br>(with-output-to-temp-buffer "*HN headlines*"<br>(princ (format "Headlines found: %d\n\n" (length...

emacs text browser page html headlines

Related Articles