Syntax highlighting for 150 languages in < 200 KB

After creating a (formerly) 270 KB static site generator and adding internal link-checking with minimal bloat, the feature creep continues: luasmith now supports syntax highlighting for ~150 languages, thanks to Scintillua--and it only increased the gzipped download by about 180 KB!

This post describes my path to adding lightweight and customizable syntax highlighting to luasmith.

Research

Initial research

My initial research on existing well-supported solutions was fruitless:

What I wanted was something either Lua-native or small but easily extensible.

LPeg and Scintillua

The most obvious starting point for parsing in Lua is LPeg, from the same folks who created Lua. While investigating LPeg-based highlighters, I ran across Scintillua, which supports an impressive number of languages with a minimalist approach--it's even apparently used in the lightweight vis editor.

Investigating further, I found that Scintillua was well documented and a sample grammar from the documentation was easy to understand. Here's that sample grammar, for reference:

local lexer = lexer
local P, S = lpeg.P, lpeg.S

local lex = lexer.new(...)

lex:add_rule('keyword', lex:tag(lexer.KEYWORD, lex:word_match(lexer.KEYWORD)))
lex:add_rule('custom', lex:tag('custom', 'quux'))
lex:add_rule('identifier', lex:tag(lexer.IDENTIFIER, lexer.word))
lex:add_rule('string', lex:tag(lexer.STRING, lexer.range('"')))
lex:add_rule('comment', lex:tag(lexer.COMMENT, lexer.to_eol('#')))
lex:add_rule('number', lex:tag(lexer.NUMBER, lexer.number))
lex:add_rule('operator', lex:tag(lexer.OPERATOR, S('+-*/%^=<>,.()[]{}')))

lex:add_fold_point(lexer.OPERATOR, '{', '}')

lex:set_word_list(lexer.KEYWORD, {'foo', 'bar', 'baz'})

return lex

Given that Scintillua used one tenth the code of highlight.js and was Lua-native and well documented, it was exactly what I was looking for.

Implementation

Highlighting code blocks

For maximum efficiency, I would have hooked syntax highlighting directly into md4c (the Markdown parser luasmith uses), so that it would syntax-highlight code as it is parsed. That might have required creating a callback mechanism for md4c, leading to Lua-calling-into-C-calling-into-Lua messiness.

In pursuit of relentless laziness simplicity, I instead used the trivial HTML parser I previously created for link-checking to find <pre> and <code> blocks with language-* CSS classes on them, and funneled those into Scintillua. This resulted in maybe a page of simple Lua code, as opposed to probably a few hundred lines of tricky C-Lua integration code in the "optimal" implementation. I guess the "efficiency" I optimized for was "time to produce a working tool, with acceptable performance", rather than strict runtime performance.

Aside: given that this approach used HTML as input, it would be trivial to use it to create a tool that adds syntax highlighting to hand-authored HTML files, or even a tool that adds syntax highlighting as <b>, <i>, etc. tags instead of <span>'s, so that text mode browsers like Lynx or w3m could enjoy the fun!

Loading grammars

Rather than ship a zip with 100+ Lua files from Scintillua, I decided to embed all those scripts directly into the executable (with the ability to override them, as needed). This allowed me to ship a single static binary (my preferred packaging method for one-off tools).

The only trick was that Scintillua, in its library form, implemented its own file search and load system based on probing files with Lua's loadfile function. I ended up wrapping loadfile with some code to detect Scintillua's calls and point them at the embedded strings (which are lazily loaded into Lua). In general, I dislike Lua's file-loading infrastructure, but I understand that they're limited by the C standard library (specifically: its lack of file enumeration support).

Result

In the end, adding in LPeg, Scintillua, almost all its grammars, and glue code only added around 175 KB to the (gzip-compressed) tarball (a 67% increase--not as bad as I originally feared). Even better, syntax highlighting for my blog only increased the generation time by around 50% (roughly 1 millisecond per page, on my 2013 laptop).

Finally, I had a small and simple static site generator that did what I wanted!

Bonus: authoring an etlua grammar

One aspect of Scintillua that hooked me was that adding new grammars simply required writing some Lua code, using the LPeg library. I had not actually used LPeg before, but its documentation was easy to understand (and there's also a great LPeg tutorial on leafo.net).

Given that luasmith uses etlua for templates, I thought it would be fun to write a syntax highlighter for etlua.

Structure

Scintillua's documentation provides information and examples for embedding one parser within another. In the case of etlua, I'm assuming Lua code will always be embedded in HTML, so an etlua parser should simply be an HTML parser with Lua embedded between <% ... %> tags. The author of Scintillua even pointed out to me that there's already a very similar parser: RHTML (Ruby in HTML).

Implementation

Using the RHTML parser as a starting point, it was simply a matter of including the appropriate start and end tags (note: LPeg uses Lua metatables for repurposing arithmetic operations as parsing expression grammar building blocks, e.g. ^-1 means "at most one", * means "concatenation", and + means "alternation/or" -- and lpeg.P(...) just matches string literals). Here's the trivial parser:

local lexer = lexer
local P, S = lpeg.P, lpeg.S

-- Base on the existing HTML parser
local lex = lexer.new(..., {inherit = lexer.load('html')})

-- Embed the existing Lua parser
local lua = lexer.load('lua')

-- Specify `<%` and `%>` (and highlight the tags as "preprocessor directives")
local start_rule = lex:tag(lexer.PREPROCESSOR, '<%' * (P('=') + P('-'))^-1)
local end_rule = lex:tag(lexer.PREPROCESSOR, P('-')^-1 * '%>')

-- Highlight contained code as Lua
lex:embed(lua, start_rule, end_rule)

return lex

Result

Finally, here is an example of a highlighted etlua template:

<ul>
<% for i, item in ipairs(table.sortBy(items, "date", true)) do -%>
<li><a href="<%= item.path %>"><%= item.title %></a></li>
<% end -%>
</ul>

Notice that for i, item in ipairs(...) is highlighted as Lua code, while the surrounding HTML is highlighted as HTML. Of course, this also reveals a limitation of Sctinllua's nesting approach in that the item.path Lua code that's inside an HTML attribute is just treated as part of the HTML attribute, i.e. a string. Not perfect, but also not a big deal for, essentially, ornamentation.

Overall, it works pretty well, while remaining small and simple.

Further reading

If a minimal, Lua-based static site generator sounds good to you, give luasmith a look. Note that it's still "alpha" quality, and is likely to remain that way unless it miraculously becomes internet famous.

Example sites are here:

Note: The latter repository contains a simple self-contained luasmith "template" (script) for generating its README file.