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

sile-typesetter / sile / 14908152066

08 May 2025 01:49PM UTC coverage: 61.309% (-5.7%) from 67.057%
14908152066

push

github

alerque
chore(registries): Touchup command registry deprecation shims

1 of 2 new or added lines in 2 files covered. (50.0%)

1379 existing lines in 46 files now uncovered.

13556 of 22111 relevant lines covered (61.31%)

11834.23 hits per line

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

71.96
/linebreakers/base.lua
1
--- SILE linebreaker class.
2
-- @interfaces linebreakers
3

4
local module = require("types.module")
42✔
5
local linebreaker = pl.class(module)
42✔
6
linebreaker.type = "linebreaker"
42✔
7

8
function linebreaker:_init (typesetter)
42✔
9
   self.typesetter = typesetter
81✔
10
   module._init(self)
81✔
11
   self.classes = { "tight", "decent", "loose", "veryLoose" }
81✔
12
   self.passSerial = 0
81✔
13
   self.awful_bad = 1073741823
81✔
14
   self.inf_bad = 10000
81✔
15
   self.ejectPenalty = -self.inf_bad
81✔
16
end
17

18
function linebreaker:_declareSettings ()
42✔
19
   self.settings:declare({
84✔
20
      parameter = "linebreak.parShape",
21
      type = "boolean",
22
      default = false,
23
      help = "If set to true, the paragraph shaping method is activated.",
24
   })
25
   self.settings:declare({ parameter = "linebreak.tolerance", type = "integer or nil", default = 500 })
84✔
26
   self.settings:declare({ parameter = "linebreak.pretolerance", type = "integer or nil", default = 100 })
84✔
27
   self.settings:declare({ parameter = "linebreak.hangIndent", type = "measurement", default = 0 })
84✔
28
   self.settings:declare({ parameter = "linebreak.hangAfter", type = "integer or nil", default = nil })
84✔
29
   self.settings:declare({
84✔
30
      parameter = "linebreak.adjdemerits",
31
      type = "integer",
32
      default = 10000,
33
      help = "Additional demerits which are accumulated in the course of paragraph building when two consecutive lines are visually incompatible. In these cases, one line is built with much space for justification, and the other one with little space.",
34
   })
35
   self.settings:declare({ parameter = "linebreak.looseness", type = "integer", default = 0 })
84✔
36
   self.settings:declare({ parameter = "linebreak.prevGraf", type = "integer", default = 0 })
84✔
37
   self.settings:declare({ parameter = "linebreak.emergencyStretch", type = "measurement", default = 0 })
84✔
38
   self.settings:declare({ parameter = "linebreak.doLastLineFit", type = "boolean", default = false }) -- unimplemented
84✔
39
   self.settings:declare({ parameter = "linebreak.linePenalty", type = "integer", default = 10 })
84✔
40
   self.settings:declare({ parameter = "linebreak.hyphenPenalty", type = "integer", default = 50 })
84✔
41
   self.settings:declare({ parameter = "linebreak.doubleHyphenDemerits", type = "integer", default = 10000 })
84✔
42
   self.settings:declare({ parameter = "linebreak.finalHyphenDemerits", type = "integer", default = 5000 })
84✔
43
end
44

45
--[[
46
  Basic control flow:
47
  doBreak:
48
    init
49
    for each node:
50
      checkForLegalBreak
51
        tryBreak
52
          createNewActiveNodes
53
          considerDemerits
54
            deactivateR (or) recordFeasible
55
    tryFinalBreak
56
    postLineBreak
57
]]
58

59
function linebreaker:_param (key)
42✔
60
   local value = self.settings:get("linebreak." .. key)
8,428✔
61
   return type(value) == "table" and value:absolute() or value
4,511✔
62
end
63

64
-- Routines here will be called thousands of times; we micro-optimize
65
-- to avoid debugging and concat calls.
66
local debugging = false
42✔
67

68
function linebreaker:init ()
42✔
69
   self:trimGlue() -- 842
285✔
70
   -- 849
71
   self.activeWidth = SILE.types.length()
570✔
72
   self.curActiveWidth = SILE.types.length()
570✔
73
   self.breakWidth = SILE.types.length()
570✔
74
   -- 853
75
   local rskip = (self.settings:get("document.rskip") or SILE.types.node.glue()).width:absolute()
1,020✔
76
   local lskip = (self.settings:get("document.lskip") or SILE.types.node.glue()).width:absolute()
1,021✔
77
   self.background = rskip + lskip
570✔
78
   -- 860
79
   self.bestInClass = {}
285✔
80
   for i = 1, #self.classes do
1,425✔
81
      self.bestInClass[self.classes[i]] = {
1,140✔
82
         minimalDemerits = self.awful_bad,
1,140✔
83
      }
1,140✔
84
   end
85
   self.minimumDemerits = self.awful_bad
285✔
86
   self:setupLineLengths()
285✔
87
end
88

89
function linebreaker:trimGlue () -- 842
42✔
90
   local nodes = self.nodes
285✔
91
   if nodes[#nodes].is_glue then
285✔
92
      nodes[#nodes] = nil
×
93
   end
94
   nodes[#nodes + 1] = SILE.types.node.penalty(self.inf_bad)
570✔
95
end
96

97
-- NOTE FOR DEVELOPERS: this method is called when the linebreak.parShape
98
-- setting is true. The arguments passed are self (the linebreaker instance)
99
-- and a counter representing the current line number.
100
--
101
-- The default implementation does nothing but waste a function call, resulting
102
-- in normal paragraph shapes. Extended paragraph shapes are intended to be
103
-- provided by overriding this method.
104
--
105
-- The expected return is three values, any of which may be nil to use default
106
-- values or a measurement to override the defaults. The values are considered
107
-- as left, width, and right respectively.
108
--
109
-- Since self.hsize holds the current line width, these three values should add
110
-- up to the that total. Returning values that don't add up may produce
111
-- unexpected results.
112
--
113
-- TeX wizards shall also note that this is slightly different from
114
-- Knuth's definition "nline l1 i1 l2 i2 ... lN iN".
115
function linebreaker:parShape (_)
42✔
116
   return 0, self.hsize, 0
×
117
end
118

119
local parShapeCache = {}
42✔
120

121
local grantLeftoverWidth = function (hsize, l, w, r)
122
   local width = SILE.types.measurement(w or hsize)
141✔
123
   if not w and l then
141✔
124
      width = width - SILE.types.measurement(l)
36✔
125
   end
126
   if not w and r then
141✔
127
      width = width - SILE.types.measurement(r)
36✔
128
   end
129
   local remaining = hsize:tonumber() - width:tonumber()
423✔
130
   local left = SU.cast("number", l or (r and (remaining - SU.cast("number", r))) or 0)
149✔
131
   local right = SU.cast("number", r or (l and (remaining - SU.cast("number", l))) or remaining)
177✔
132
   return left, width, right
141✔
133
end
134

135
-- Wrap linebreak:parShape in a memoized table for fast access
136
function linebreaker:parShapeCache (n)
42✔
137
   local cache = parShapeCache[n]
141✔
138
   if not cache then
141✔
139
      local l, w, r = self:parShape(n)
141✔
140
      local left, width, right = grantLeftoverWidth(self.hsize, l, w, r)
141✔
141
      cache = { left, width, right }
141✔
142
   end
143
   return cache[1], cache[2], cache[3]
141✔
144
end
145

146
function linebreaker:parShapeCacheClear ()
42✔
147
   pl.tablex.clear(parShapeCache)
285✔
148
end
149

150
function linebreaker:setupLineLengths () -- 874
42✔
151
   self.parShaping = self:_param("parShape") or false
570✔
152
   if self.parShaping then
285✔
153
      self.lastSpecialLine = nil
2✔
154
      self.easy_line = nil
2✔
155
   else
156
      self.hangAfter = self:_param("hangAfter") or 0
566✔
157
      self.hangIndent = self:_param("hangIndent"):tonumber()
849✔
158
      if self.hangIndent == 0 then
283✔
159
         self.lastSpecialLine = 0
283✔
160
         self.secondWidth = self.hsize or SU.error("No hsize")
283✔
161
      else -- 875
UNCOV
162
         self.lastSpecialLine = math.abs(self.hangAfter)
×
UNCOV
163
         if self.hangAfter < 0 then
×
UNCOV
164
            self.secondWidth = self.hsize or SU.error("No hsize")
×
UNCOV
165
            self.firstWidth = self.hsize - math.abs(self.hangIndent)
×
166
         else
UNCOV
167
            self.firstWidth = self.hsize or SU.error("No hsize")
×
UNCOV
168
            self.secondWidth = self.hsize - math.abs(self.hangIndent)
×
169
         end
170
      end
171
      if self:_param("looseness") == 0 then
566✔
172
         self.easy_line = self.lastSpecialLine
283✔
173
      else
174
         self.easy_line = self.awful_bad
×
175
      end
176
      -- self.easy_line = self.awful_bad
177
   end
178
end
179

180
function linebreaker:tryBreak () -- 855
42✔
181
   local pi, breakType
182
   local node = self.nodes[self.place]
3,415✔
183
   if not node then
3,415✔
184
      pi = self.ejectPenalty
×
185
      breakType = "hyphenated"
×
186
   elseif node.is_discretionary then
3,415✔
187
      breakType = "hyphenated"
536✔
188
      pi = self:_param("hyphenPenalty")
1,072✔
189
   else
190
      breakType = "unhyphenated"
2,879✔
191
      pi = node.penalty or 0
2,879✔
192
   end
193
   if debugging then
3,415✔
194
      SU.debug("break", "Trying a", breakType, "break p =", pi)
×
195
   end
196
   self.no_break_yet = true -- We have to store all this state crap in the object, or it's global variables all the way
3,415✔
197
   self.prev_prev_r = nil
3,415✔
198
   self.prev_r = self.activeListHead
3,415✔
199
   self.old_l = 0
3,415✔
200
   self.r = nil
3,415✔
201
   self.curActiveWidth = SILE.types.length(self.activeWidth)
6,830✔
202
   while true do
203
      while true do -- allows "break" to function as "continue"
204
         self.r = self.prev_r.next
12,221✔
205
         if debugging then
12,221✔
206
            SU.debug(
×
207
               "break",
208
               "We have moved the link  forward, ln is now",
209
               self.r.type == "delta" and "XX" or self.r.lineNumber
×
210
            )
211
         end
212
         if self.r.type == "delta" then -- 858
12,221✔
213
            if debugging then
2,364✔
214
               SU.debug("break", " Adding delta node width of", self.r.width)
×
215
            end
216
            self.curActiveWidth:___add(self.r.width)
2,364✔
217
            self.prev_prev_r = self.prev_r
2,364✔
218
            self.prev_r = self.r
2,364✔
219
            break
2,364✔
220
         end
221
         -- 861
222
         if self.r.lineNumber > self.old_l then
9,857✔
223
            if debugging then
6,840✔
224
               SU.debug("break", "Minimum demerits =", self.minimumDemerits)
×
225
            end
226
            if
227
               self.minimumDemerits < self.awful_bad and (self.old_l ~= self.easy_line or self.r == self.activeListHead)
6,840✔
228
            then
229
               self:createNewActiveNodes(breakType)
858✔
230
            end
231
            if self.r == self.activeListHead then
6,840✔
232
               if debugging then
3,415✔
233
                  SU.debug("break", "<- tryBreak")
×
234
               end
235
               return
3,415✔
236
            end
237
            -- 876
238
            if self.easy_line and self.r.lineNumber > self.easy_line then
3,425✔
239
               self.lineWidth = self.secondWidth
3,299✔
240
               self.old_l = self.awful_bad - 1
3,299✔
241
            else
242
               self.old_l = self.r.lineNumber
126✔
243
               if self.lastSpecialLine and self.r.lineNumber > self.lastSpecialLine then
126✔
244
                  self.lineWidth = self.secondWidth
×
245
               elseif self.parShaping then
126✔
246
                  local _
247
                  _, self.lineWidth, _ = self:parShapeCache(self.r.lineNumber)
252✔
248
               else
UNCOV
249
                  self.lineWidth = self.firstWidth
×
250
               end
251
            end
252
            if debugging then
3,425✔
253
               SU.debug("break", "line width =", self.lineWidth)
×
254
            end
255
         end
256
         if debugging then
6,442✔
257
            SU.debug("break", " ---> (2) cuaw is", self.curActiveWidth)
×
258
            SU.debug("break", " ---> aw is", self.activeWidth)
×
259
         end
260
         self:considerDemerits(pi, breakType)
6,442✔
261
         if debugging then
6,442✔
262
            SU.debug("break", " <--- cuaw is", self.curActiveWidth)
×
263
            SU.debug("break", " <--- aw is ", self.activeWidth)
×
264
         end
265
      end
266
   end
267
end
268

269
-- Note: This function gets called a lot and to optimize it we're assuming that
270
-- the lengths being passed are already absolutized. This is not a safe
271
-- assumption to make universally.
272
local function fitclass (self, shortfall)
273
   shortfall = shortfall.amount
6,442✔
274
   local badness, class
275
   local stretch = self.curActiveWidth.stretch.amount
6,442✔
276
   local shrink = self.curActiveWidth.shrink.amount
6,442✔
277
   if shortfall > 0 then
6,442✔
278
      if shortfall > 110 and stretch < 25 then
6,048✔
279
         badness = self.inf_bad
3,169✔
280
      else
281
         badness = SU.rateBadness(self.inf_bad, shortfall, stretch)
5,758✔
282
      end
283
      if badness > 99 then
6,048✔
284
         class = "veryLoose"
4,968✔
285
      elseif badness > 12 then
1,080✔
286
         class = "loose"
69✔
287
      else
288
         class = "decent"
1,011✔
289
      end
290
   else
291
      shortfall = -shortfall
394✔
292
      if shortfall > shrink then
394✔
293
         badness = self.inf_bad + 1
272✔
294
      else
295
         badness = SU.rateBadness(self.inf_bad, shortfall, shrink)
244✔
296
      end
297
      if badness > 12 then
394✔
298
         class = "tight"
333✔
299
      else
300
         class = "decent"
61✔
301
      end
302
   end
303
   return badness, class
6,442✔
304
end
305

306
function linebreaker:tryAlternatives (from, to)
42✔
307
   local altSizes = {}
×
308
   local alternates = {}
×
309
   for i = from, to do
×
310
      if self.nodes[i] and self.nodes[i].is_alternative then
×
311
         alternates[#alternates + 1] = self.nodes[i]
×
312
         altSizes[#altSizes + 1] = #self.nodes[i].options
×
313
      end
314
   end
315
   if #alternates == 0 then
×
316
      return
×
317
   end
318
   local localMinimum = self.awful_bad
×
319
   -- local selectedShortfall
320
   local shortfall = self.lineWidth - self.curActiveWidth
×
321
   if debugging then
×
322
      SU.debug("break", "Shortfall was ", shortfall)
×
323
   end
324
   for combination in SU.allCombinations(altSizes) do
×
325
      local addWidth = 0
×
326
      for i = 1, #alternates do
×
327
         local alternative = alternates[i]
×
328
         addWidth = (addWidth + alternative.options[combination[i]].width - alternative:minWidth())
×
329
         if debugging then
×
330
            SU.debug("break", alternative.options[combination[i]], " width", addWidth)
×
331
         end
332
      end
333
      local ss = shortfall - addWidth
×
334
      -- Warning, assumes abosolute
335
      local badness = SU.rateBadness(
×
336
         self.inf_bad,
×
337
         ss.length.amount,
×
338
         self.curActiveWidth[ss > 0 and "stretch" or "shrink"].length.amount
×
339
      )
340
      if debugging then
×
341
         SU.debug("break", "  badness of", ss, "(", self.curActiveWidth, ") is", badness)
×
342
      end
343
      if badness < localMinimum then
×
344
         self.r.alternates = alternates
×
345
         self.r.altSelections = combination
×
346
         -- selectedShortfall = addWidth
347
         localMinimum = badness
×
348
      end
349
   end
350
   if debugging then
×
351
      SU.debug("break", "Choosing ", alternates[1].options[self.r.altSelections[1]])
×
352
   end
353
   -- self.curActiveWidth:___add(selectedShortfall)
354
   shortfall = self.lineWidth - self.curActiveWidth
×
355
   if debugging then
×
356
      SU.debug("break", "Is now ", shortfall)
×
357
   end
358
end
359

360
function linebreaker:considerDemerits (pi, breakType) -- 877
42✔
361
   self.artificialDemerits = false
6,442✔
362
   local nodeStaysActive = false
6,442✔
363
   -- self:dumpActiveRing()
364
   if self.seenAlternatives then
6,442✔
365
      self:tryAlternatives(
×
366
         self.r.prevBreak and self.r.prevBreak.curBreak or 1,
×
367
         self.r.curBreak and self.r.curBreak or 1
×
368
      )
369
   end
370
   local shortfall = self.lineWidth - self.curActiveWidth
6,442✔
371
   self.badness, self.fitClass = fitclass(self, shortfall)
12,884✔
372
   if debugging then
6,442✔
373
      SU.debug("break", self.badness, self.fitClass)
×
374
   end
375
   if self.badness > self.inf_bad or pi == self.ejectPenalty then
6,442✔
376
      if
377
         self.finalpass
378
         and self.minimumDemerits == self.awful_bad
811✔
379
         and self.r.next == self.activeListHead
48✔
380
         and self.prev_r == self.activeListHead
29✔
381
      then
382
         self.artificialDemerits = true
29✔
383
      else
384
         if self.badness > self.threshold then
782✔
385
            self:deactivateR()
262✔
386
            return
262✔
387
         end
388
      end
389
   else
390
      self.prev_r = self.r
5,631✔
391
      if self.badness > self.threshold then
5,631✔
392
         return
4,887✔
393
      end
394
      nodeStaysActive = true
744✔
395
   end
396

397
   local _shortfall = shortfall:tonumber()
1,293✔
398
   local function shortfallratio (metric)
399
      local prop = self.curActiveWidth[metric]:tonumber()
1,293✔
400
      local factor = prop ~= 0 and prop or self.awful_bad
1,293✔
401
      return _shortfall / factor
1,293✔
402
   end
403
   self.lastRatio = shortfallratio(_shortfall > 0 and "stretch" or "shrink")
2,586✔
404
   self:recordFeasible(pi, breakType)
1,293✔
405
   if not nodeStaysActive then
1,293✔
406
      self:deactivateR()
549✔
407
   end
408
end
409

410
function linebreaker:deactivateR () -- 886
42✔
411
   if debugging then
811✔
412
      SU.debug("break", " Deactivating r (" .. self.r.type .. ")")
×
413
   end
414
   self.prev_r.next = self.r.next
811✔
415
   if self.prev_r == self.activeListHead then
811✔
416
      -- 887
417
      self.r = self.activeListHead.next
811✔
418
      if self.r.type == "delta" then
811✔
419
         self.activeWidth:___add(self.r.width)
451✔
420
         self.curActiveWidth = SILE.types.length(self.activeWidth)
902✔
421
         self.activeListHead.next = self.r.next
451✔
422
      end
423
      if debugging then
811✔
424
         SU.debug("break", "  Deactivate, branch 1")
×
425
      end
426
   else
UNCOV
427
      if self.prev_r.type == "delta" then
×
UNCOV
428
         self.r = self.prev_r.next
×
UNCOV
429
         if self.r == self.activeListHead then
×
430
            self.curActiveWidth:___sub(self.prev_r.width)
×
431
            -- FIXME It was crashing here, so changed from:
432
            -- self.curActiveWidth:___sub(self.r.width)
433
            -- But I'm not so sure reading Knuth here...
434
            self.prev_prev_r.next = self.activeListHead
×
435
            self.prev_r = self.prev_prev_r
×
UNCOV
436
         elseif self.r.type == "delta" then
×
UNCOV
437
            self.curActiveWidth:___add(self.r.width)
×
UNCOV
438
            self.prev_r.width:___add(self.r.width)
×
UNCOV
439
            self.prev_r.next = self.r.next
×
440
         end
441
      end
UNCOV
442
      if debugging then
×
443
         SU.debug("break", "  Deactivate, branch 2")
×
444
      end
445
   end
446
end
447

448
function linebreaker:computeDemerits (pi, breakType)
42✔
449
   if self.artificialDemerits then
1,293✔
450
      return 0
29✔
451
   end
452
   local demerit = self:_param("linePenalty") + self.badness
2,528✔
453
   if math.abs(demerit) >= 10000 then
1,264✔
454
      demerit = 100000000
×
455
   else
456
      demerit = demerit * demerit
1,264✔
457
   end
458
   if pi > 0 then
1,264✔
459
      demerit = demerit + pi * pi
233✔
460
   -- elseif pi == 0 then
461
   --   -- do nothing
462
   elseif pi > self.ejectPenalty then
1,031✔
463
      demerit = demerit - pi * pi
511✔
464
   end
465
   if breakType == "hyphenated" and self.r.type == "hyphenated" then
1,264✔
466
      if self.nodes[self.place] then
66✔
467
         demerit = demerit + self:_param("doubleHyphenDemerits")
132✔
468
      else
469
         demerit = demerit + self:_param("finalHyphenDemerits")
×
470
      end
471
   end
472
   -- XXX adjDemerits not added here
473
   return demerit
1,264✔
474
end
475

476
function linebreaker:recordFeasible (pi, breakType) -- 881
42✔
477
   local demerit = self:computeDemerits(pi, breakType)
1,293✔
478
   if debugging then
1,293✔
479
      if self.nodes[self.place] then
×
480
         SU.debug(
×
481
            "break",
482
            "@",
483
            self.nodes[self.place],
×
484
            "via @@",
485
            (self.r.serial or "0"),
×
486
            "badness =",
487
            self.badness,
×
488
            "demerit =",
489
            demerit
490
         ) -- 882
491
      else
492
         SU.debug("break", "@ \\par via @@")
×
493
      end
494
      SU.debug("break", " fit class =", self.fitClass)
×
495
   end
496
   demerit = demerit + self.r.totalDemerits
1,293✔
497
   if demerit <= self.bestInClass[self.fitClass].minimalDemerits then
1,293✔
498
      self.bestInClass[self.fitClass] = {
976✔
499
         minimalDemerits = demerit,
976✔
500
         node = self.r.serial and self.r,
976✔
501
         line = self.r.lineNumber,
976✔
502
      }
976✔
503
      -- XXX do last line fit
504
      if demerit < self.minimumDemerits then
976✔
505
         self.minimumDemerits = demerit
933✔
506
      end
507
   end
508
end
509

510
function linebreaker:createNewActiveNodes (breakType) -- 862
42✔
511
   if self.no_break_yet then
858✔
512
      -- 863
513
      self.no_break_yet = false
858✔
514
      self.breakWidth = SILE.types.length(self.background)
1,716✔
515
      local place = self.place
858✔
516
      local node = self.nodes[place]
858✔
517
      if node and node.is_discretionary then -- 866
858✔
518
         self.breakWidth:___add(node:prebreakWidth())
182✔
519
         self.breakWidth:___add(node:postbreakWidth())
182✔
520
         self.breakWidth:___sub(node:replacementWidth())
182✔
521
      end
522
      while self.nodes[place] and not self.nodes[place].is_box do
2,275✔
523
         if self.sideways and self.nodes[place].height then
1,417✔
524
            self.breakWidth:___sub(self.nodes[place].height)
×
525
            self.breakWidth:___sub(self.nodes[place].depth)
×
526
         elseif self.nodes[place].width then -- We use the fact that (a) nodes know if they have width and (b) width subtraction is polymorphic
1,417✔
527
            self.breakWidth:___sub(self.nodes[place]:lineContribution())
2,834✔
528
         end
529
         place = place + 1
1,417✔
530
      end
531
      if debugging then
858✔
532
         SU.debug("break", "Value of breakWidth =", self.breakWidth)
×
533
      end
534
   end
535
   -- 869 (Add a new delta node)
536
   if self.prev_r.type == "delta" then
858✔
537
      self.prev_r.width:___sub(self.curActiveWidth)
3✔
538
      self.prev_r.width:___add(self.breakWidth)
6✔
539
   elseif self.prev_r == self.activeListHead then
855✔
540
      self.activeWidth = SILE.types.length(self.breakWidth)
616✔
541
   else
542
      local newDelta = { next = self.r, type = "delta", width = self.breakWidth - self.curActiveWidth }
1,094✔
543
      if debugging then
547✔
544
         SU.debug("break", "Added new delta node =", newDelta.width)
×
545
      end
546
      self.prev_r.next = newDelta
547✔
547
      self.prev_prev_r = self.prev_r
547✔
548
      self.prev_r = newDelta
547✔
549
   end
550
   if math.abs(self.adjdemerits) >= (self.awful_bad - self.minimumDemerits) then
858✔
551
      self.minimumDemerits = self.awful_bad - 1
×
552
   else
553
      self.minimumDemerits = self.minimumDemerits + math.abs(self.adjdemerits)
858✔
554
   end
555

556
   for i = 1, #self.classes do
4,290✔
557
      local class = self.classes[i]
3,432✔
558
      local best = self.bestInClass[class]
3,432✔
559
      local value = best.minimalDemerits
3,432✔
560
      if debugging then
3,432✔
561
         SU.debug("break", "Class is", class, "Best value here is", value)
×
562
      end
563

564
      if value <= self.minimumDemerits then
3,432✔
565
         -- 871: this is what creates new active notes
566
         self.passSerial = self.passSerial + 1
873✔
567

568
         local newActive = {
873✔
569
            type = breakType,
873✔
570
            next = self.r,
873✔
571
            curBreak = self.place,
873✔
572
            prevBreak = best.node,
873✔
573
            serial = self.passSerial,
873✔
574
            ratio = self.lastRatio,
873✔
575
            lineNumber = best.line + 1,
873✔
576
            fitness = class,
873✔
577
            totalDemerits = value,
873✔
578
         }
579
         -- DoLastLineFit? 1636 XXX
580
         self.prev_r.next = newActive
873✔
581
         self.prev_r = newActive
873✔
582
         self:dumpBreakNode(newActive)
873✔
583
      end
584
      self.bestInClass[class] = { minimalDemerits = self.awful_bad }
3,432✔
585
   end
586

587
   self.minimumDemerits = self.awful_bad
858✔
588
   -- 870
589
   if self.r ~= self.activeListHead then
858✔
590
      local newDelta = { next = self.r, type = "delta", width = self.curActiveWidth - self.breakWidth }
6✔
591
      self.prev_r.next = newDelta
3✔
592
      self.prev_prev_r = self.prev_r
3✔
593
      self.prev_r = newDelta
3✔
594
   end
595
end
596

597
function linebreaker:dumpBreakNode (node)
42✔
598
   if not SU.debugging("break") then
1,746✔
599
      return
873✔
600
   end
601
   SU.debug("break", self:describeBreakNode(node))
×
602
end
603

604
function linebreaker:describeBreakNode (node)
42✔
605
   --SU.debug("break", "@@", b.serial, ": line", b.lineNumber - 1, ".", b.fitness, b.type, "t=", b.totalDemerits, "-> @@", b.prevBreak and b.prevBreak.serial or "0")
606
   if node.sentinel then
×
607
      return node.sentinel
×
608
   end
609
   if node.type == "delta" then
×
610
      return "delta " .. node.width .. "pt"
×
611
   end
612
   local before = self.nodes[node.curBreak - 1]
×
613
   local after = self.nodes[node.curBreak + 1]
×
614
   local from = node.prevBreak and node.prevBreak.curBreak or 1
×
615
   local to = node.curBreak
×
616
   return ('b %s-%s "%s | %s" [%s, %s]'):format(
×
617
      from,
618
      to,
619
      before and before:toText() or "",
×
620
      after and after:toText() or "",
×
621
      node.totalDemerits,
×
622
      node.fitness
623
   )
624
end
625

626
-- NOTE: this function is called many thousands of times even in single
627
-- page documents. Speed is more important than pretty code here.
628
function linebreaker:checkForLegalBreak (node) -- 892
42✔
629
   if debugging then
6,733✔
630
      SU.debug("break", "considering node " .. node)
×
631
   end
632
   local previous = self.nodes[self.place - 1]
6,733✔
633
   if node.is_alternative then
6,733✔
634
      self.seenAlternatives = true
×
635
   end
636
   if self.sideways and node.is_box then
6,733✔
637
      self.activeWidth:___add(node.height)
×
638
      self.activeWidth:___add(node.depth)
×
639
   elseif self.sideways and node.is_vglue then
6,733✔
640
      if previous and previous.is_box then
×
641
         self:tryBreak()
×
642
      end
643
      self.activeWidth:___add(node.height)
×
644
      self.activeWidth:___add(node.depth)
×
645
   elseif node.is_alternative then
6,733✔
646
      self.activeWidth:___add(node:minWidth())
×
647
   elseif node.is_box then
6,733✔
648
      self.activeWidth:___add(node:lineContribution())
9,537✔
649
   elseif node.is_glue then
3,554✔
650
      -- 894 (We removed the auto_breaking parameter)
651
      if previous and previous.is_box then
2,381✔
652
         self:tryBreak()
2,284✔
653
      end
654
      self.activeWidth:___add(node.width)
4,762✔
655
   elseif node.is_kern then
1,173✔
656
      self.activeWidth:___add(node.width)
80✔
657
   elseif node.is_discretionary then -- 895
1,133✔
658
      self.activeWidth:___add(node:prebreakWidth())
1,072✔
659
      self:tryBreak()
536✔
660
      self.activeWidth:___sub(node:prebreakWidth())
1,072✔
661
      self.activeWidth:___add(node:replacementWidth())
1,608✔
662
   elseif node.is_penalty then
597✔
663
      self:tryBreak()
595✔
664
   end
665
end
666

667
function linebreaker:tryFinalBreak () -- 899
42✔
668
   -- XXX TeX has self:tryBreak() here. But this doesn't seem to work
669
   -- for us. If we call tryBreak(), we end up demoting all break points
670
   -- to veryLoose (possibly because the active width gets reset - why?).
671
   -- This means we end up doing unnecessary passes.
672
   -- However, there doesn't seem to be any downside to not calling it
673
   -- (how scary is that?) so I have removed it for now. With this
674
   -- "fix", we only perform hyphenation and emergency passes when necessary
675
   -- instead of every single time. If things go strange with the break
676
   -- algorithm in the future, this should be the first place to look!
677
   -- self:tryBreak()
678
   if self.activeListHead.next == self.activeListHead then
285✔
679
      return
×
680
   end
681
   self.r = self.activeListHead.next
285✔
682
   local fewestDemerits = self.awful_bad
285✔
683
   repeat
684
      if self.r.type ~= "delta" and self.r.totalDemerits < fewestDemerits then
483✔
685
         fewestDemerits = self.r.totalDemerits
285✔
686
         self.bestBet = self.r
285✔
687
      end
688
      self.r = self.r.next
483✔
689
   until self.r == self.activeListHead
483✔
690
   if self:_param("looseness") == 0 then
570✔
691
      return true
285✔
692
   end
693
   -- XXX node 901 not implemented
694
   if self.actualLooseness == self:_param("looseness") or self.finalpass then
×
695
      return true
×
696
   end
697
end
698

699
function linebreaker:doBreak (nodes, hsize, sideways)
42✔
700
   self.passSerial = 1
285✔
701
   debugging = SILE.debugFlags["break"]
285✔
702
   self.seenAlternatives = false
285✔
703
   self.nodes = nodes
285✔
704
   self.hsize = hsize
285✔
705
   self.sideways = sideways
285✔
706
   self:init()
285✔
707
   self.adjdemerits = self:_param("adjdemerits")
570✔
708
   self.threshold = self:_param("pretolerance")
570✔
709
   if self.threshold >= 0 then
285✔
710
      self.pass = "first"
285✔
711
      self.finalpass = false
285✔
712
   else
713
      self.threshold = self:_param("tolerance")
×
714
      self.pass = "second"
×
715
      self.finalpass = self:_param("emergencyStretch") <= 0
×
716
   end
717
   -- 889
718
   while 1 do
322✔
719
      if debugging then
322✔
720
         SU.debug("break", "@", self.pass, "pass")
×
721
      end
722
      if self.threshold > self.inf_bad then
322✔
723
         self.threshold = self.inf_bad
×
724
      end
725
      if self.pass == "second" then
322✔
726
         local hyphenator = self.typesetter.language.hyphenator
23✔
727
         self.nodes = hyphenator:hyphenate(self.nodes)
46✔
728
         self.typesetter.state.nodes = self.nodes -- Horrible breaking of separation of concerns here. :-(
23✔
729
      end
730
      -- 890
731
      self.activeListHead = {
322✔
732
         sentinel = "START",
733
         type = "hyphenated",
734
         lineNumber = self.awful_bad,
322✔
735
         subtype = 0,
736
      } -- 846
322✔
737
      self.activeListHead.next = {
322✔
738
         sentinel = "END",
739
         type = "unhyphenated",
740
         fitness = "decent",
741
         next = self.activeListHead,
322✔
742
         lineNumber = self:_param("prevGraf") + 1,
644✔
743
         totalDemerits = 0,
744
      }
322✔
745

746
      -- Not doing 1630
747
      self.activeWidth = SILE.types.length(self.background)
644✔
748

749
      self.place = 1
322✔
750
      while self.nodes[self.place] and self.activeListHead.next ~= self.activeListHead do
7,055✔
751
         self:checkForLegalBreak(self.nodes[self.place])
6,733✔
752
         self.place = self.place + 1
6,733✔
753
      end
754
      if self.place > #self.nodes then
322✔
755
         if self:tryFinalBreak() then
570✔
756
            break
285✔
757
         end
758
      end
759
      -- (Not doing 891)
760
      if self.pass ~= "second" then
37✔
761
         self.pass = "second"
23✔
762
         self.threshold = self:_param("tolerance")
46✔
763
      else
764
         self.pass = "emergency"
14✔
765
         self.background.stretch:___add(self:_param("emergencyStretch"))
28✔
766
         self.finalpass = true
14✔
767
      end
768
   end
769
   -- Not doing 1638
770
   return self:postLineBreak()
285✔
771
end
772

773
function linebreaker:postLineBreak () -- 903
42✔
774
   local p = self.bestBet
285✔
775
   local breaks = {}
285✔
776
   local line = 1
285✔
777

778
   local nbLines = 0
285✔
779
   local p2 = p
285✔
780
   repeat
781
      nbLines = nbLines + 1
397✔
782
      p2 = p2.prevBreak
397✔
783
   until not p2
397✔
784

785
   repeat
786
      local left, _, right
787
      -- SILE handles the actual line width differently than TeX,
788
      -- so below always return a width of self.hsize. Would they
789
      -- be needed at some point, the exact width are commented out
790
      -- below.
791
      if self.parShaping then
397✔
792
         left, _, right = self:parShapeCache(nbLines + 1 - line)
30✔
793
      else
794
         if self.hangAfter == 0 then
382✔
795
            -- width = self.hsize
796
            left = 0
382✔
797
            right = 0
382✔
798
         else
799
            local indent
UNCOV
800
            if self.hangAfter > 0 then
×
801
               -- width = line > nbLines - self.hangAfter and self.firstWidth or self.secondWidth
UNCOV
802
               indent = line > nbLines - self.hangAfter and 0 or self.hangIndent
×
803
            else
804
               -- width = line > nbLines + self.hangAfter and self.firstWidth or self.secondWidth
UNCOV
805
               indent = line > nbLines + self.hangAfter and self.hangIndent or 0
×
806
            end
UNCOV
807
            if indent > 0 then
×
UNCOV
808
               left = indent
×
UNCOV
809
               right = 0
×
810
            else
UNCOV
811
               left = 0
×
UNCOV
812
               right = -indent
×
813
            end
814
         end
815
      end
816

817
      table.insert(breaks, 1, {
794✔
818
         position = p.curBreak,
397✔
819
         width = self.hsize,
397✔
820
         left = left,
397✔
821
         right = right,
397✔
822
      })
823
      if p.alternates then
397✔
824
         for i = 1, #p.alternates do
×
825
            p.alternates[i].selected = p.altSelections[i]
×
826
            p.alternates[i].width = p.alternates[i].options[p.altSelections[i]].width
×
827
         end
828
      end
829
      p = p.prevBreak
397✔
830
      line = line + 1
397✔
831
   until not p
397✔
832
   self:parShapeCacheClear()
285✔
833
   return breaks
285✔
834
end
835

836
function linebreaker:dumpActiveRing ()
42✔
837
   local p = self.activeListHead
×
838
   if not SILE.quiet then
×
839
      io.stderr:write("\n")
×
840
   end
841
   repeat
842
      if not SILE.quiet then
×
843
         if p == self.r then
×
844
            io.stderr:write("-> ")
×
845
         else
846
            io.stderr:write("   ")
×
847
         end
848
      end
849
      SU.debug("break", self:describeBreakNode(p))
×
850
      p = p.next
×
851
   until p == self.activeListHead
×
852
end
853

854
return linebreaker
42✔
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

© 2026 Coveralls, Inc