Explore the inner workings of TeX’s box model using LuaTeX’s powerful Lua integration. This advanced guide reveals how LaTeX constructs documents internally and provides practical techniques for debugging and manipulating the typesetting process.

Advanced Topic: This guide assumes strong LaTeX knowledge and basic programming experience. For LaTeX basics, start with Creating Your First Document.

What You’ll Learn

  • ✅ TeX’s fundamental box model concepts
  • ✅ How LaTeX builds pages from boxes
  • ✅ Using LuaTeX to inspect box contents
  • ✅ Practical debugging techniques
  • ✅ Manipulating boxes programmatically
  • ✅ Real-world applications
  • ✅ Performance considerations

Introduction to TeX Boxes

What Are Boxes?

In TeX, everything on a page is built from boxes. Think of boxes as rectangular containers that hold content:

The TeX Box Hierarchy

Character
Single glyph box
hbox
Horizontal list
vbox
Vertical list
Page
Complete output

Box Types

hbox (Horizontal Box)

Contains items arranged horizontally:

  • Characters in a word
  • Words in a line
  • Inline math
\hbox{Hello World}

vbox (Vertical Box)

Contains items arranged vertically:

  • Lines in a paragraph
  • Paragraphs on a page
  • Display math
\vbox{Line 1\Line 2}

Glue (Flexible Space)

Stretchable/shrinkable space:

  • Between words
  • Between paragraphs
  • For justification
\hskip 1em plus 2pt minus 1pt

LuaTeX: Opening Pandora’s Box

What Makes LuaTeX Special?

LuaTeX embeds the Lua programming language directly into TeX, providing:

  1. Direct access to TeX’s internal structures
  2. Ability to manipulate nodes and boxes
  3. Powerful debugging capabilities
  4. Performance optimizations through callbacks

Basic Box Inspection

\documentclass{article}
\begin{document}

% Create a simple box
\setbox0=\hbox{Hello World}

% Inspect it with Lua
\directlua{
  local box = tex.box[0]
  print("Box width: " .. box.width / 65536 .. "pt")
  print("Box height: " .. box.height / 65536 .. "pt")
  print("Box depth: " .. box.depth / 65536 .. "pt")
}

% Use the box
\box0

\end{document}

Console output:

Box width: 50.27774pt
Box height: 6.94444pt  
Box depth: 0.0pt

Understanding Node Lists

Every box contains a node list - a linked list of items:

\documentclass{article}
\begin{document}

\setbox0=\hbox{Hi}

\directlua{
  local box = tex.box[0]
  local head = box.head
  
  -- Traverse the node list
  for node in node.traverse(head) do
    print("Node type: " .. node.id .. 
          " (" .. node.type(node.id) .. ")")
    
    if node.id == node.id("glyph") then
      print("  Character: " .. unicode.utf8.char(node.char))
      print("  Font: " .. node.font)
    elseif node.id == node.id("glue") then
      print("  Width: " .. node.width / 65536 .. "pt")
    end
  end
}

\end{document}

Console output:

Node type: 0 (glyph)
  Character: H
  Font: 1
Node type: 0 (glyph)
  Character: i
  Font: 1

Practical Box Visualization

Creating a Box Inspector

\documentclass{article}
\usepackage{xcolor}

\directlua{
  function inspect_box(n)
    local box = tex.box[n]
    if not box then
      print("Box " .. n .. " is empty")
      return
    end
    
    print("\string\nBox " .. n .. " properties:")
    print("  Type: " .. (box.id == 0 and "hbox" or "vbox"))
    print("  Width: " .. box.width / 65536 .. "pt")
    print("  Height: " .. box.height / 65536 .. "pt")
    print("  Depth: " .. box.depth / 65536 .. "pt")
    
    -- Count nodes
    local count = 0
    for node in node.traverse(box.head) do
      count = count + 1
    end
    print("  Nodes: " .. count)
  end
}

\newcommand{\inspectbox}[1]{%
  \directlua{inspect_box(#1)}%
}

\begin{document}

% Create different types of boxes
\setbox1=\hbox{Simple text}
\setbox2=\hbox{$x^2 + y^2 = z^2$}
\setbox3=\vbox{\hsize=3cm Lorem ipsum dolor sit amet.}

\inspectbox{1}
\inspectbox{2}
\inspectbox{3}

\end{document}

Visualizing Box Structure

\documentclass{article}
\usepackage{tikz}

\directlua{
  function draw_box_structure(n, x, y)
    local box = tex.box[n]
    if not box then return end
    
    -- Draw box outline
    tex.print("\string\\draw[red,thick] (" .. x .. "," .. y .. ") rectangle +(" 
              .. box.width/65536 .. "pt," .. box.height/65536 .. "pt);")
    
    -- Draw baseline
    tex.print("\string\\draw[blue,dashed] (" .. x .. "," .. y .. ") -- +(" 
              .. box.width/65536 .. "pt,0);")
    
    -- Add dimensions
    tex.print("\string\\node[above,font=\string\\tiny] at (" 
              .. x + box.width/131072 .. "," .. y + box.height/65536 
              .. ") {" .. string.format("%.1f", box.width/65536) .. "pt};")
  end
}

\begin{document}

\setbox0=\hbox{Sample Text}

\begin{tikzpicture}
\directlua{draw_box_structure(0, 0, 0)}
\node at (0,0) {\box0};
\end{tikzpicture}

\end{document}

Rendered output:

53.1pt
Sample Text

Advanced Box Manipulation

Modifying Box Contents

\documentclass{article}

\directlua{
  function add_color_to_glyphs(head)
    for n in node.traverse(head) do
      if n.id == node.id("glyph") then
        -- Insert color node before glyph
        local color = node.new(node.id("whatsit"), 
                              node.subtype("pdf_colorstack"))
        color.stack = 0
        color.cmd = 1  -- push
        color.data = "1 0 0 rg"  -- red color
        
        head = node.insert_before(head, n, color)
        
        -- Insert color reset after glyph
        local reset = node.new(node.id("whatsit"), 
                              node.subtype("pdf_colorstack"))
        reset.stack = 0
        reset.cmd = 2  -- pop
        
        head = node.insert_after(head, n, reset)
      end
    end
    return head
  end
  
  -- Register callback
  luatexbase.add_to_callback("pre_linebreak_filter", 
                            add_color_to_glyphs, 
                            "color glyphs")
}

\begin{document}
This text will have each character colored individually!
\end{document}

Box Metrics Analysis

\documentclass{article}

\directlua{
  function analyze_paragraph_boxes()
    local head = tex.lists.page_head
    if not head then return end
    
    local line_count = 0
    local total_badness = 0
    
    for n in node.traverse(head) do
      if n.id == node.id("hlist") then
        line_count = line_count + 1
        
        -- Check glue settings
        if n.glue_sign == 1 then  -- stretching
          print("Line " .. line_count .. 
                " stretched by factor " .. n.glue_set)
        elseif n.glue_sign == 2 then  -- shrinking
          print("Line " .. line_count .. 
                " shrunk by factor " .. n.glue_set)
        end
      end
    end
  end
  
  -- Add to shipout callback
  luatexbase.add_to_callback("pre_shipout_filter",
    function(head)
      analyze_paragraph_boxes()
      return head
    end, "analyze paragraphs")
}

\begin{document}
\parbox{3cm}{
This is a narrow paragraph that will likely have 
some badly stretched or compressed lines that we 
can detect and analyze using our Lua code.
}
\end{document}

Real-World Applications

1. Debugging Overfull/Underfull Boxes

\documentclass{article}
\usepackage{xcolor}

\directlua{
  -- Highlight overfull hboxes
  function highlight_overfull_boxes(head, groupcode)
    for n in node.traverse(head) do
      if n.id == node.id("hlist") and n.width > tex.hsize then
        -- Create a rule to highlight
        local rule = node.new(node.id("rule"))
        rule.width = n.width
        rule.height = n.height
        rule.depth = n.depth
        
        -- Add color
        local color = node.new(node.id("whatsit"), 
                              node.subtype("pdf_colorstack"))
        color.stack = 0
        color.cmd = 1
        color.data = "1 0 0 0.2 k"  -- light red
        
        n.head = node.insert_before(n.head, n.head, color)
        n.head = node.insert_before(n.head, n.head, rule)
      end
    end
    return head
  end
  
  luatexbase.add_to_callback("post_linebreak_filter",
                            highlight_overfull_boxes,
                            "highlight overfull")
}

\begin{document}
This line contains a verylongwordthatwillcauseanoverfullhbox in our text.
\end{document}

2. Custom Line Breaking

\documentclass{article}

\directlua{
  function custom_linebreak_filter(head, is_display)
    -- Get natural breaks
    local copy = node.copy_list(head)
    local params = {
      hsize = tex.hsize,
      emergencystretch = tex.emergencystretch,
      pretolerance = tex.pretolerance,
      tolerance = tex.tolerance
    }
    
    local breaks, info = tex.linebreak(copy, params)
    
    -- Analyze break quality
    if info.prevgraf > 5 then
      -- For long paragraphs, try different parameters
      params.tolerance = 2000
      params.emergencystretch = tex.sp("3em")
      
      local alt_breaks, alt_info = tex.linebreak(head, params)
      
      if alt_info.demerits < info.demerits then
        print("Using alternative line breaking")
        return alt_breaks
      end
    end
    
    return breaks
  end
  
  luatexbase.add_to_callback("linebreak_filter",
                            custom_linebreak_filter,
                            "custom linebreak")
}

\begin{document}
\noindent This paragraph demonstrates custom line 
breaking logic that adjusts parameters based on 
paragraph length and quality metrics.
\end{document}

3. Box Measurement Tools

\documentclass{article}

\directlua{
  function measure_content(text)
    -- Create temporary box
    local box = node.hpack(
      node.copy_list(
        tex.nest[tex.nest.ptr].head
      )
    )
    
    print("Content measurements:")
    print("  Width: " .. box.width / 65536 .. "pt")
    print("  Height: " .. box.height / 65536 .. "pt")
    print("  Depth: " .. box.depth / 65536 .. "pt")
    
    -- Calculate ink coverage
    local glyph_area = 0
    for n in node.traverse(box.head) do
      if n.id == node.id("glyph") then
        glyph_area = glyph_area + n.width * n.height
      end
    end
    
    local total_area = box.width * (box.height + box.depth)
    local coverage = glyph_area / total_area * 100
    
    print("  Ink coverage: " .. string.format("%.1f%%", coverage))
  end
}

\newcommand{\measure}[1]{%
  \setbox0=\hbox{#1}%
  \directlua{
    local b = tex.box[0]
    measure_content()
  }%
  \box0%
}

\begin{document}
\measure{Sample text for measurement}
\end{document}

Debugging Techniques

Visual Box Debugging

\documentclass{article}
\usepackage{xcolor}

\directlua{
  local show_boxes = true
  
  function visualize_boxes(head, groupcode)
    if not show_boxes then return head end
    
    for n in node.traverse(head) do
      if n.id == node.id("hlist") or n.id == node.id("vlist") then
        -- Add colored frame
        local rule = node.new(node.id("rule"))
        rule.width = tex.sp("0.1pt")
        rule.height = n.height + tex.sp("2pt")
        rule.depth = n.depth + tex.sp("2pt")
        
        -- Different colors for different box types
        local color = n.id == node.id("hlist") and 
                     "0 0 1" or "1 0 0"  -- blue/red
        
        -- Insert visualization
        -- (simplified for clarity)
      end
    end
    return head
  end
}

% Toggle command
\newcommand{\showboxes}{\directlua{show_boxes = true}}
\newcommand{\hideboxes}{\directlua{show_boxes = false}}

\begin{document}
\showboxes
This text will show box boundaries.

\hideboxes  
This text will not.
\end{document}

Performance Profiling

\documentclass{article}

\directlua{
  local stats = {
    total_boxes = 0,
    total_glyphs = 0,
    total_glue = 0,
    processing_time = 0
  }
  
  function profile_document(head)
    local start_time = os.clock()
    
    for n in node.traverse_id(node.id("hlist"), head) do
      stats.total_boxes = stats.total_boxes + 1
      
      for m in node.traverse(n.head) do
        if m.id == node.id("glyph") then
          stats.total_glyphs = stats.total_glyphs + 1
        elseif m.id == node.id("glue") then
          stats.total_glue = stats.total_glue + 1
        end
      end
    end
    
    stats.processing_time = os.clock() - start_time
    return head
  end
  
  function print_stats()
    print("\string\nDocument Statistics:")
    print("  Total boxes: " .. stats.total_boxes)
    print("  Total glyphs: " .. stats.total_glyphs)
    print("  Total glue nodes: " .. stats.total_glue)
    print("  Processing time: " .. 
          string.format("%.3f", stats.processing_time) .. "s")
  end
  
  luatexbase.add_to_callback("post_linebreak_filter",
                            profile_document,
                            "profile")
}

\AtEndDocument{\directlua{print_stats()}}

\begin{document}
Your document content here...
\end{document}

Best Practices

1. Performance Considerations

⚡ Performance Tips

  • Cache calculations: Store results of expensive operations
  • Minimize traversals: Use specific node types when possible
  • Batch operations: Group modifications together
  • Clean up: Free unused nodes with node.free()

2. Safety Guidelines

Important Safety Rules:

  • Always check if nodes exist before accessing
  • Use node.copy_list() when modifying shared content
  • Be careful with callbacks - they affect all processing
  • Test thoroughly - box manipulation can break output

3. Debugging Workflow

  1. Start simple: Test with minimal examples
  2. Use print statements: Track execution flow
  3. Visualize: Draw boxes to understand structure
  4. Compare: Check against known good output
  5. Profile: Measure performance impact

Quick Reference

LuaTeX Box Commands

Box Access

tex.box[n]node.traverse(head)node.traverse_id(id, head)node.copy_list(head)

Node Properties

node.id, node.next, node.prevbox.width, box.height, box.depthglyph.char, glyph.fontglue.width, glue.stretch

Callbacks

pre_linebreak_filterpost_linebreak_filterpre_shipout_filterbuildpage_filter

Further Resources

LaTeX Cloud Studio supports LuaTeX! Enable it in your project settings to use these advanced features. Our platform provides enhanced debugging output and visualization tools.