[edit] Overleaf guides

[edit] LaTeX Basics

[edit] Mathematics

[edit] Figures and tables

[edit] References and Citations

[edit] Languages

[edit] Document structure

[edit] Formatting

[edit] Fonts

[edit] Presentations

[edit] Commands

[edit] Field specific

[edit] Class files

Contents

The goal of this article

In this article we explore how \(\mathrm\TeX\) calculates table column widths when tables contain entries (e.g., table headings) that span multiple columns (e.g., using the \(\mathrm\TeX\) primitives \omit and \span). Using a basic “reference” table as a starting point, we create a range of examples—derived from that reference table—by amending various entries to create spanned columns. By examining the effect of those alterations we can start to develop an understanding of the underlying algorithm that \(\mathrm\TeX\) uses to calculate the width of spanned columns.

Using \(\mathrm\TeX\) not \(\mathrm\LaTeX\)

To examine and explain how \(\mathrm\TeX\) itself decides the widths of spanned columns it is necessary to eschew any of the marvellous \(\mathrm\LaTeX\) table packages and revert back to fundamental, low-level (primitive), table-creation commands: in particular, \halign{...}, \span and \omit. Existing \(\mathrm\TeX\)/\(\mathrm\LaTeX\) table packages are of course essential productivity tools and provide a wealth of extremely useful functionality which enables users to quickly produce a vast range of tabular material using \(\mathrm\LaTeX\). Those packages provide essential “macro scaffolding” constructed around \(\mathrm\TeX\)’s low-level behaviour and their developers provide very welcome abstractions and layers of insulation which take care of the underlying complexities. Many of those packages are truly incredible feats of complex \(\mathrm\TeX\) programming: we should all be grateful that they exist to shield us from having to use raw \(\mathrm\TeX\)!

The actual algorithm that \(\mathrm\TeX\) uses to calculate spanned column widths is explained on page 245 of the \(\mathrm\TeX\text{book}\) and, with further details, in Section 801 (page 336) of the printed book containing \(\mathrm\TeX\)’s source code \(\mathrm\TeX\text{: The Program}\). However, for many people (myself included) Knuth’s explanations are, on occasion, rather compact and succinct and, at times, they can be difficult to follow in detail: illustrated examples are always very helpful.

Yes, tables are complex

In section 768 (page 322) of the book \(\mathrm\TeX\text{: The Program}\), Knuth makes an interesting comment:

“It’s sort of a miracle whenever \halign and \valign work, because they cut across so many of the control structures of \(\mathrm\TeX\).”

In addition, Volume IV of the four-volume book series \(\mathrm\TeX\text{ in Practice}\) dedicates no fewer than 180 pages (pp199–379) to creating tables in \(\mathrm\TeX\) via \halign and \valign.

So, it is safe to observe that \(\mathrm\TeX\) tables are indeed “rather tricky”.

Spanning columns: \omit, \span and \multispan

As noted, to explore \(\mathrm\TeX\)’s column-width calculations we need to use “raw” \(\mathrm\TeX\); what this means is a combination of primitive commands and one \(\text{Plain }\mathrm\TeX\) macro called \multispan. Although we won’t be using these commands to directly illustrate our example tables (i.e, fully explaining all the \(\mathrm\TeX\) code), it is worth including a brief note explain them:

  • \halign: One of two \(\mathrm\TeX\) primitives (commands) for creating tables. The other is \valign but that is not as widely used and will not be discussed in this article.
  • \omit: A \(\mathrm\TeX\) primitive (command) that instructs \(\mathrm\TeX\) to ignore a table entry’s preamble template.
  • \span: A \(\mathrm\TeX\) primitive (command) used to combine two adjacent table entries.
  • \multispan{n}: A plain \(\mathrm\TeX\) macro to span n columns.

In essence, to span columns \(\mathrm\TeX\) ignores the appropriate number of table preamble templates and combines the required number of table entries into a single entry. \multispan{n} works by expanding to the sequence of \omit and \span tokens needed to span n columns. For example, \multispan{3} expands to \omit\span\omit\span\omit.

Introducing our “reference” table

Here is our reference table followed by an annotated version which explains the elements used in its construction:

By amending our reference table we will observe what happens to the table’s width, and the width of individual columns, as we add entries that span various columns. This reference table was produced in raw \(\mathrm\TeX\) using the \halign{...} primitive together with a number of custom macros required to typeset the tables—we won’t discuss those macros because they are not essential to understanding the examples and explanations.

Here is an annotated version of our reference table to explain its features:

Our first set of example tables, and initial reference table, have all set \tabskip=0pt so that \(\mathrm\TeX\) does not add any space between our columns: in effect, they all touch each other. The reason for doing this is to simplify the initial discussion and ensuing calculations—later in the article we re-introduce non-zero \tabskip glue to examine its effect on calculating spanned column widths.

As noted in the annotations, we have added a small amount of white space (5pt) at the start of all non-spanning table entries (except the first row). That 5pt of white space forms part of the total width of all non-spanning entries (except the first row) and was added just to make the table look a little less cluttered.

A brief note on table widths

The \halign{...} command has three forms:

  • \halign{...}: set the table to whatever width \(\mathrm\TeX\) calculates, based on the size of the entries (and \tabskip glue);
  • \halign to width {...}: instruct \(\mathrm\TeX\) to typeset the table to a specified width;
  • \halign spread amount{...}: adjust the calculated width by amount.

When \(\mathrm\TeX\) typesets a table using \halign{...} it has to read the entire table into memory to perform the various calculations required to typeset it. Consequently, unless you have specified the width by using \halign to width {...} you cannot know the final width until \(\mathrm\TeX\) has finished processing (typesetting) it. One way to obtain the width of a table produced by \halign{...} is to first typeset the table inside a \vbox{...} (e.g., \setbox0=\vbox{\halign{...}}) and then, for example, use \the\wd0 to obtain the width.

No automatic line-breaking in table entries

It is important to note that when \(\mathrm\TeX\) is typesetting a table created with \halign{...} any text within table entries is not automatically subjected to line-breaking: table entries are typeset in restricted horizontal mode—just like an \hbox. To enable line-breaking, a table entry’s text needs to be enclosed inside a \vbox{...} together with using an appropriate value for \hsize within that \vbox{...}. Note, however, that text within a \noalign{...} command (a \(\mathrm\TeX\) primitive) used in an \halign{...} is subject to \(\mathrm\TeX\)’s line-breaking. In effect, and as its name suggests, \noalign{...} allows \(\mathrm\TeX\) to “escape” from the \halign{...} and place material between the rows of the table—typically to produce horizontal rules between table rows.

Not allowed: \halign{...} inside \hbox{...}

You cannot directly typeset an \halign{...} inside an \hbox{...}. Attempting to use \hbox{\halign{...}} will generate a rather confusing error:

! Missing } inserted.
<inserted text>
                }
<to be read again>
                   \halign
l.1 \hbox{\halign

An explanation of this error

Due to the enclosing \hbox{...} \(\mathrm\TeX\) is in restricted horizontal mode; it then detects \halign{...} which is a vertical mode command. For example, if you use \halign{...} within a paragraph, \(\mathrm\TeX\) will terminate the paragraph, process the \halign{...} and then carry on with the rest of the paragraph.

When used inside an \hbox{...}, the \halign{...} triggers \(\mathrm\TeX\) to try to escape back to vertical mode by attempting to force closure of the current group: \(\mathrm\TeX\) reports a “! Missing }” and issues an error because it thinks you’ve made a mistake in your use of grouping. Although a right-hand brace (}) may not be missing from your \(\mathrm\TeX\) code, the error message is a symptom of the \hbox{...} “getting in the way” and \(\mathrm\TeX\) taking its “best guess” at the appropriate course of action to resolve the problem.

Examples of tables with spanned columns

The following sequence of table graphics provide a range of examples to demonstrate the effect of spanning table columns: indicating that lengthy table entries can have unexpected results on the width of certain columns—and, consequently, on the width of the table itself. The question we are going to address is what does \(\mathrm\TeX\) do when a particular table entry spans a number of columns but it is “too wide to fit”. As noted above, \(\mathrm\TeX\) does indeed apply a specific algorithm to this problem of calculating column widths: the following examples are designed to help develop a “feel” for the workings of that algorithm.

Example table 1

In this example we use \multispan{2} to span columns 1 and 2 with an entry whose text is A table heading:

Observations

  • The width of this table is the same as the reference table: \(327.71722\text{pt}\).
  • The width of the entry that spans columns 1 and 2 is \(81.04953\text{pt}\) which is less than the total width of the entries in the columns it spans: \(52.56676\text{pt} + 57.06679\text{pt} = 109.63355\text{pt}\)

Example table 2

As in Example table 1, this example also uses \multispan{2} to span columns 1 and 2 but here we use a longer entry whose text is A slightly longer table heading.

Observations

If you compare this example to our reference table we can see the following:

  • The width of this table has increased from \(327.71722\text{pt}\) to \(374.37032\text{pt}\): a total of \(46.6531\text{pt}\).
  • The width of the entry that spans columns 1 and 2 (\(156.28664\text{pt}\)) is greater than the total width of the entries in the columns it spans: \(52.56676\text{pt} + 57.06679\text{pt} = 109.63355\text{pt}\). That difference is \(156.28664\text{pt}-109.63355\text{pt} = 46.6531\text{pt}\) which is the same amount by which the table width has increased.
  • \(\mathrm\TeX\) has adjusted the width of column 2 to provide the additional space required. Later we will see how \(\mathrm\TeX\) calculates the amount by which column 2 has to increase.
  • Column 1 is unaffected: its width has not been affected by the entry that spans columns 1 and 2.

Example table 3

In this example we use \multispan{3} to span columns 1 to 3 with an entry whose text is the same as Example table 2: A slightly longer table heading.

Observations

  • The width of this table is the same as the reference table: \(327.71722\text{pt}\).
  • The width of the entry that spans columns 1 to 3 (\(156.28664\text{pt}\)) is less than the total width of the entries in the three columns it spans: \(52.56676\text{pt} + 57.06679\text{pt} + 59.03899\text{pt} = 168.67254\text{pt}\).
  • None of the column widths have been affected by the entry spanning columns 1 to 3.

Are you starting to see a pattern emerging?

Example table 4

As with Example table 3, here we use \multispan{3} to span columns 1 to 3 but this time with an entry whose text is considerably longer: A considerably longer table heading that extends a long way.

Observations

  • Compared to the reference table, the width of this table has increased from \(327.71722\text{pt}\) to \(465.95685\text{pt}\): an increase of \(138.23963\text{pt}\).
  • The width of the entry that spans columns 1 to 3 is \(306.91216\text{pt}\).
  • The total width of the entries in the three spanned columns is \(52.56676\text{pt} + 57.06679\text{pt} + 59.03899\text{pt} = 168.67254\text{pt}\).
  • The difference in width between the long spanning entry and the entries in columns 1 to 3 is \(306.91216\text{pt}-168.67254\text{pt}=138.23962\text{pt}\). The same amount (to 4 decimal places!) by which the table width has increased.
  • Only column 3 has had its width increased: neither column 1 or column 2 are affected.

A pattern emerges

If we look at Example table 2 and Example table 4 we can see that in both cases it is the last column in the span which had its width increased to make space for the long entry which spanned the columns:

  • In Example table 2: The long entry spanned columns 1 and 2. Column 2 became “stretched”.
  • In Example table 4: The long entry spanned columns 1 to 3. Column 3 became “stretched”.

The width of column 3: An algorithm emerges?

The following calculations give a clearer indication of what \(\mathrm\TeX\) is doing. Here is what we know:

  • The width of the long entry spanning columns 1 to 3 is \(306.91216\text{pt}\).
  • The total width of the entries in columns 1 and 2 is \(52.56676\text{pt} + 57.06679\text{pt} = 109.63355\text{pt}\).

What is the difference between those values? It is \(306.91216\text{pt}-109.63355\text{pt} = 197.2786\text{pt}\) and this is the width used for column 3: it arises directly from the algorithm used by \(\mathrm\TeX\).

Example table 5

Before we get to a more complicated example, here is one more “simple” example. This table contains the same lengthy entry as Example table 4: A considerably longer table heading that extends a long way; however, this time we use \multispan{6} which allows that entry to span the entire table. As you can see, the resulting table still has the same width as our reference table (\(327.71722\text{pt}\)) meaning that no columns have been affected by this very long entry. Clearly, this is because the width of the entry (\(306.91216\text{pt}\)) is less than the total width of all the entries it is spanning: \(327.71722\text{pt}\); i.e., the width of the table.

Example table 6: Slightly more complicated

Here, we look at a series of three example tables (6(a)–6(c)) to show the effect of two different entries which both span into column 5. Example table 6(a) and Example table 6(b) each show a table containing a single entry that spans several columns up to column 5. Example table 6(c) combines both spanning entries into a single table and asks the question: which entry actually determines the width of column 5, and why? The answer takes us to the essence of the algorithm used by \(\mathrm\TeX\).

Example table 6(a)

Observations
  • Compared to the reference table, the width of this table has increased from \(327.71722\text{pt}\) to \(371.11153\text{pt}\): an increase of \(43.39431\text{pt}\).
  • The width of the entry that spans columns 3 to 5 is \(215.06683\text{pt}\).
  • The total width of the entries in columns 3 to 5 is \(59.03899\text{pt} + 52.98344\text{pt} + 59.6501\text{pt} = 171.67253\text{pt}\).
  • The difference in width between the entries spanned in columns 3 to 5 and the width of the spanning entry is \(215.06683\text{pt}-171.67253\text{pt}=43.3943\text{pt}\): the exact amount (to 4 decimal places!) by which the width of the table has increased .

Example table 6(b)

Observations
  • Compared to the reference table, the width of this table has increased from \(327.71722\text{pt}\) to \(353.3233\text{pt}\): an increase of \(25.60608\text{pt}\).
  • The width of the entry that spans columns 1 to 5 is \(306.91216\text{pt}\).
  • The total width of the entries in columns 1 to 5 is \(52.56676\text{pt} + 57.06679\text{pt} + 59.03899\text{pt} + 52.98344\text{pt} + 59.6501\text{pt} = 281.30608\text{pt}\).
  • The difference in width between the entries spanned in columns 1 to 5 and the width of the spanning entry is \(306.91216\text{pt}-281.30608\text{pt}=25.60608\text{pt}\): note this is less than the value calculated for Example 6(a), which was \(43.3943\text{pt}\).

Example table 6(c)

Here, we combine the entries in example tables 6(a) and 6(b) into a single table: what happens?

Observation
  • Compared to the reference table, the width of this table has increased from \(327.71722\text{pt}\) to \(371.11153\text{pt}\): an increase of \(43.39431\text{pt}\). We note this is exactly the same as Example table 6(a).

What is \(\mathrm\TeX\) doing?

To understand the results of \(\mathrm\TeX\)’s algorithm and decision-making processes we note that this entry

extends beyond the entries being spanned by \(25.60608\text{pt}\); however, this entry

extends even further beyond the entries being spanned: by \(43.3943\text{pt}\). Hence that entry “wins the race” and column 5 has its width increased by the maximum of these two values (\(43.3943\text{pt}\)). The width of column 5 now becomes \(59.6501\text{pt} + 43.3943\text{pt} = 103.0444\text{pt}\) to accommodate the entry which spans columns 3 to 5. Our description of the exact “sequence of events” is simplified slightly but the outcome is as we have described.

Bringing back some complexity

To minimize the complexity of our discussions (thus far) we’ve used relatively simple examples to demonstrate the principles of \(\mathrm\TeX\)’s algorithm; in particular, we set \tabskip=0pt. In practice, “real world” tables are likely to have many entries that span a range of columns and, of course, will have non-zero values for the \tabskip glue—a topic we’ll now revisit.

\tabskip glue and spanned column widths

Table design often requires the addition of white space between columns and, of course, \(\mathrm\TeX\) has this facility through a primitive command called \tabskip. This command can be used to put fixed or flexible glue (spacing):

  • before a table (i.e., to the left of column 1);
  • between one or more columns;
  • after the table (i.e., to the right of the last column).

Here an example to remind ourselves:

How does \tabskip glue affect spanned column widths?

The presence of non-zero \tabskip glue between columns provides additional space that spanned entries can “absorb” before \(\mathrm\TeX\) needs to think about increasing the width of the last column in a span.

In our next example we will use two tables to compare the results of spanning two columns. The only difference between the tables is the use of \tabskip glue.

  • The first example uses our original “reference” table which, if you recall, has set \tabskip=0pt.
  • The second example uses a modified version of our reference table (annotated above) which has \tabskip=10pt before and after the table but, more importantly, it has set \tabskip=20pt between the columns.

Within the modified reference table the two spanned columns have no effect on the column widths (and table width) but they do affect the width of column 2 (and table width) in the original reference table.

Original reference table: \tabskip=0pt

Here, we show our original reference table together with a second table (derived from our original reference table) which has an entry “Test a longer table heading” spanning columns 1 and 2, Quite clearly, column 2 (of the second table in the diagram) and thus the whole table, are both affected by the spanned columns.

Modified reference table: \tabskip=20pt

Here, we show our modified reference table together with a second table (derived from our modified reference table) which also has an entry “Test a longer table heading” spanning columns 1 and 2, Quite clearly, within the second table in the diagram, neither the width of column 2 or the table is affected by the spanned columns. In this case, the presence of \tabskip glue (20pt) between the columns has helped to “absorb” the space required by the text in the entry spanning colums 1 and 2:

The essence of \(\mathrm\TeX\)’s algorithm

Hopefully, the range of examples provided above have helped develop a “sense” of what \(\mathrm\TeX\) does to accommodate spanned entries and how \(\mathrm\TeX\) will, if necessary, adjust the width of the last column within each range of spanned columns. In addition to the width of entries within individual columns being spanned, the presence of non-zero \tabskip glue is an important factor that \(\mathrm\TeX\) takes into consideration when deciding whether it needs to adjust any column widths. The key point to remember is that \(\mathrm\TeX\)’s goal is to calculate a suitable width for the last column within each range of spanned columns.

Final table example: last columns in a spanned range

In this final example we once again use our modified reference table (with \tabskip glues values discussed above) to derive another table which contains various columns spanned by rules—we have used rules to make the spans easier to see.

The two tables have been carefully aligned to show that, in the upper table, no columns prior to column 5 have been affected by the spanned columns. The darker green area to the left of the diagram shows that columns 1 to 4 of both tables still align perfectly. On the right is a lighter-green shaded area showing that only columns 5 and 6 have been affected by the spanned entries.

In the upper table, the spans are as follows:

  • columns 1 to 5: spanned by a \(400\text{pt}\) rule;
  • columns 3 to 5: spanned by a \(200\text{pt}\) rule;
  • columns 4 to 6: spanned by a \(250\text{pt}\) rule.

Once again, the explanation is that within a series of spanned columns, only the width of the final column is adjusted (if required): intervening columns are not affected and here that means columns 1 to 4—although, of course, the width of columns 1 to 4 (and intervening \tabskip glue) is taken into consideration when calculating the adjusted widths of columns 5 and 6.

A walkthrough of \(\mathrm\TeX\)’s algorithm

We’ll conclude with a simplified walkthrough of “\(\mathrm\TeX\)’s thought processes” as it works out the widths of columns in spanned entries. Describing \(\mathrm\TeX\)’s algorithms is not always straightforward so we’ll adopt some “simplifying artistic licence” to provide an overview of what is happening. Readers interested in all the messy details are referred to Section 801 (page 336) of the printed book containing \(\mathrm\TeX\)’s source code \(\mathrm\TeX\text{: The Program}\).

Real-world tables are often created with many uses of the \span primitive (e.g., inside \(\mathrm\LaTeX\) packages) to construct multiple instances of spanned columns within the table. To manage this, the data structures (deep inside \(\mathrm\TeX\)) maintain information (so-called span nodes) which tell \(\mathrm\TeX\) about the links (spans) between table entries/columns. Clearly, \(\mathrm\TeX\) has to apply its algorithms in a systematic way and will need to process the entire table to make its final calculations—to determine all column widths, the total width of the table and, if required, the amount by which flexible glues used in the table have to stretch or shrink. It is not really surprising that \(\mathrm\TeX\) cannot tell you the final table width until it has completely processed the \halign{...} command—it really has a lot of work to do!

The starting point for column-width calculations is column 1 because, of course, nothing can span from the left of (and across/into) column 1. \(\mathrm\TeX\) starts out by determining the width of column 1 by determining which entry that has the maximum natural width. Let’s call that maximum width \(w_1\)and if there are entries that span from column 1 to column 2 let’s call the width of that entry \(w_{12}\) (width from 1 to 2). In addition, we’ll denote the \tabskip glue between columns 1 and 2 as \(t_{1}\)—note that we are only considering the natural width of that \tabskip glue and, for now, ignoring any stretch or shrink components it might possess. Also, let the maximum natural width of all non-spanning entries in column 2 be \(w_2\).

The key point to note is that \(\mathrm\TeX\) is trying to calculate the width of column 2 by considering only those entries where the span starts with column 1 and ends at column 2. The key consideration for \(\mathrm\TeX\) is the test \(\max(w_{2}, w_{12} - (w_1+ t_1))\)—there might be multiple entries spanning columns 1 and 2: some may be narrow (small \(w_{12}\)), others very wide (large \(w_{12}\)) so \(\mathrm\TeX\) is looking for the one that has the greatest effect (hence the \(\max(\text{...})\)). Here, the value of \(w_{12} -(w_1+ t_1)\) is the amount by which an entry spanning columns 1 and 2 “spills over” from column 1 into column 2: note that \(\mathrm\TeX\) is using the width of column 1 and the \tabskip glue (\(t_{1}\)) between columns 1 and 2. Once \(\mathrm\TeX\) has determined whether any spans from column 1 to 2 do affect the width of column 2 it sets the width of column 2 to the maximum value it has determined (using the test described). \(\mathrm\TeX\) continues to work its way through all the other columns, performing similar tests.

And finally, just for completeness, here we quote the essence of \(\mathrm\TeX\)’s algorithm for calculating column widths (taken from Knuth’s source code documentation of \(\mathrm\TeX\)):

Let \(w_{ij}\) be the maximum of the natural widths of all entries that span columns \(i\) through \(j\), inclusive. The final column widths are defined by the formula

\( \begin{equation*} w_j=\max_{1\leq i\leq j}\biggl(w_{ij}-\sum_{i\leq k< j}(t_k+w_k)\biggr) \end{equation*} \)

where \(t_k\) is the natural width of the tabskip glue between columns \(k\) and \(k+1\).

Colophon: Using Overleaf to produce tables as SVG graphics

All \(\mathrm\TeX\) tables presented in this article are Scalable Vector Graphics (SVG) files produced on the Overleaf platform. The annotations (arrows and green boxes) were added by opening the SVG graphic in Inkscape—note, however, that the text of the annotations was typeset in \(\mathrm\TeX\) as additional text to accompany the table: only the arrows and green backgrounds were added in Inkscape. If you are interested to know how this was achieved, read on.

Overleaf’s servers use the \(\mathrm\TeX \text{ Live}\) distribution which, in addition to \(\mathrm\TeX\)-based typesetting engines, provides a wealth of very useful \(\mathrm\TeX\)-related software tools and utilities. Among those is one called dvisvgm which, as its name suggests, converts \(\mathrm\TeX\)’s traditional DVI (DeVice Independent) output file format into SVG. Among its many command-line options dvisvgm provides an option (-n or --no-fonts) that will instruct it to convert all text into paths which means that the text in SVG graphics is drawn using lines and curves rather than actual fonts and glyphs. This may increase the file size of the resulting SVG graphic but it ensures that the SVG graphics are extremely portable and almost certain to work well on any device.

So... how was it done?

In a previous article I discussed how you can use \(\text{Lua}\mathrm\TeX\) to run the various software tools and utilities installed on Overleaf’s servers—it is an extremely easy and convenient technique. That technique was used to generate SVG graphics of typeset \(\mathrm\TeX\) tables, as follows. From within the main \(\mathrm\TeX\) document file, the code to typeset each table (created using using \halign) was written to a .tex file. This was achieved by enclosing the table code within a pair of commands which I called \beginscoop and \endscoop. There are likely to be many other ways to achieve the desired results but here are the macro definitions that I used:

\def\cc{\catcode`\#=12\relax}
\long\def\scoop#1\endscoop{\global\fulltoks={#1}\egroup}
\def\beginscoop{\global\advance\numfigs by1\relax\bgroup\cc\scoop}

You use them like this:

\beginscoop
\halign{...}
\endscoop

Note that the \endscoop token merely serves to delimit the parameter of the \scoop macro: \(\mathrm\TeX\) effectively discards the \endscoop token so we don’t actually need to define it (e.g., by \def\endscoop{...}).

The \(\mathrm\TeX\) code contained in the \halign{...} is saved into a toks register called \fulltoks. One tricky point I came across (with \(\text{Lua}\mathrm\TeX\)) was needing to prevent # characters within the \halign{...} preamble from being “doubled” to ## when written out to a .tex file. To avoid this I had to temporarily set the \catcodeof # characters to 12 prior to saving the \(\mathrm\TeX\) code (tokens) in the \fulltoks token register.

The next step is to write the tokens contained in \fulltoks as a \(\mathrm\TeX\) file—because I was using \(\text{Lua}\mathrm\TeX\) this proved to be extremely easy thanks to \(\text{Lua}\mathrm\TeX\)’s wonderful Lua API. In brief, I wrote a macro called \writefile{...} which takes as its parameter the name of a token register whose tokens you want to write out to a file (e.g., \writefile{fulltoks}). Within the \writefile{...} macro I used the Lua API to get a textual representation of the \fulltoks token register:

\def\writefile#1{%
\directlua{
...
...
 local p=tex.toks["#1"] 
...
...
}}

Here is a screenshot showing a little more of the \writefile{...}command:

The Lua language and the Lua API provided by \(\text{Lua}\mathrm\TeX\) can often simplify \(\mathrm\TeX\) programming tasks and it is because of these useful and powerful features that I have used \(\text{Lua}\mathrm\TeX\) since ~2009—and remain a huge fan of this truly marvellous \(\mathrm\TeX\) engine. OK, the \(\text{Lua}\mathrm\TeX\) advert now concludes.

Having so easily obtained the \(\mathrm\TeX\) code stored in \fulltoks it is written out to a file together with some additional code to make it into a correctly formed \(\mathrm\LaTeX\) file. The next steps are:

  1. Process the .tex file containing our table with \(\text{pdf}\mathrm\LaTeX\) (in DVI mode) so that it typesets the table and generates a .dvi file for dvisvgm to process. Yes, you can use \(\text{Lua}\mathrm\TeX\) to run \(\text{pdf}\mathrm\LaTeX\)—once again I used the method discussed in a previous article.
  2. And finally, run dvisvgm to process the .dvi file to generate an SVG graphic of the typeset \(\mathrm\TeX\) table.
  3. To obtain the actual SVG graphics you can download a ZIP file from Overleaf—making sure to select the Input and Output Files option.