8. Extending groff using Lua

To invoke Lua from groff, we use the macro .eval with its closer .endeval. These are defined in the supplied groff macro file pca-eval.tmac, which you should put in a macro directory.

.eval does only one thing: It allows you to place arbitrary Lua code until the following .endeval, and the text written to the stream troff by this Lua code is substituted for the .eval ... .endeval. The usefulness of this tactic will be apparent from an example. Consider the following groff document, tau.ms:

    .mso pca-eval.tmac
    The ratio of the circumference of a circle to
    its radius is τ ≈
    -- following prints tau, because cos(tau/2) = -1
    troff:write(2*math.acos(-1), '.0)

Run groff in unsafe mode on tau.ms:

    groff -z -U -ms tau.ms

(The -z avoids generating output, because we are not ready for it yet. The -U runs in “unsafe” mode, i.e., it allows the writing of aux files.) You will find that the groff asks you to run it again:

    Rerun groff with -U

Call groff again as follows:

    groff -U -ms tau.ms > tau.ps

tau.ps will now look like:

The ratio of the circumference of a circle to its radius is τ ≈ 6.2831853071796.

The first groff call produces a temp Lua file. The second groff call invokes Lua on this temp file to create an auxiliary file for each .eval, and each .eval sources its assigned aux file into the document.

Lua code via .eval can serve as a very powerful second extension language for groff. This benefit is available even when the document is processed by Troff2page: We could run Troff2page on the same document, tau.ms, and the resulting tau.html will show the same content.

Furthermore, we can embed .eval-enclosed Lua code inside an .if (or .ie or .el) statement so that it gets executed only for groff or only for Troff2page. (Recall we used the number register \n[.troff2page] for this purpose on page 3.) In particular, by calling Lua code only for Troff2page, the user has the means to extend Troff2page to deal with things that the core program as supplied does not.

Note that when Troff2page sees .eval-enclosed Lua code, it runs it in a Lua environment that has not only the basic Lua language but also includes the procedures and data structures defined in Troff2page. These extras are not available (and are arguably not useful) to the .eval code seen by groff.

Defining color names using HSL

For a more substantial example of .eval’s use, consider defining color names using the HSL (Hue/Saturation/Lightness) scheme rather than the RGB and CMYK schemes supported by groff’s .defcolor. For instance, we would like to define the color Venetian red using its HSL spec (357°, 49%, 24%), which to many is a more intuitive description than RGB (91, 31, 34).

While there is an algorithm to convert HSL to RGB, implementing it using groff’s limited arithmetic is tedious. Instead, we’ll define a much easier Lua procedure to do the same, and put it inside an .eval:

    .mso pca-eval.tmac
      local function between_0_and_1(n)
        if n < 0 then
          return n + 1
        elseif n > 1 then
          return n - 1
          return n

      local function tc_to_c(tc, p, q)
        if tc < 1/6 then
          return p + (q - p)*6*tc
        elseif 1/6 <= tc and tc < 1/2 then
          return q
        elseif 1/2 <= tc and tc < 2/3 then
          return p + (q - p)*6*(2/3 - tc)
          return p

      local function hsl_to_rgb(h, s, l)
        h = (h % 360) / 360
        local q
        if l < 1/2 then
          q = l * (s + 1)
          q = 1 + s - l*s
        local p = 2*l - q
        local tr = between_0_and_1(h + 1/3)
        local tg = between_0_and_1(h)
        local tb = between_0_and_1(h - 1/3)
        return tc_to_c(tr, p, q),
          tc_to_c(tg, p, q),
          tc_to_c(tb, p, q)

      function def_hsl_color(name, h, s, l)
        local r, g, b = hsl_to_rgb(h, s, l)
        troff:write(string.format('.defcolor %s rgb %s %s %s0, name, r, g, b))

Here, the Lua procedure def_hsl_color takes an HSL spec and writes out the equivalent RGB groff definition. (The Troff2page distribution provides def_hsl_color in the macro file defhsl.tmac.)

We can call this Lua procedure inside another .eval to define venetianred using its HSL spec:

    def_hsl_color('venetianred', 357, .49, .24)

The color name venetianred can now be used like any other groff color name:

    Prismacolor’s burnt ochre pencil is a close match for Derwent’s
    \fB\m[venetianred]Venetian red\m[]\fP, and either can be used to
    emulate the sanguine chalk drawings of the Old Masters.

This produces:

Prismacolor’s burnt ochre pencil is a close match for Derwent’s Venetian red, and either can be used to emulate the sanguine chalk drawings of the Old Masters.

Extending Troff2page only

Troff2page treats groff’s .ig environment, whenever it uses ## as ender, as containing Lua code that can be used to extend Troff2page. This syntactic overloading of .ig ## is an innovation introduced by Oliver Laumann’s unroff.

Any Lua code enclosed within .ig ## ... .## will be processed by Troff2page but not by groff, which treats it as a multiline comment of course. Note that .ig ## does not pipe its stdout back into the document, as .eval does. This is to maintain the invariant that as far as output is concerned, .ig ##, like other .igs, is always a comment. However, you can add Lua code within .ig ## to influence how Troff2page — but not groff! — processes the rest of the document.

For example, let’s define a \*[url ...] string register that simply typesets its URL argument within angle brackets.

    .ds url \(la\fC\\$1\fP\(ra

This is adequate for the print output. For Troff2page though, we’d like to re-define this macro, in Lua, to create a hyperlink. We enclose this re-definition in a .ig ##, which not only allows it to be in Lua, but also makes it apply only when Troff2page reads it:

    .ig ##
    defstring('url', function(url)
      return link_start(url) .. url .. link_stop()

The procedures defstring, link_start, and link_stop are defined in the Troff2page code.