diff --git a/silex/packages/color/init.lua b/silex/packages/color/init.lua
index fb0cc30..4c1dfd2 100644
--- a/silex/packages/color/init.lua
+++ b/silex/packages/color/init.lua
@@ -14,9 +14,7 @@ function package:registerCommands ()
     -- (folio, footnotes, etc.)
     SILE.typesetter:liner("color", content, function (box, typesetter, line)
       SILE.outputter:pushColor(color)
-      for _, node in ipairs(box.inner) do
-        node:outputYourself(typesetter, line)
-      end
+      box:outputContent(typesetter, line)
       SILE.outputter:popColor()
     end)
   end, "Changes the active ink color to the color <color>.")
diff --git a/silex/packages/pdf/init.lua b/silex/packages/pdf/init.lua
index 4500095..fbe5865 100644
--- a/silex/packages/pdf/init.lua
+++ b/silex/packages/pdf/init.lua
@@ -63,12 +63,10 @@ function package:registerCommands ()
 
         -- Build the content.
         -- Cursor will be moved by the actual definitive size.
-        for _, node in ipairs(box.inner) do
-          node:outputYourself(typesetter, line)
-        end
-
+        box:outputContent(typesetter, line)
         local x1 = typesetter.frame.state.cursorX:tonumber()
         local y1 = (SILE.documentState.paperSize[2] - typesetter.frame.state.cursorY + box.height):tonumber()
+
         SILE.outputter:endLink(dest, opts, x0, y0, x1, y1) -- Unstable API
       end
     )
diff --git a/silex/packages/rules/init.lua b/silex/packages/rules/init.lua
index 78abaf0..3b02126 100644
--- a/silex/packages/rules/init.lua
+++ b/silex/packages/rules/init.lua
@@ -156,9 +156,7 @@ function package:registerCommands ()
 
         -- Build the content.
         -- Cursor will be moved by the actual definitive size.
-        for _, node in ipairs(box.inner) do
-          node:outputYourself(typesetter, line)
-        end
+        box:outputContent(typesetter, line)
         local newX = typesetter.frame.state.cursorX
 
         -- Output a line.
@@ -180,10 +178,9 @@ function package:registerCommands ()
 
         -- Build the content.
         -- Cursor will be moved by the actual definitive size.
-        for _, node in ipairs(box.inner) do
-          node:outputYourself(typesetter, line)
-        end
+        box:outputContent(typesetter, line)
         local newX = typesetter.frame.state.cursorX
+
         -- Output a line.
         -- NOTE: The OpenType spec is not explicit regarding how the size
         -- (thickness) affects the position. We opt to distribute evenly
diff --git a/silex/typesetters/base.lua b/silex/typesetters/base.lua
index 595dbef..278fdda 100644
--- a/silex/typesetters/base.lua
+++ b/silex/typesetters/base.lua
@@ -1014,6 +1014,31 @@ function linerLeaveNode:__tostring ()
   return "-L[" .. self.name .. "]"
 end
 
+local linerBox = pl.class(SILE.nodefactory.hbox)
+function linerBox:_init (name, outputMethod)
+  SILE.nodefactory.hbox._init(self)
+  self.width = SILE.length()
+  self.height = SILE.length()
+  self.depth = SILE.length()
+  self.name = name
+  self.inner = {}
+  self.outputYourself = outputMethod
+end
+function linerBox:append (node)
+  self.inner[#self.inner+1] = node
+  self.width = self.width + node.width
+  self.height = SU.max(self.height, node.height)
+  self.depth = SU.max(self.depth, node.depth)
+end
+function linerBox:outputContent (tsetter, line)
+  for _, node in ipairs(self.inner) do
+    node.outputYourself(node, tsetter, line)
+  end
+end
+function linerBox:__tostring ()
+  return "*L[" .. self.name .. "]H<" .. tostring(self.width) .. ">^" .. tostring(self.height) .. "-" .. tostring(self.depth) .. "v"
+end
+
 --- Any unclosed liner is reopened on the current line, so we clone and repeat
 -- it.
 -- An assumption is that the inserts are done after the current slice content,
@@ -1040,43 +1065,32 @@ end
 function typesetter:reboxLiners (slice)
   local outSlice = {}
   local migratingList = {}
-  local hboxStack = {}
+  local lboxStack = {}
   for i = 1, #slice do
     local node = slice[i]
     if node.is_enter then
       SU.debug("typesetter.liner", "Start reboxing", node)
-      local n = SILE.nodefactory.hbox({
-        width = SILE.length(),
-        height = SILE.length(),
-        depth = SILE.length(),
-        name = node.name, -- For mere debug
-        inner = {},
-        outputYourself = node.outputMethod
-      })
-      hboxStack[#hboxStack+1] = n
+      local n = linerBox(node.name, node.outputMethod)
+      lboxStack[#lboxStack+1] = n
     elseif node.is_leave then
-      if #hboxStack == 0 then
+      if #lboxStack == 0 then
         SU.error("Multiliner box stacking mismatch" .. node)
-      elseif  #hboxStack == 1 then
+      elseif  #lboxStack == 1 then
         SU.debug("typesetter.liner", "End reboxing", node, "(toplevel)")
-        outSlice[#outSlice+1] = hboxStack[1]
+        outSlice[#outSlice+1] = lboxStack[1]
       else
         SU.debug("typesetter.liner", "End reboxing", node, "(nested)")
-        local hbox = hboxStack[#hboxStack - 1]
-        hbox.inner[#hbox.inner+1] = hboxStack[#hboxStack]
+        local hbox = lboxStack[#lboxStack - 1]
+        hbox.inner[#hbox.inner+1] = lboxStack[#lboxStack]
       end
-      hboxStack[#hboxStack] = nil
+      lboxStack[#lboxStack] = nil
       pl.tablex.insertvalues(outSlice, migratingList)
       migratingList = {}
     else
-      if #hboxStack > 0 then
+      if #lboxStack > 0 then
         if not node.is_migrating then
-          local hbox = hboxStack[#hboxStack]
-          -- Add node and recomputes dimensions
-          hbox.inner[#hbox.inner+1] = node
-          hbox.width = hbox.width + node.width
-          hbox.height = SU.max(hbox.height, node.height)
-          hbox.depth = SU.max(hbox.depth, node.depth)
+          local lbox = lboxStack[#lboxStack]
+          lbox:append(node)
         else
           migratingList[#migratingList+1] = node
         end
@@ -1456,8 +1470,9 @@ end
 -- into a box.
 -- These boxes will be formatted according to some output logic.
 -- The output method has the same signature as the outputYourself method
--- of a box, and is responsible for outputting the liner content, which
--- is in the "inner" field of this wrapping box.
+-- of a box, and is responsible for outputting the liner inner content with the
+-- outputContent(typesetter, line) method, possibly surrounded by some additional
+-- effects.
 -- If we are already in horizontal-restricted mode, the liner is processed
 -- immediately, since line breaking won't occur then.
 ---@param name            string    Name of the liner (usefull for debugging)
@@ -1467,13 +1482,9 @@ function typesetter:liner (name, content, outputYourself)
   if self.state.hmodeOnly then
     SU.debug("typesetter.liner", "Applying liner in horizontal-restricted mode")
     local hbox, hlist = self:makeHbox(content)
-    self:pushHbox({
-      width = hbox.width,
-      height = hbox.height,
-      depth = hbox.depth,
-      inner = { hbox },
-      outputYourself = outputYourself,
-    })
+    local lbox = linerBox(name, outputYourself)
+    lbox:append(hbox)
+    self:pushHorizontal(lbox)
     self:pushHlist(hlist)
   else
     self.state.linerCount = (self.state.linerCount or 0) + 1