Skip to content

rouge

rouge

Rouge/Pygments-class lexers that tokenize source and emit highlighted HTML.

Install

go get github.com/go-rouge/rouge

Requires Go 1.26.4 or newer. CGO_ENABLED=0.

Use

import "github.com/go-rouge/rouge"

html, err := rouge.Highlight(`puts "hello"`, "ruby", "html")
// => <span class="nb">puts</span> <span class="s2">"hello"</span>

lex := rouge.FindLexer("rb")        // by tag or alias, case-insensitive
lex = rouge.Guess(src)              // content sniff; never nil (PlainText fallback)
toks := lex.Lex(src)                // []TokenValue{Token, Value}
page := rouge.WrapHTML(rouge.HTMLFormatter{}.Format(toks))

The CSS short-codes are the gem’s own (k, s, c, nf, …), so any stylesheet written for Ruby rouge — or for Pygments — styles this output unchanged.

Languages

31 lexers, as of v0.7.0. Each is a transcription of the reference rouge 5.0.0 gem’s own lexer source, and each is validated byte-for-byte against HTML produced by that gem: every corpus file in testdata/ is highlighted by both and the output compared exactly.

LanguageTags
Cc
C++cpp, c++
COBOLcobol
CSScss
diffdiff, patch, udiff
Dockerdocker, dockerfile, Dockerfile, containerfile, Containerfile
Fortranfortran
Gogo, golang
Hashicorp Configuration Languagehcl
HTMLhtml
Javajava
JavaScriptjavascript, js
JSONjson
JSXjsx, react
Makemake, makefile, mf, gnumake, bsdmake
Markdownmarkdown, md, mkd
PHPphp, php3, php4, php5
Plain Textplaintext, text
Protobufprotobuf, proto
Pythonpython, py
Rubyruby, rb
Rustrust, rs (+ the six rust-skeptic aliases)
shellshell, bash, zsh, ksh, sh
SQLsql
Terraformterraform, tf
TeXlatex, tex, TeX, LaTeX
TOMLtoml
TSXtsx
TypeScripttypescript, ts
XMLxml
YAMLyaml, yml

The XML lexer is also the SVG lexer, as it is in the gem: it claims *.svg and *.plist along with *.xml, *.xsl, *.xsd, *.wsdl and *.rss.

Where the gem writes a lexer as a subclass, so does this port. TypeScript, JSX and TSX share one JavaScript machine, and C++ shares C’s, so a rule added to the parent is a rule the children get rather than a copy that drifts.

Where a lexer is not byte-identical, the module says so

The README carries a documented simplifications section listing every deviation from the gem and the measurement behind it — for example that the gem’s ^ and lookbehind assertions do not constrain where it evaluates them, so COBOL, Fortran and TypeScript rules drop them; and that HCL’s here-document rule and Make’s :shell function rule are not reachable in the gem itself, so they are recorded rather than quietly “fixed”. Nothing is left to be discovered by surprise.

Safety

A lexer is a parser fed untrusted input, so the module treats it as one.

  • Every lexer is fuzzed — all 31, enforced by a test that reads the lexer registry and fails on one that is registered but not fuzzed. The invariant is the one that holds whatever the input: lexing accounts for every byte — none dropped, which would delete source from a rendered page, and none invented, which would be an injection.
  • Lexing is linear in the input — all 31 lexers, including against inputs built to defeat the lazy quantifiers the gem’s rules use: unterminated comments, strings, tags, macros and template literals. Measured at 1k/2k/4k bytes, where linear is 2x per doubling and 4x is quadratic.
  • Coverage is 100%, with zero uncovered blocks rather than a rounded total.

It was not always linear, and the history is on the record

Until v0.7.1, C, C++ and Java were quadratic on an unterminated block comment: 32 KiB of /* with no closing */ took about 26 seconds (#17). That was a denial of service on exactly the untrusted input this library exists to highlight.

The cause was not backtracking. The C-family function-signature rule ends in (\{|;) — Java’s in (\() — so on an input holding none of those it scans to end of input, O(n), from every scan position, O(n) of them.

The fix is a necessary-condition prefilter: when no required byte lies at or after the scan position, the regex is not run. It can only skip attempts that would have failed, so the byte-for-byte golden suite against the gem passes unchanged. 32 KiB now takes 12.5 ms.

Two earlier versions of this page were wrong about this, which is why the history stays here. The first claimed lexing was linear across the board with a worst ratio of 2.36x — nothing measured it, and when the measurement was written three lexers turned out to be quadratic. The second told you not to highlight untrusted C, C++ or Java at all; that advice was right for v0.7.0 and is obsolete from v0.7.1.

Links

See the module’s README for full, up-to-date details.