• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sile-typesetter / sile / 15597603218

11 Jun 2025 11:06PM UTC coverage: 62.526% (+2.7%) from 59.841%
15597603218

push

github

alerque
chore(tooling): Copy developer tooling from develop branch

13575 of 21711 relevant lines covered (62.53%)

4250.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.05
/types/node.lua
1
--- This module defines the standard node classes used in SILE.
2
-- Some packages or other modules may define their own node classes for specific purposes,
3
-- but this does not have to be covered here.
4
--
5
-- @module SILE.types.node
6

7
-- This infinity needs to be smaller than an actual infinity but bigger than the infinite stretch
8
-- added by the typesetter. See https://github.com/sile-typesetter/sile/issues/227
9
local infinity = SILE.types.measurement(1e13)
56✔
10

11
local function _maxnode (nodes, dim)
12
   local dims = SU.map(function (node)
7,720✔
13
      -- TODO there is a bug here because we shouldn't need to cast to lengths,
14
      -- somebody is setting a height as a number value (test in Lua 5.1)
15
      -- return node[dim]
16
      return SU.cast("length", node[dim])
9,248✔
17
   end, nodes)
3,860✔
18
   return SU.max(SILE.types.length(0), pl.utils.unpack(dims))
11,580✔
19
end
20

21
local _dims = pl.Set({ "width", "height", "depth" })
56✔
22

23
--- Base abstract box class used by the other box types.
24
--
25
-- Other node classes derive from it, adding or overriding properties and methods.
26
-- It should not be used directly.
27
--
28
-- @type box
29

30
local box = pl.class()
28✔
31
box.type = "special"
28✔
32

33
box.height = nil
28✔
34
box.depth = nil
28✔
35
box.width = nil
28✔
36
box.misfit = false
28✔
37
box.explicit = false
28✔
38
box.discardable = false
28✔
39
box.value = nil
28✔
40
box._default_length = "width"
28✔
41

42
--- Constructor
43
-- @tparam table spec A table with the properties of the box.
44
function box:_init (spec)
28✔
45
   if
46
      type(spec) == "string"
6,377✔
47
      or type(spec) == "number"
6,166✔
48
      or SU.type(spec) == "measurement"
11,866✔
49
      or SU.type(spec) == "length"
11,824✔
50
   then
51
      self[self._default_length] = SU.cast("length", spec)
2,316✔
52
   elseif SU.type(spec) == "table" then
10,438✔
53
      if spec._tospec then
4,200✔
54
         spec = spec:_tospec()
×
55
      end
56
      for k, v in pairs(spec) do
22,106✔
57
         self[k] = _dims[k] and SU.cast("length", v) or v
23,183✔
58
      end
59
   elseif type(spec) ~= "nil" and SU.type(spec) ~= self.type then
1,019✔
60
      SU.error("Unimplemented, creating " .. self.type .. " node from " .. SU.type(spec), 1)
×
61
   end
62
   for dim in pairs(_dims) do
25,508✔
63
      if not self[dim] then
19,527✔
64
         self[dim] = SILE.types.length()
25,392✔
65
      end
66
   end
67
   self["is_" .. self.type] = true
6,377✔
68
   self.is_box = self.is_hbox or self.is_vbox or self.is_zerohbox or self.is_alternative or self.is_nnode
7,037✔
69
   self.is_zero = self.is_zerohbox or self.is_zerovglue
6,641✔
70
   if self.is_migrating then
6,509✔
71
      self.is_hbox, self.is_box = true, true
2✔
72
   end
73
end
74

75
-- De-init instances by shallow copying properties and removing meta table
76
function box:_tospec ()
28✔
77
   return pl.tablex.copy(self)
109✔
78
end
79

80
function box:tostring ()
28✔
81
   return self:__tostring()
×
82
end
83

84
function box:__tostring ()
28✔
85
   return self.type
×
86
end
87

88
function box.__concat (a, b)
28✔
89
   return tostring(a) .. tostring(b)
×
90
end
91

92
--- Create an absolute version of the box.
93
-- All Dimensions are based absolute (i.e. in points)
94
--
95
-- @treturn box A new box with the same properties as the original, but with absolute dimensions.
96
function box:absolute ()
28✔
97
   local clone = self._class(self:_tospec())
218✔
98
   for dim in pairs(_dims) do
436✔
99
      clone[dim] = self[dim]:absolute()
654✔
100
   end
101
   if self.nodes then
109✔
102
      clone.nodes = pl.tablex.map_named_method("absolute", self.nodes)
×
103
   end
104
   return clone
109✔
105
end
106

107
--- Returns either the width or the height of the box.
108
-- Regardless of the orientations, "width" is always in the writingDirection,
109
-- and "height" is always in the "pageDirection"
110
--
111
-- @treturn SILE.types.length  The width or height of the box, depending on the orientation.
112
function box:lineContribution ()
28✔
113
   return self.misfit and self.height or self.width
4,188✔
114
end
115

116
--- Output routine for a box.
117
-- This is an abstract method that must be overridden by subclasses.
118
--
119
function box:outputYourself (_, _)
28✔
120
   SU.error(self.type .. " with no output routine")
×
121
end
122

123
--- Returns a text description of the box for debugging purposes.
124
-- @treturn string A string representation of the box.
125
function box:toText ()
28✔
126
   return self.type
×
127
end
128

129
--- A hbox is a box node used in horizontal mode.
130
--
131
-- Derived from `box`.
132
--
133
-- Properties is_hbox and is_box are true.
134
--
135
-- @type hbox
136

137
local hbox = pl.class(box)
28✔
138
hbox.type = "hbox"
28✔
139

140
--- Constructor
141
--
142
-- @tparam table spec A table with the properties of the hbox.
143
function hbox:_init (spec)
28✔
144
   box._init(self, spec)
4,499✔
145
end
146

147
function hbox:__tostring ()
28✔
148
   return "H<" .. tostring(self.width) .. ">^" .. tostring(self.height) .. "-" .. tostring(self.depth) .. "v"
×
149
end
150

151
--- Returns the width of the hbox, scaled by the line ratio.
152
-- This is used to determine the width of the hbox when it is output.
153
--
154
-- @tparam table line The line properties (notably ratio)
155
-- @treturn SILE.types.length The scaled width of the hbox.
156
function hbox:scaledWidth (line)
28✔
157
   return SU.rationWidth(self:lineContribution(), self.width, line.ratio)
2,396✔
158
end
159

160
--- ôutput routine for a hbox.
161
--
162
-- @tparam SILE.typesetters.base typesetter The typesetter object (only used for the frame).
163
-- @tparam table line Line properties (notably ratio)
164
function hbox:outputYourself (typesetter, line)
28✔
165
   local outputWidth = self:scaledWidth(line)
1,184✔
166
   if not self.value.glyphString then
1,184✔
167
      return
415✔
168
   end
169
   if typesetter.frame:writingDirection() == "RTL" then
1,538✔
170
      typesetter.frame:advanceWritingDirection(outputWidth)
60✔
171
   end
172
   SILE.outputter:setCursor(typesetter.frame.state.cursorX, typesetter.frame.state.cursorY)
769✔
173
   SILE.outputter:setFont(self.value.options)
769✔
174
   SILE.outputter:drawHbox(self.value, outputWidth)
769✔
175
   if typesetter.frame:writingDirection() ~= "RTL" then
1,538✔
176
      typesetter.frame:advanceWritingDirection(outputWidth)
709✔
177
   end
178
end
179

180
--- A zerohbox is a special-kind of hbox with zero width, height and depth.
181
--
182
-- Derived from `hbox`.
183
--
184
-- Properties is_zerohbox (and convenience is_zero) and is_box are true.
185
-- Note that is_hbox is NOT true: zerohbox are used in a specific context
186
--
187
-- @type zerohbox
188
local zerohbox = pl.class(hbox)
28✔
189
zerohbox.type = "zerohbox"
28✔
190
zerohbox.value = { glyph = 0 }
28✔
191

192
--- A nnode is a node representing text content.
193
--
194
-- Derived from `hbox`.
195
--
196
-- Properties is_nnode and is_box are true.
197
--
198
-- @type nnode
199
local nnode = pl.class(hbox)
28✔
200
nnode.type = "nnode"
28✔
201
nnode.language = ""
28✔
202
nnode.pal = nil
28✔
203
nnode.nodes = {}
28✔
204

205
--- Constructor
206
-- @tparam table spec A table with the properties of the nnode.
207
function nnode:_init (spec)
28✔
208
   self:super(spec)
1,747✔
209
   if 0 == self.depth:tonumber() then
3,494✔
210
      self.depth = _maxnode(self.nodes, "depth")
3,494✔
211
   end
212
   if 0 == self.height:tonumber() then
3,494✔
213
      self.height = _maxnode(self.nodes, "height")
3,494✔
214
   end
215
   if 0 == self.width:tonumber() then
3,494✔
216
      self.width = SU.sum(SU.map(function (node)
5,241✔
217
         return node.width
1,615✔
218
      end, self.nodes))
3,494✔
219
   end
220
end
221

222
function nnode:__tostring ()
28✔
223
   return "N<"
×
224
      .. tostring(self.width)
×
225
      .. ">^"
×
226
      .. tostring(self.height)
×
227
      .. "-"
×
228
      .. tostring(self.depth)
×
229
      .. "v("
×
230
      .. self:toText()
×
231
      .. ")"
×
232
end
233

234
--- Create an absolute version of the box.
235
-- This overrides the base class method as nnode content is assumed to in points already.
236
--
237
-- @treturn box The box itself.
238
function nnode:absolute ()
28✔
239
   return self
×
240
end
241

242
--- Output routine for a nnode.
243
-- @tparam SILE.typesetters.base typesetter The typesetter object (only used for the frame).
244
-- @tparam table line Line properties (notably ratio)
245
function nnode:outputYourself (typesetter, line)
28✔
246
   -- See typesetter:computeLineRatio() which implements the currently rather messy
247
   -- and probably slightly dubious 'hyphenated' logic.
248
   -- Example: consider the word "out-put".
249
   -- The node queue therefore contains N(out)D(-)N(put) all pointing to the same
250
   -- parent N(output).
251
   if self.parent and not self.parent.hyphenated then
1,168✔
252
      -- When we hit N(out) and are not hyphenated, we output N(output) directly
253
      -- and mark it as used, so as to skip D(-)N(put) afterwards.
254
      -- I guess this was done to ensure proper kerning (vs. outputting each of
255
      -- the nodes separately).
256
      if not self.parent.used then
399✔
257
         self.parent:outputYourself(typesetter, line)
160✔
258
      end
259
      self.parent.used = true
399✔
260
   else
261
      -- It's possible not to have a parent, e.g. N(word) without hyphenation points.
262
      -- Either that, or we have a hyphenated parent but are in the case we are
263
      -- outputting one of the elements e.g. N(out)D(-) [line break] N(put).
264
      -- (ignoring the G(margin) nodes and potentially zerohbox nodes also on either side of the line break)
265
      for _, node in ipairs(self.nodes) do
1,538✔
266
         node:outputYourself(typesetter, line)
769✔
267
      end
268
   end
269
end
270

271
--- Returns the text content of the nnode.
272
-- Contrary to the parent class, this is the actual text content of the node,
273
-- not a text representation of the node.
274
--
275
-- @treturn string The text content of the nnode.
276
function nnode:toText ()
28✔
277
   return self.text
2✔
278
end
279

280
--- An unshaped node is a text node that has not been shaped yet.
281
--
282
-- Derived from `nnode`.
283
--
284
-- Properties is_unshaped is true.
285
-- Note that is_nnode is NOT true, as an unshaped node is not a representable node yet.
286
--
287
-- @type unshaped
288

289
local unshaped = pl.class(nnode)
28✔
290
unshaped.type = "unshaped"
28✔
291

292
--- Constructor
293
--
294
-- @tparam table spec A table with the properties of the unshaped node.
295

296
function unshaped:_init (spec)
28✔
297
   self:super(spec)
132✔
298
   self.width = nil
132✔
299
end
300

301
function unshaped:__tostring ()
28✔
302
   return "U(" .. self:toText() .. ")"
×
303
end
304

305
getmetatable(unshaped).__index = function (_, _)
28✔
306
   -- if k == "width" then SU.error("Can't get width of unshaped node", true) end
307
   -- TODO: No idea why porting to proper Penlight classes this ^^^^^^ started
308
   -- killing everything. Perhaps because this function started working and would
309
   -- actually need to return rawget(self, k) or something?
310
end
311

312
--- Shapes the text of the unshaped node.
313
-- This is done by calling the current shaper with the text and options of the node.
314
-- The result is a list of nnodes inheriting the parent of the unshaped node.
315
-- The notion of parent is used by the hyphenation logic and discretionaries.
316
--
317
-- @treturn table A list of nnodes representing the shaped text.
318
function unshaped:shape ()
28✔
319
   local node = SILE.shaper:createNnodes(self.text, self.options)
118✔
320
   for i = 1, #node do
1,441✔
321
      node[i].parent = self.parent
2,646✔
322
   end
323
   return node
118✔
324
end
325

326
--- Output routine for an unshaped node.
327
-- Unshaped nodes are not supposed to make it to the output, so this method raises an error.
328
--
329
function unshaped:outputYourself (_, _)
28✔
330
   SU.error("An unshaped node made it to output", true)
×
331
end
332

333
--- A discretionary node is a node that can be broken at a certain point.
334
-- It has optional replacement, prebreak and postbreak nodes, which must be `nnode` nodes.
335
--
336
-- Derived from `hbox`.
337
--
338
-- Properties is_discretionary is true.
339
--
340
-- @type discretionary
341
-- @usage
342
-- SILE.types.node.discretionary({ replacement = ..., prebreak =  ..., postbreak = ...})
343

344
local discretionary = pl.class(hbox)
28✔
345

346
discretionary.type = "discretionary"
28✔
347
discretionary.prebreak = {}
28✔
348
discretionary.postbreak = {}
28✔
349
discretionary.replacement = {}
28✔
350
discretionary.used = false
28✔
351

352
function discretionary:__tostring ()
28✔
353
   return "D("
×
354
      .. SU.concat(self.prebreak, "")
×
355
      .. "|"
×
356
      .. SU.concat(self.postbreak, "")
×
357
      .. "|"
×
358
      .. SU.concat(self.replacement, "")
×
359
      .. ")"
×
360
end
361

362
--- Returns a text representation of the discretionary node.
363
-- This is used for debugging purposes, returning '-' for a used discretionary and '_' otherwise.
364
--
365
-- @treturn string A string representation of the discretionary node ('-' or '_').
366
function discretionary:toText ()
28✔
367
   return self.used and "-" or "_"
×
368
end
369

370
--- Mark the discretionary node as used in prebreak context.
371
-- This is used to indicate that the discretionary node is used (i.e. the parent is hyphenated)
372
-- and the prebreak nodes should be output (typically at the end of a broken line).
373
function discretionary:markAsPrebreak ()
28✔
374
   self.used = true
11✔
375
   if self.parent then
11✔
376
      self.parent.hyphenated = true
11✔
377
   end
378
   self.is_prebreak = true
11✔
379
end
380

381
--- Clone the discretionary node for postbreak use.
382
-- This is used to create a new discretionary node that is used in postbreak context.
383
-- The discretionary must previously have been marked as used.
384
--
385
-- When breaking compound words, some languages expect the hyphen (prebreak) to be
386
-- repeated in the postbreak context, typically at the beginning of the next line.
387
--
388
-- @treturn SILE.types.node.discretionary A new discretionary node with the same properties as the original, but marked for use in postbreak context.
389
function discretionary:cloneAsPostbreak ()
28✔
390
   if not self.used then
11✔
391
      SU.error("Cannot clone a non-used discretionary (previously marked as prebreak)")
×
392
   end
393
   return SILE.types.node.discretionary({
11✔
394
      prebreak = self.prebreak,
11✔
395
      postbreak = self.postbreak,
11✔
396
      replacement = self.replacement,
11✔
397
      parent = self.parent,
11✔
398
      used = true,
399
      is_prebreak = false,
400
   })
11✔
401
end
402

403
--- Output routine for a discretionary node.
404
-- Depending on how the node was marked, it will output either the prebreak, postbreak or replacement nodes.
405
--
406
-- @tparam SILE.typesetters.base typesetter The typesetter object (only used for the frame).
407
-- @tparam table line Line properties (notably ratio)
408
function discretionary:outputYourself (typesetter, line)
28✔
409
   -- See typesetter:computeLineRatio() which implements the currently rather
410
   -- messy hyphenated checks.
411
   -- Example: consider the word "out-put-ter".
412
   -- The node queue contains N(out)D(-)N(put)D(-)N(ter) all pointing to the same
413
   -- parent N(output), and here we hit D(-)
414

415
   -- Non-hyphenated parent: when N(out) was hit, we went for outputting
416
   -- the whole parent, so all other elements must now be skipped.
417
   if self.parent and not self.parent.hyphenated then
285✔
418
      return
239✔
419
   end
420

421
   -- It's possible not to have a parent (e.g. on a discretionary directly
422
   -- added in the queue and not coming from the hyphenator logic).
423
   -- Eiher that, or we have a hyphenated parent.
424
   if self.used then
46✔
425
      -- This is the actual hyphenation point.
426
      if self.is_prebreak then
22✔
427
         for _, node in ipairs(self.prebreak) do
22✔
428
            node:outputYourself(typesetter, line)
11✔
429
         end
430
      else
431
         for _, node in ipairs(self.postbreak) do
11✔
432
            node:outputYourself(typesetter, line)
×
433
         end
434
      end
435
   else
436
      -- This is not the hyphenation point (but another discretionary in the queue)
437
      -- E.g. we were in the case where we have N(out)D(-) [line break] N(out)D(-)N(ter)
438
      -- and now hit the second D(-).
439
      -- Unused discretionaries are obviously replaced.
440
      for _, node in ipairs(self.replacement) do
25✔
441
         node:outputYourself(typesetter, line)
1✔
442
      end
443
   end
444
end
445

446
--- Returns the width of the prebreak nodes.
447
--
448
-- @treturn SILE.types.length The total width of the prebreak nodes.
449
function discretionary:prebreakWidth ()
28✔
450
   if self.prebw then
657✔
451
      return self.prebw
383✔
452
   end
453
   self.prebw = SILE.types.length()
548✔
454
   for _, node in ipairs(self.prebreak) do
548✔
455
      self.prebw:___add(node.width)
274✔
456
   end
457
   return self.prebw
274✔
458
end
459

460
--- Returns the width of the postbreak nodes.
461
--
462
-- @treturn SILE.types.length The total width of the postbreak nodes.
463
function discretionary:postbreakWidth ()
28✔
464
   if self.postbw then
81✔
465
      return self.postbw
×
466
   end
467
   self.postbw = SILE.types.length()
162✔
468
   for _, node in ipairs(self.postbreak) do
81✔
469
      self.postbw:___add(node.width)
×
470
   end
471
   return self.postbw
81✔
472
end
473

474
--- Returns the width of the replacement nodes.
475
--
476
-- @treturn SILE.types.length The total width of the replacement nodes.
477
function discretionary:replacementWidth ()
28✔
478
   if self.replacew then
382✔
479
      return self.replacew
108✔
480
   end
481
   self.replacew = SILE.types.length()
548✔
482
   for _, node in ipairs(self.replacement) do
277✔
483
      self.replacew:___add(node.width)
3✔
484
   end
485
   return self.replacew
274✔
486
end
487

488
--- Returns the height of the prebreak nodes.
489
--
490
-- @treturn SILE.types.length The total height of the prebreak nodes.
491
function discretionary:prebreakHeight ()
28✔
492
   if self.prebh then
11✔
493
      return self.prebh
×
494
   end
495
   self.prebh = _maxnode(self.prebreak, "height")
22✔
496
   return self.prebh
11✔
497
end
498

499
--- Returns the height of the postbreak nodes.
500
--
501
-- @treturn SILE.types.length The total height of the postbreak nodes.
502
function discretionary:postbreakHeight ()
28✔
503
   if self.postbh then
11✔
504
      return self.postbh
×
505
   end
506
   self.postbh = _maxnode(self.postbreak, "height")
22✔
507
   return self.postbh
11✔
508
end
509

510
--- Returns the height of the replacement nodes.
511
--
512
-- @treturn SILE.types.length The total height of the replacement nodes.
513
function discretionary:replacementHeight ()
28✔
514
   if self.replaceh then
24✔
515
      return self.replaceh
×
516
   end
517
   self.replaceh = _maxnode(self.replacement, "height")
48✔
518
   return self.replaceh
24✔
519
end
520

521
--- Returns the depth of the prebreak nodes.
522
--
523
-- @treturn SILE.types.length The total depth of the prebreak nodes.
524
function discretionary:replacementDepth ()
28✔
525
   if self.replaced then
×
526
      return self.replaced
×
527
   end
528
   self.replaced = _maxnode(self.replacement, "depth")
×
529
   return self.replaced
×
530
end
531

532
--- An alternative node is a node that can be replaced by one of its options.
533
-- Not for general use:
534
-- This solution is known to be broken, but it is not clear how to fix it.
535
--
536
-- Derived from `hbox`.
537
--
538
-- Properties is_alternative and is_box are true.
539
--
540
-- @type alternative
541

542
local alternative = pl.class(hbox)
28✔
543

544
alternative.type = "alternative"
28✔
545
alternative.options = {}
28✔
546
alternative.selected = nil
28✔
547

548
function alternative:__tostring ()
28✔
549
   return "A(" .. SU.concat(self.options, " / ") .. ")"
×
550
end
551

552
function alternative:minWidth ()
28✔
553
   local minW = function (a, b)
554
      return SU.min(a.width, b.width)
×
555
   end
556
   return pl.tablex.reduce(minW, self.options)
×
557
end
558

559
function alternative:deltas ()
28✔
560
   local minWidth = self:minWidth()
×
561
   local rv = {}
×
562
   for i = 1, #self.options do
×
563
      rv[#rv + 1] = self.options[i].width - minWidth
×
564
   end
565
   return rv
×
566
end
567

568
function alternative:outputYourself (typesetter, line)
28✔
569
   if self.selected then
×
570
      self.options[self.selected]:outputYourself(typesetter, line)
×
571
   end
572
end
573

574
--- A glue node is a node that can stretch or shrink to fill horizontal space.
575
--
576
-- Derived from `box`.
577
--
578
-- Properties is_glue is true
579
--
580
-- @type glue
581

582
local glue = pl.class(box)
28✔
583
glue.type = "glue"
28✔
584
glue.discardable = true
28✔
585

586
function glue:__tostring ()
28✔
587
   return (self.explicit and "E:" or "") .. "G<" .. tostring(self.width) .. ">"
×
588
end
589

590
function glue:toText ()
28✔
591
   return " "
×
592
end
593

594
--- Output routine for a glue node.
595
--
596
-- @tparam SILE.typesetters.base typesetter The typesetter object (only used for the frame).
597
-- @tparam table line Line properties (notably ratio)
598
function glue:outputYourself (typesetter, line)
28✔
599
   local outputWidth = SU.rationWidth(self.width:absolute(), self.width:absolute(), line.ratio)
3,273✔
600
   typesetter.frame:advanceWritingDirection(outputWidth)
1,091✔
601
end
602

603
--- A hfillglue is just a standard glue with infinite stretch.
604
-- (Convenience subclass so callers do not have to know what infinity is.)
605
--
606
-- Derived from `glue`.
607
--
608
-- @type hfillglue
609

610
local hfillglue = pl.class(glue)
28✔
611

612
--- Constructor
613
--
614
-- @tparam table spec A table with the properties of the glue.
615
function hfillglue:_init (spec)
28✔
616
   self:super(spec)
43✔
617
   self.width = SILE.types.length(self.width.length, infinity, self.width.shrink)
86✔
618
end
619

620
--- A hssglue is just a standard glue with infinite stretch and shrink.
621
-- (Convenience subclass so callers do not have to know what infinity is.)
622
--
623
-- Derived from `glue`.
624
--
625
-- @type hssglue
626

627
local hssglue = pl.class(glue)
28✔
628

629
--- Constructor
630
--
631
-- @tparam table spec A table with the properties of the glue.
632
function hssglue:_init (spec)
28✔
633
   self:super(spec)
×
634
   self.width = SILE.types.length(self.width.length, infinity, infinity)
×
635
end
636

637
--- A kern node is a node that can stretch or shrink to fill horizontal space,
638
-- It represents a non-breakable space (for the purpose of line breaking).
639
--
640
-- Derived from `glue`.
641
--
642
-- Property is_kern is true.
643
--
644
-- @type kern
645
local kern = pl.class(glue)
28✔
646
kern.type = "kern" -- Perhaps some smell here, see comment on vkern
28✔
647
kern.discardable = false
28✔
648

649
function kern:__tostring ()
28✔
650
   return "K<" .. tostring(self.width) .. ">"
×
651
end
652

653
--- A vglue node is a node that can stretch or shrink to fill vertical space.
654
--
655
-- Derived from `box`.
656
--
657
-- Property is_vglue is true.
658
--
659
-- @type vglue
660

661
local vglue = pl.class(box)
28✔
662
vglue.type = "vglue"
28✔
663
vglue.discardable = true
28✔
664
vglue._default_length = "height"
28✔
665
vglue.adjustment = nil
28✔
666

667
--- Constructor
668
--
669
-- @tparam table spec A table with the properties of the vglue.
670
function vglue:_init (spec)
28✔
671
   self.adjustment = SILE.types.measurement()
692✔
672
   self:super(spec)
346✔
673
end
674

675
function vglue:__tostring ()
28✔
676
   return (self.explicit and "E:" or "") .. "VG<" .. tostring(self.height) .. ">"
×
677
end
678

679
--- Adjust the vglue by a certain amount.
680
--
681
-- @tparam SILE.types.length adjustment The amount to adjust the vglue by.
682
function vglue:adjustGlue (adjustment)
28✔
683
   self.adjustment = adjustment
172✔
684
end
685

686
--- Output routine for a vglue.
687
--
688
-- @tparam SILE.typesetters.base typesetter The typesetter object (only used for the frame).
689
-- @tparam table line Line properties (notably height and depth)
690
function vglue:outputYourself (typesetter, line)
28✔
691
   typesetter.frame:advancePageDirection(line.height:absolute() + line.depth:absolute() + self.adjustment)
1,090✔
692
end
693

694
function vglue:unbox ()
28✔
695
   return { self }
2✔
696
end
697

698
--- A vfillglue is just a standard vglue with infinite stretch.
699
-- (Convenience subclass so callers do not have to know what infinity is.)
700
--
701
-- Derived from `vglue`.
702
-- @type vfillglue
703

704
local vfillglue = pl.class(vglue)
28✔
705

706
function vfillglue:_init (spec)
28✔
707
   self:super(spec)
42✔
708
   self.height = SILE.types.length(self.width.length, infinity, self.width.shrink)
84✔
709
end
710

711
--- A vssglue is a just standard vglue with infinite stretch and shrink.
712
-- (Convenience subclass so callers do not have to know what infinity is.)
713
--
714
-- Derived from `vglue`
715
--
716
-- @type vssglue
717
local vssglue = pl.class(vglue)
28✔
718
function vssglue:_init (spec)
28✔
719
   self:super(spec)
×
720
   self.height = SILE.types.length(self.width.length, infinity, infinity)
×
721
end
722

723
--- A zerovglue is a standard vglue with zero height and depth.
724
-- (Convenience subclass)
725
--
726
-- Derived from `vglue`.
727
--
728
-- @type zerovglue
729

730
local zerovglue = pl.class(vglue)
28✔
731

732
--- A vkern node is a node that can stretch or shrink to fill vertical space,
733
-- It represents a non-breakable space (for the purpose of page breaking).
734
--
735
-- Derived from `vglue`.
736
--
737
-- @type vkern
738
local vkern = pl.class(vglue)
28✔
739
-- FIXME TODO
740
-- Here we cannot do:
741
--   vkern.type = "vkern"
742
-- It cannot be typed as "vkern" as the pagebuilder doesn't check is_vkern.
743
-- So it's just a vglue currrenty, marked as not discardable...
744
-- But on the other hand, kern is typed "kern" and is not a glue...
745
-- Frankly, the discardable/explicit flags and the types are too
746
-- entangled and point towards a more general design issue.
747
-- N.B. this vkern node is only used in the linespacing package so far.
748
vkern.discardable = false
28✔
749

750
function vkern:__tostring ()
28✔
751
   return "VK<" .. tostring(self.height) .. ">"
×
752
end
753

754
--- A penalty node has a value which is used by the line breaking algorithm (in horizontal mode)
755
-- or the page breaking algorithm (in vertical mode), to determine where to break.
756
-- The value is expected to be a number between -10000 and 10000.
757
-- The higher the value, the less desirable it is to break at that point.
758
-- The extreme values (-10000 and 10000) are used to indicate that the break is forbidden or mandatory,
759
-- i.e. in certain way represent an infinite penalty.
760
--
761
-- Derived from `box`.
762
--
763
-- Property is_penalty is true.
764
--
765
-- @type penalty
766

767
local penalty = pl.class(box)
28✔
768
penalty.type = "penalty"
28✔
769
penalty.discardable = true
28✔
770
penalty.penalty = 0
28✔
771

772
--- Constructor
773
-- @tparam table spec A table with the properties of the penalty.
774
function penalty:_init (spec)
28✔
775
   self:super(spec)
288✔
776
   if type(spec) ~= "table" then
288✔
777
      self.penalty = SU.cast("number", spec)
462✔
778
   end
779
end
780

781
function penalty:__tostring ()
28✔
782
   return "P(" .. tostring(self.penalty) .. ")"
×
783
end
784

785
--- Output routine for a penalty.
786
-- If found in the output, penalties have no representation, so this method does nothing.
787
-- (Overriding it on some penalties may be useful for debugging purposes.)
788
--
789
function penalty:outputYourself (_, _) end
178✔
790

791
--- Returns a text representation of the penalty node.
792
-- This is used for debugging purposes, returning '(!)' for a penalty.
793
--
794
-- @treturn string A string representation of the penalty node ('(!)').
795
function penalty:toText ()
28✔
796
   return "(!)"
×
797
end
798

799
--- Unbox a penalty.
800
-- This method exists consistency with vbox-derived classes, for a penalty used in vertical mode.
801
--
802
-- @treturn table A table with the penalty node.
803
function penalty:unbox ()
28✔
804
   return { self }
×
805
end
806

807
--- A vbox is a box node used in vertical mode.
808
--
809
-- Derived from `box`.
810
--
811
-- Properties is_vbox and is_box are true.
812
--
813
-- @type vbox
814

815
local vbox = pl.class(box)
28✔
816
vbox.type = "vbox"
28✔
817
vbox.nodes = {}
28✔
818
vbox._default_length = "height"
28✔
819

820
--- Constructor
821
--
822
-- @tparam table spec A table with the properties of the vbox.
823
function vbox:_init (spec)
28✔
824
   self.nodes = {}
160✔
825
   self:super(spec)
160✔
826
   self.depth = _maxnode(self.nodes, "depth")
320✔
827
   self.height = _maxnode(self.nodes, "height")
320✔
828
end
829

830
function vbox:__tostring ()
28✔
831
   return "VB<" .. tostring(self.height) .. "|" .. self:toText() .. "v" .. tostring(self.depth) .. ")"
×
832
end
833

834
--- Returns a text representation of the vbox.
835
-- This is used for debugging purposes, returning a string representation of the vbox and its content.
836
--
837
-- @treturn string A string representation of the vbox.
838
function vbox:toText ()
28✔
839
   return "VB["
×
840
      .. SU.concat(
×
841
         SU.map(function (node)
×
842
            return node:toText()
×
843
         end, self.nodes),
×
844
         ""
845
      )
846
      .. "]"
×
847
end
848

849
--- Output routine for a vbox.
850
--
851
-- @tparam SILE.typesetters.base typesetter The typesetter object (only used for the frame).
852
-- @tparam table line Line properties (notably ratio)
853
function vbox:outputYourself (typesetter, line)
28✔
854
   typesetter.frame:advancePageDirection(self.height)
152✔
855
   local initial = true
152✔
856
   for _, node in ipairs(self.nodes) do
3,086✔
857
      if not (initial and (node.is_glue or node.is_penalty)) then
2,934✔
858
         initial = false
2,934✔
859
         node:outputYourself(typesetter, line)
2,934✔
860
      end
861
   end
862
   typesetter.frame:advancePageDirection(self.depth)
152✔
863
   typesetter.frame:newLine()
152✔
864
end
865

866
--- Unbox a vbox.
867
--
868
-- @treturn table A table with the nodes inside the vbox.
869
function vbox:unbox ()
28✔
870
   for i = 1, #self.nodes do
3✔
871
      if self.nodes[i].is_vbox or self.nodes[i].is_vglue then
3✔
872
         return self.nodes
3✔
873
      end
874
   end
875
   return { self }
×
876
end
877

878
--- Added a box or several to the current vbox.
879
-- The box height is the height is the total height of the content, minus the depth of the last box.
880
-- The depth of the vbox is the depth of the last box.
881
--
882
-- @tparam box|table box A box or a list of boxes to add to the vbox.
883
function vbox:append (node)
28✔
884
   if not node then
7✔
885
      SU.error("nil box given", true)
×
886
   end
887
   local nodes = node.type and node:unbox() or node
11✔
888
   self.height = self.height:absolute()
14✔
889
   self.height:___add(self.depth)
7✔
890
   local lastdepth = SILE.types.length()
7✔
891
   for i = 1, #nodes do
187✔
892
      table.insert(self.nodes, nodes[i])
180✔
893
      self.height:___add(nodes[i].height)
180✔
894
      self.height:___add(nodes[i].depth:absolute())
360✔
895
      if nodes[i].is_vbox then
180✔
896
         lastdepth = nodes[i].depth
78✔
897
      end
898
   end
899
   self.height:___sub(lastdepth)
7✔
900
   self.ratio = 1
7✔
901
   self.depth = lastdepth
7✔
902
end
903

904
--- A migrating node is a node that can be moved from one frame to another.
905
-- Typically, footnotes are migrating nodes.
906
--
907
-- Derived from `hbox`.
908
--
909
-- Properties ìs_migrating, is_hbox and is_box are true.
910
--
911
-- @type migrating
912
-- @usage
913
-- SILE.types.node.migrating({ material =  ... })
914
--
915

916
local migrating = pl.class(hbox)
28✔
917
migrating.type = "migrating"
28✔
918
migrating.material = {}
28✔
919
migrating.value = {}
28✔
920
migrating.nodes = {}
28✔
921

922
function migrating:__tostring ()
28✔
923
   return "<M: " .. tostring(self.material) .. ">"
×
924
end
925

926
-- DEPRECATED FUNCTIONS
927

928
local function _deprecated_isX ()
929
   SU.deprecated("node:isX()", "is_X", "0.10.0", "0.16.0")
×
930
end
931

932
box.isBox = _deprecated_isX
28✔
933
box.isNnode = _deprecated_isX
28✔
934
box.isGlue = _deprecated_isX
28✔
935
box.isVglue = _deprecated_isX
28✔
936
box.isZero = _deprecated_isX
28✔
937
box.isUnshaped = _deprecated_isX
28✔
938
box.isAlternative = _deprecated_isX
28✔
939
box.isVbox = _deprecated_isX
28✔
940
box.isInsertion = _deprecated_isX
28✔
941
box.isMigrating = _deprecated_isX
28✔
942
box.isPenalty = _deprecated_isX
28✔
943
box.isDiscretionary = _deprecated_isX
28✔
944
box.isKern = _deprecated_isX
28✔
945

946
local _deprecated_nodefactory = {
28✔
947
   newHbox = true,
948
   newNnode = true,
949
   newUnshaped = true,
950
   newDisc = true,
951
   disc = true,
952
   newAlternative = true,
953
   newGlue = true,
954
   newKern = true,
955
   newVglue = true,
956
   newVKern = true,
957
   newPenalty = true,
958
   newDiscretionary = true,
959
   newVbox = true,
960
   newMigrating = true,
961
   zeroGlue = true,
962
   hfillGlue = true,
963
   vfillGlue = true,
964
   hssGlue = true,
965
   vssGlue = true,
966
   zeroHbox = true,
967
   zeroVglue = true,
968
}
969

970
-- EXPORTS WRAP-UP
971

972
local nodetypes = {
28✔
973
   box = box,
28✔
974
   hbox = hbox,
28✔
975
   zerohbox = zerohbox,
28✔
976
   nnode = nnode,
28✔
977
   unshaped = unshaped,
28✔
978
   discretionary = discretionary,
28✔
979
   alternative = alternative,
28✔
980
   glue = glue,
28✔
981
   hfillglue = hfillglue,
28✔
982
   hssglue = hssglue,
28✔
983
   kern = kern,
28✔
984
   vglue = vglue,
28✔
985
   vfillglue = vfillglue,
28✔
986
   vssglue = vssglue,
28✔
987
   zerovglue = zerovglue,
28✔
988
   vkern = vkern,
28✔
989
   penalty = penalty,
28✔
990
   vbox = vbox,
28✔
991
   migrating = migrating,
28✔
992
}
993

994
setmetatable(nodetypes, {
56✔
995
   __index = function (_, prop)
996
      if _deprecated_nodefactory[prop] then
×
997
         SU.deprecated(
×
998
            "SILE.types.node." .. prop,
×
999
            "SILE.types.node." .. prop:match("n?e?w?(.*)"):lower(),
×
1000
            "0.10.0",
1001
            "0.14.0"
1002
         )
1003
      elseif type(prop) == "number" then -- luacheck: ignore 542
×
1004
      -- Likely an attempt to iterate, inspect, or dump the table, sort of safe to ignore
1005
      else
1006
         SU.error("Attempt to access non-existent SILE.types.node." .. prop)
×
1007
      end
1008
   end,
1009
})
1010

1011
return nodetypes
28✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc