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

sile-typesetter / sile / 9304049654

30 May 2024 02:12PM UTC coverage: 60.021% (-14.7%) from 74.707%
9304049654

push

github

web-flow
Merge 1a26b4f22 into a1fd105f8

6743 of 12900 new or added lines in 186 files covered. (52.27%)

347 existing lines in 49 files now uncovered.

10311 of 17179 relevant lines covered (60.02%)

3307.34 hits per line

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

76.55
/core/break.lua
1
SILE.settings:declare({
370✔
2
   parameter = "linebreak.parShape",
3
   type = "boolean",
4
   default = false,
5
   help = "If set to true, the paragraph shaping method is activated.",
6
})
7
SILE.settings:declare({ parameter = "linebreak.tolerance", type = "integer or nil", default = 500 })
370✔
8
SILE.settings:declare({ parameter = "linebreak.pretolerance", type = "integer or nil", default = 100 })
370✔
9
SILE.settings:declare({ parameter = "linebreak.hangIndent", type = "measurement", default = 0 })
370✔
10
SILE.settings:declare({ parameter = "linebreak.hangAfter", type = "integer or nil", default = nil })
370✔
11
SILE.settings:declare({
370✔
12
   parameter = "linebreak.adjdemerits",
13
   type = "integer",
14
   default = 10000,
15
   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.",
16
})
17
SILE.settings:declare({ parameter = "linebreak.looseness", type = "integer", default = 0 })
370✔
18
SILE.settings:declare({ parameter = "linebreak.prevGraf", type = "integer", default = 0 })
370✔
19
SILE.settings:declare({ parameter = "linebreak.emergencyStretch", type = "measurement", default = 0 })
370✔
20
SILE.settings:declare({ parameter = "linebreak.doLastLineFit", type = "boolean", default = false }) -- unimplemented
370✔
21
SILE.settings:declare({ parameter = "linebreak.linePenalty", type = "integer", default = 10 })
370✔
22
SILE.settings:declare({ parameter = "linebreak.hyphenPenalty", type = "integer", default = 50 })
370✔
23
SILE.settings:declare({ parameter = "linebreak.doubleHyphenDemerits", type = "integer", default = 10000 })
370✔
24
SILE.settings:declare({ parameter = "linebreak.finalHyphenDemerits", type = "integer", default = 5000 })
370✔
25

26
-- doubleHyphenDemerits
27
-- hyphenPenalty
28

29
local classes = { "tight", "decent", "loose", "veryLoose" }
370✔
30
local passSerial = 0
370✔
31
local awful_bad = 1073741823
370✔
32
local inf_bad = 10000
370✔
33
local ejectPenalty = -inf_bad
370✔
34
local lineBreak = {}
370✔
35

36
--[[
37
  Basic control flow:
38
  doBreak:
39
    init
40
    for each node:
41
      checkForLegalBreak
42
        tryBreak
43
          createNewActiveNodes
44
          considerDemerits
45
            deactivateR (or) recordFeasible
46
    tryFinalBreak
47
    postLineBreak
48
]]
49

50
local param = function (key)
51
   local value = SILE.settings:get("linebreak." .. key)
18,227✔
52
   return type(value) == "table" and value:absolute() or value
19,177✔
53
end
54

55
-- Routines here will be called thousands of times; we micro-optimize
56
-- to avoid debugging and concat calls.
57
local debugging = false
370✔
58

59
function lineBreak:init ()
370✔
60
   self:trimGlue() -- 842
893✔
61
   -- 849
62
   self.activeWidth = SILE.types.length()
1,786✔
63
   self.curActiveWidth = SILE.types.length()
1,786✔
64
   self.breakWidth = SILE.types.length()
1,786✔
65
   -- 853
66
   local rskip = (SILE.settings:get("document.rskip") or SILE.types.node.glue()).width:absolute()
2,490✔
67
   local lskip = (SILE.settings:get("document.lskip") or SILE.types.node.glue()).width:absolute()
2,467✔
68
   self.background = rskip + lskip
1,786✔
69
   -- 860
70
   self.bestInClass = {}
893✔
71
   for i = 1, #classes do
4,465✔
72
      self.bestInClass[classes[i]] = {
3,572✔
73
         minimalDemerits = awful_bad,
3,572✔
74
      }
3,572✔
75
   end
76
   self.minimumDemerits = awful_bad
893✔
77
   self:setupLineLengths()
893✔
78
end
79

80
function lineBreak:trimGlue () -- 842
370✔
81
   local nodes = self.nodes
893✔
82
   if nodes[#nodes].is_glue then
893✔
NEW
83
      nodes[#nodes] = nil
×
84
   end
85
   nodes[#nodes + 1] = SILE.types.node.penalty(inf_bad)
1,786✔
86
end
87

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

110
local parShapeCache = {}
370✔
111

112
local grantLeftoverWidth = function (hsize, l, w, r)
113
   local width = SILE.types.measurement(w or hsize)
133✔
114
   if not w and l then
133✔
115
      width = width - SILE.types.measurement(l)
32✔
116
   end
117
   if not w and r then
133✔
118
      width = width - SILE.types.measurement(r)
32✔
119
   end
120
   local remaining = hsize:tonumber() - width:tonumber()
399✔
121
   local left = SU.cast("number", l or (r and (remaining - SU.cast("number", r))) or 0)
139✔
122
   local right = SU.cast("number", r or (l and (remaining - SU.cast("number", l))) or remaining)
169✔
123
   return left, width, right
133✔
124
end
125

126
-- Wrap linebreak:parShape in a memoized table for fast access
127
function lineBreak:parShapeCache (n)
370✔
128
   local cache = parShapeCache[n]
133✔
129
   if not cache then
133✔
130
      local l, w, r = self:parShape(n)
133✔
131
      local left, width, right = grantLeftoverWidth(self.hsize, l, w, r)
133✔
132
      cache = { left, width, right }
133✔
133
   end
134
   return cache[1], cache[2], cache[3]
133✔
135
end
136

137
function lineBreak.parShapeCacheClear (_)
370✔
138
   pl.tablex.clear(parShapeCache)
893✔
139
end
140

141
function lineBreak:setupLineLengths () -- 874
280✔
142
   self.parShaping = param("parShape") or false
1,448✔
143
   if self.parShaping then
724✔
144
      self.lastSpecialLine = nil
2✔
145
      self.easy_line = nil
2✔
146
   else
147
      self.hangAfter = param("hangAfter") or 0
1,444✔
148
      self.hangIndent = param("hangIndent"):tonumber()
2,166✔
149
      if self.hangIndent == 0 then
722✔
150
         self.lastSpecialLine = 0
711✔
151
         self.secondWidth = self.hsize or SU.error("No hsize")
711✔
152
      else -- 875
153
         self.lastSpecialLine = math.abs(self.hangAfter)
11✔
154
         if self.hangAfter < 0 then
11✔
155
            self.secondWidth = self.hsize or SU.error("No hsize")
9✔
156
            self.firstWidth = self.hsize - math.abs(self.hangIndent)
18✔
157
         else
158
            self.firstWidth = self.hsize or SU.error("No hsize")
2✔
159
            self.secondWidth = self.hsize - math.abs(self.hangIndent)
4✔
160
         end
161
      end
162
      if param("looseness") == 0 then
1,444✔
163
         self.easy_line = self.lastSpecialLine
722✔
164
      else
NEW
165
         self.easy_line = awful_bad
×
166
      end
167
      -- self.easy_line = awful_bad
168
   end
169
end
170

171
function lineBreak:tryBreak () -- 855
280✔
172
   local pi, breakType
173
   local node = self.nodes[self.place]
13,337✔
174
   if not node then
13,337✔
NEW
175
      pi = ejectPenalty
×
NEW
176
      breakType = "hyphenated"
×
177
   elseif node.is_discretionary then
13,337✔
178
      breakType = "hyphenated"
3,128✔
179
      pi = param("hyphenPenalty")
6,256✔
180
   else
181
      breakType = "unhyphenated"
10,209✔
182
      pi = node.penalty or 0
10,209✔
183
   end
184
   if debugging then
13,337✔
NEW
185
      SU.debug("break", "Trying a", breakType, "break p =", pi)
×
186
   end
187
   self.no_break_yet = true -- We have to store all this state crap in the object, or it's global variables all the way
13,337✔
188
   self.prev_prev_r = nil
13,337✔
189
   self.prev_r = self.activeListHead
13,337✔
190
   self.old_l = 0
13,337✔
191
   self.r = nil
13,337✔
192
   self.curActiveWidth = SILE.types.length(self.activeWidth)
26,674✔
193
   while true do
194
      while true do -- allows "break" to function as "continue"
195
         self.r = self.prev_r.next
46,183✔
196
         if debugging then
46,183✔
NEW
197
            SU.debug(
×
198
               "break",
199
               "We have moved the link  forward, ln is now",
NEW
200
               self.r.type == "delta" and "XX" or self.r.lineNumber
×
201
            )
202
         end
203
         if self.r.type == "delta" then -- 858
46,183✔
204
            if debugging then
8,482✔
NEW
205
               SU.debug("break", " Adding delta node width of", self.r.width)
×
206
            end
207
            self.curActiveWidth:___add(self.r.width)
8,482✔
208
            self.prev_prev_r = self.prev_r
8,482✔
209
            self.prev_r = self.r
8,482✔
210
            break
8,482✔
211
         end
212
         -- 861
213
         if self.r.lineNumber > self.old_l then
37,701✔
214
            if debugging then
26,739✔
NEW
215
               SU.debug("break", "Minimum demerits =", self.minimumDemerits)
×
216
            end
217
            if self.minimumDemerits < awful_bad and (self.old_l ~= self.easy_line or self.r == self.activeListHead) then
26,739✔
218
               self:createNewActiveNodes(breakType)
2,500✔
219
            end
220
            if self.r == self.activeListHead then
26,739✔
221
               if debugging then
13,337✔
NEW
222
                  SU.debug("break", "<- tryBreak")
×
223
               end
224
               return
13,337✔
225
            end
226
            -- 876
227
            if self.easy_line and self.r.lineNumber > self.easy_line then
13,402✔
228
               self.lineWidth = self.secondWidth
12,724✔
229
               self.old_l = awful_bad - 1
12,724✔
230
            else
231
               self.old_l = self.r.lineNumber
678✔
232
               if self.lastSpecialLine and self.r.lineNumber > self.lastSpecialLine then
678✔
NEW
233
                  self.lineWidth = self.secondWidth
×
234
               elseif self.parShaping then
678✔
235
                  local _
236
                  _, self.lineWidth, _ = self:parShapeCache(self.r.lineNumber)
236✔
237
               else
238
                  self.lineWidth = self.firstWidth
560✔
239
               end
240
            end
241
            if debugging then
13,402✔
NEW
242
               SU.debug("break", "line width =", self.lineWidth)
×
243
            end
244
         end
245
         if debugging then
24,364✔
NEW
246
            SU.debug("break", " ---> (2) cuaw is", self.curActiveWidth)
×
NEW
247
            SU.debug("break", " ---> aw is", self.activeWidth)
×
248
         end
249
         self:considerDemerits(pi, breakType)
24,364✔
250
         if debugging then
24,364✔
NEW
251
            SU.debug("break", " <--- cuaw is", self.curActiveWidth)
×
NEW
252
            SU.debug("break", " <--- aw is ", self.activeWidth)
×
253
         end
254
      end
255
   end
256
end
257

258
-- Note: This function gets called a lot and to optimize it we're assuming that
259
-- the lengths being passed are already absolutized. This is not a safe
260
-- assumption to make universally.
261
local function fitclass (self, shortfall)
262
   shortfall = shortfall.amount
24,364✔
263
   local badness, class
264
   local stretch = self.curActiveWidth.stretch.amount
24,364✔
265
   local shrink = self.curActiveWidth.shrink.amount
24,364✔
266
   if shortfall > 0 then
24,364✔
267
      if shortfall > 110 and stretch < 25 then
22,721✔
268
         badness = inf_bad
11,688✔
269
      else
270
         badness = SU.rateBadness(inf_bad, shortfall, stretch)
22,066✔
271
      end
272
      if badness > 99 then
22,721✔
273
         class = "veryLoose"
18,336✔
274
      elseif badness > 12 then
4,385✔
275
         class = "loose"
276✔
276
      else
277
         class = "decent"
4,109✔
278
      end
279
   else
280
      shortfall = -shortfall
1,643✔
281
      if shortfall > shrink then
1,643✔
282
         badness = inf_bad + 1
1,205✔
283
      else
284
         badness = SU.rateBadness(inf_bad, shortfall, shrink)
876✔
285
      end
286
      if badness > 12 then
1,643✔
287
         class = "tight"
1,405✔
288
      else
289
         class = "decent"
238✔
290
      end
291
   end
292
   return badness, class
24,364✔
293
end
294

295
function lineBreak:tryAlternatives (from, to)
280✔
NEW
296
   local altSizes = {}
×
NEW
297
   local alternates = {}
×
NEW
298
   for i = from, to do
×
NEW
299
      if self.nodes[i] and self.nodes[i].is_alternative then
×
NEW
300
         alternates[#alternates + 1] = self.nodes[i]
×
NEW
301
         altSizes[#altSizes + 1] = #self.nodes[i].options
×
302
      end
303
   end
NEW
304
   if #alternates == 0 then
×
NEW
305
      return
×
306
   end
NEW
307
   local localMinimum = awful_bad
×
308
   -- local selectedShortfall
NEW
309
   local shortfall = self.lineWidth - self.curActiveWidth
×
NEW
310
   if debugging then
×
NEW
311
      SU.debug("break", "Shortfall was ", shortfall)
×
312
   end
NEW
313
   for combination in SU.allCombinations(altSizes) do
×
NEW
314
      local addWidth = 0
×
NEW
315
      for i = 1, #alternates do
×
NEW
316
         local alternative = alternates[i]
×
NEW
317
         addWidth = (addWidth + alternative.options[combination[i]].width - alternative:minWidth())
×
NEW
318
         if debugging then
×
NEW
319
            SU.debug("break", alternative.options[combination[i]], " width", addWidth)
×
320
         end
321
      end
NEW
322
      local ss = shortfall - addWidth
×
323
      -- Warning, assumes abosolute
324
      local badness =
NEW
325
         SU.rateBadness(inf_bad, ss.length.amount, self.curActiveWidth[ss > 0 and "stretch" or "shrink"].length.amount)
×
NEW
326
      if debugging then
×
NEW
327
         SU.debug("break", "  badness of", ss, "(", self.curActiveWidth, ") is", badness)
×
328
      end
NEW
329
      if badness < localMinimum then
×
NEW
330
         self.r.alternates = alternates
×
NEW
331
         self.r.altSelections = combination
×
332
         -- selectedShortfall = addWidth
NEW
333
         localMinimum = badness
×
334
      end
335
   end
NEW
336
   if debugging then
×
NEW
337
      SU.debug("break", "Choosing ", alternates[1].options[self.r.altSelections[1]])
×
338
   end
339
   -- self.curActiveWidth:___add(selectedShortfall)
NEW
340
   shortfall = self.lineWidth - self.curActiveWidth
×
NEW
341
   if debugging then
×
NEW
342
      SU.debug("break", "Is now ", shortfall)
×
343
   end
344
end
345

346
function lineBreak:considerDemerits (pi, breakType) -- 877
280✔
347
   self.artificialDemerits = false
24,364✔
348
   local nodeStaysActive = false
24,364✔
349
   -- self:dumpActiveRing()
350
   if self.seenAlternatives then
24,364✔
NEW
351
      self:tryAlternatives(
×
NEW
352
         self.r.prevBreak and self.r.prevBreak.curBreak or 1,
×
NEW
353
         self.r.curBreak and self.r.curBreak or 1
×
354
      )
355
   end
356
   local shortfall = self.lineWidth - self.curActiveWidth
24,364✔
357
   self.badness, self.fitClass = fitclass(self, shortfall)
48,728✔
358
   if debugging then
24,364✔
NEW
359
      SU.debug("break", self.badness, self.fitClass)
×
360
   end
361
   if self.badness > inf_bad or pi == ejectPenalty then
24,364✔
362
      if
363
         self.finalpass
364
         and self.minimumDemerits == awful_bad
2,570✔
365
         and self.r.next == self.activeListHead
295✔
366
         and self.prev_r == self.activeListHead
146✔
367
      then
368
         self.artificialDemerits = true
146✔
369
      else
370
         if self.badness > self.threshold then
2,424✔
371
            self:deactivateR()
1,175✔
372
            return
1,175✔
373
         end
374
      end
375
   else
376
      self.prev_r = self.r
21,794✔
377
      if self.badness > self.threshold then
21,794✔
378
         return
17,922✔
379
      end
380
      nodeStaysActive = true
3,872✔
381
   end
382

383
   local _shortfall = shortfall:tonumber()
5,267✔
384
   local function shortfallratio (metric)
385
      local prop = self.curActiveWidth[metric]:tonumber()
5,267✔
386
      local factor = prop ~= 0 and prop or awful_bad
5,267✔
387
      return _shortfall / factor
5,267✔
388
   end
389
   self.lastRatio = shortfallratio(_shortfall > 0 and "stretch" or "shrink")
10,534✔
390
   self:recordFeasible(pi, breakType)
5,267✔
391
   if not nodeStaysActive then
5,267✔
392
      self:deactivateR()
1,395✔
393
   end
394
end
395

396
function lineBreak:deactivateR () -- 886
280✔
397
   if debugging then
2,570✔
NEW
398
      SU.debug("break", " Deactivating r (" .. self.r.type .. ")")
×
399
   end
400
   self.prev_r.next = self.r.next
2,570✔
401
   if self.prev_r == self.activeListHead then
2,570✔
402
      -- 887
403
      self.r = self.activeListHead.next
2,563✔
404
      if self.r.type == "delta" then
2,563✔
405
         self.activeWidth:___add(self.r.width)
1,499✔
406
         self.curActiveWidth = SILE.types.length(self.activeWidth)
2,998✔
407
         self.activeListHead.next = self.r.next
1,499✔
408
      end
409
      if debugging then
2,563✔
NEW
410
         SU.debug("break", "  Deactivate, branch 1")
×
411
      end
412
   else
413
      if self.prev_r.type == "delta" then
7✔
414
         self.r = self.prev_r.next
7✔
415
         if self.r == self.activeListHead then
7✔
NEW
416
            self.curActiveWidth:___sub(self.prev_r.width)
×
417
            -- FIXME It was crashing here, so changed from:
418
            -- self.curActiveWidth:___sub(self.r.width)
419
            -- But I'm not so sure reading Knuth here...
NEW
420
            self.prev_prev_r.next = self.activeListHead
×
NEW
421
            self.prev_r = self.prev_prev_r
×
422
         elseif self.r.type == "delta" then
7✔
423
            self.curActiveWidth:___add(self.r.width)
7✔
424
            self.prev_r.width:___add(self.r.width)
7✔
425
            self.prev_r.next = self.r.next
7✔
426
         end
427
      end
428
      if debugging then
7✔
NEW
429
         SU.debug("break", "  Deactivate, branch 2")
×
430
      end
431
   end
432
end
433

434
function lineBreak:computeDemerits (pi, breakType)
280✔
435
   if self.artificialDemerits then
5,267✔
436
      return 0
146✔
437
   end
438
   local demerit = param("linePenalty") + self.badness
10,242✔
439
   if math.abs(demerit) >= 10000 then
5,121✔
NEW
440
      demerit = 100000000
×
441
   else
442
      demerit = demerit * demerit
5,121✔
443
   end
444
   if pi > 0 then
5,121✔
445
      demerit = demerit + pi * pi
633✔
446
   -- elseif pi == 0 then
447
   --   -- do nothing
448
   elseif pi > ejectPenalty then
4,488✔
449
      demerit = demerit - pi * pi
3,239✔
450
   end
451
   if breakType == "hyphenated" and self.r.type == "hyphenated" then
5,121✔
452
      if self.nodes[self.place] then
219✔
453
         demerit = demerit + param("doubleHyphenDemerits")
438✔
454
      else
NEW
455
         demerit = demerit + param("finalHyphenDemerits")
×
456
      end
457
   end
458
   -- XXX adjDemerits not added here
459
   return demerit
5,121✔
460
end
461

462
function lineBreak:recordFeasible (pi, breakType) -- 881
280✔
463
   local demerit = lineBreak:computeDemerits(pi, breakType)
5,267✔
464
   if debugging then
5,267✔
NEW
465
      if self.nodes[self.place] then
×
NEW
466
         SU.debug(
×
467
            "break",
468
            "@",
NEW
469
            self.nodes[self.place],
×
470
            "via @@",
NEW
471
            (self.r.serial or "0"),
×
472
            "badness =",
NEW
473
            self.badness,
×
474
            "demerit =",
475
            demerit
476
         ) -- 882
477
      else
NEW
478
         SU.debug("break", "@ \\par via @@")
×
479
      end
NEW
480
      SU.debug("break", " fit class =", self.fitClass)
×
481
   end
482
   demerit = demerit + self.r.totalDemerits
5,267✔
483
   if demerit <= self.bestInClass[self.fitClass].minimalDemerits then
5,267✔
484
      self.bestInClass[self.fitClass] = {
3,545✔
485
         minimalDemerits = demerit,
3,545✔
486
         node = self.r.serial and self.r,
3,545✔
487
         line = self.r.lineNumber,
3,545✔
488
      }
3,545✔
489
      -- XXX do last line fit
490
      if demerit < self.minimumDemerits then
3,545✔
491
         self.minimumDemerits = demerit
2,811✔
492
      end
493
   end
494
end
495

496
function lineBreak:createNewActiveNodes (breakType) -- 862
280✔
497
   if self.no_break_yet then
2,500✔
498
      -- 863
499
      self.no_break_yet = false
2,500✔
500
      self.breakWidth = SILE.types.length(self.background)
5,000✔
501
      local place = self.place
2,500✔
502
      local node = self.nodes[place]
2,500✔
503
      if node and node.is_discretionary then -- 866
2,500✔
504
         self.breakWidth:___add(node:prebreakWidth())
774✔
505
         self.breakWidth:___add(node:postbreakWidth())
774✔
506
         self.breakWidth:___sub(node:replacementWidth())
774✔
507
      end
508
      while self.nodes[place] and not self.nodes[place].is_box do
6,201✔
509
         if self.sideways and self.nodes[place].height then
3,701✔
NEW
510
            self.breakWidth:___sub(self.nodes[place].height)
×
NEW
511
            self.breakWidth:___sub(self.nodes[place].depth)
×
512
         elseif self.nodes[place].width then -- We use the fact that (a) nodes know if they have width and (b) width subtraction is polymorphic
3,701✔
513
            self.breakWidth:___sub(self.nodes[place]:lineContribution())
7,402✔
514
         end
515
         place = place + 1
3,701✔
516
      end
517
      if debugging then
2,500✔
NEW
518
         SU.debug("break", "Value of breakWidth =", self.breakWidth)
×
519
      end
520
   end
521
   -- 869 (Add a new delta node)
522
   if self.prev_r.type == "delta" then
2,500✔
523
      self.prev_r.width:___sub(self.curActiveWidth)
12✔
524
      self.prev_r.width:___add(self.breakWidth)
24✔
525
   elseif self.prev_r == self.activeListHead then
2,488✔
526
      self.activeWidth = SILE.types.length(self.breakWidth)
1,688✔
527
   else
528
      local newDelta = { next = self.r, type = "delta", width = self.breakWidth - self.curActiveWidth }
3,288✔
529
      if debugging then
1,644✔
NEW
530
         SU.debug("break", "Added new delta node =", newDelta.width)
×
531
      end
532
      self.prev_r.next = newDelta
1,644✔
533
      self.prev_prev_r = self.prev_r
1,644✔
534
      self.prev_r = newDelta
1,644✔
535
   end
536
   if math.abs(self.adjdemerits) >= (awful_bad - self.minimumDemerits) then
2,500✔
NEW
537
      self.minimumDemerits = awful_bad - 1
×
538
   else
539
      self.minimumDemerits = self.minimumDemerits + math.abs(self.adjdemerits)
2,500✔
540
   end
541

542
   for i = 1, #classes do
12,500✔
543
      local class = classes[i]
10,000✔
544
      local best = self.bestInClass[class]
10,000✔
545
      local value = best.minimalDemerits
10,000✔
546
      if debugging then
10,000✔
NEW
547
         SU.debug("break", "Class is", class, "Best value here is", value)
×
548
      end
549

550
      if value <= self.minimumDemerits then
10,000✔
551
         -- 871: this is what creates new active notes
552
         passSerial = passSerial + 1
2,564✔
553

554
         local newActive = {
2,564✔
555
            type = breakType,
2,564✔
556
            next = self.r,
2,564✔
557
            curBreak = self.place,
2,564✔
558
            prevBreak = best.node,
2,564✔
559
            serial = passSerial,
2,564✔
560
            ratio = self.lastRatio,
2,564✔
561
            lineNumber = best.line + 1,
2,564✔
562
            fitness = class,
2,564✔
563
            totalDemerits = value,
2,564✔
564
         }
565
         -- DoLastLineFit? 1636 XXX
566
         self.prev_r.next = newActive
2,564✔
567
         self.prev_r = newActive
2,564✔
568
         self:dumpBreakNode(newActive)
2,564✔
569
      end
570
      self.bestInClass[class] = { minimalDemerits = awful_bad }
10,000✔
571
   end
572

573
   self.minimumDemerits = awful_bad
2,500✔
574
   -- 870
575
   if self.r ~= self.activeListHead then
2,500✔
576
      local newDelta = { next = self.r, type = "delta", width = self.curActiveWidth - self.breakWidth }
24✔
577
      self.prev_r.next = newDelta
12✔
578
      self.prev_prev_r = self.prev_r
12✔
579
      self.prev_r = newDelta
12✔
580
   end
581
end
582

583
function lineBreak.dumpBreakNode (_, node)
280✔
584
   if not SU.debugging("break") then
5,128✔
585
      return
2,564✔
586
   end
NEW
587
   SU.debug("break", lineBreak:describeBreakNode(node))
×
588
end
589

590
function lineBreak:describeBreakNode (node)
280✔
591
   --SU.debug("break", "@@", b.serial, ": line", b.lineNumber - 1, ".", b.fitness, b.type, "t=", b.totalDemerits, "-> @@", b.prevBreak and b.prevBreak.serial or "0")
NEW
592
   if node.sentinel then
×
NEW
593
      return node.sentinel
×
594
   end
NEW
595
   if node.type == "delta" then
×
NEW
596
      return "delta " .. node.width .. "pt"
×
597
   end
NEW
598
   local before = self.nodes[node.curBreak - 1]
×
NEW
599
   local after = self.nodes[node.curBreak + 1]
×
NEW
600
   local from = node.prevBreak and node.prevBreak.curBreak or 1
×
NEW
601
   local to = node.curBreak
×
NEW
602
   return ('b %s-%s "%s | %s" [%s, %s]'):format(
×
603
      from,
604
      to,
NEW
605
      before and before:toText() or "",
×
NEW
606
      after and after:toText() or "",
×
NEW
607
      node.totalDemerits,
×
608
      node.fitness
609
   )
610
end
611

612
-- NOTE: this function is called many thousands of times even in single
613
-- page documents. Speed is more important than pretty code here.
614
function lineBreak:checkForLegalBreak (node) -- 892
280✔
615
   if debugging then
27,354✔
NEW
616
      SU.debug("break", "considering node " .. node)
×
617
   end
618
   local previous = self.nodes[self.place - 1]
27,354✔
619
   if node.is_alternative then
27,354✔
NEW
620
      self.seenAlternatives = true
×
621
   end
622
   if self.sideways and node.is_box then
27,354✔
NEW
623
      self.activeWidth:___add(node.height)
×
NEW
624
      self.activeWidth:___add(node.depth)
×
625
   elseif self.sideways and node.is_vglue then
27,354✔
NEW
626
      if previous and previous.is_box then
×
NEW
627
         self:tryBreak()
×
628
      end
NEW
629
      self.activeWidth:___add(node.height)
×
NEW
630
      self.activeWidth:___add(node.depth)
×
631
   elseif node.is_alternative then
27,354✔
NEW
632
      self.activeWidth:___add(node:minWidth())
×
633
   elseif node.is_box then
27,354✔
634
      self.activeWidth:___add(node:lineContribution())
40,629✔
635
   elseif node.is_glue then
13,811✔
636
      -- 894 (We removed the auto_breaking parameter)
637
      if previous and previous.is_box then
8,794✔
638
         self:tryBreak()
8,644✔
639
      end
640
      self.activeWidth:___add(node.width)
17,588✔
641
   elseif node.is_kern then
5,017✔
642
      self.activeWidth:___add(node.width)
630✔
643
   elseif node.is_discretionary then -- 895
4,702✔
644
      self.activeWidth:___add(node:prebreakWidth())
6,256✔
645
      self:tryBreak()
3,128✔
646
      self.activeWidth:___sub(node:prebreakWidth())
6,256✔
647
      self.activeWidth:___add(node:replacementWidth())
9,384✔
648
   elseif node.is_penalty then
1,574✔
649
      self:tryBreak()
1,565✔
650
   end
651
end
652

653
function lineBreak:tryFinalBreak () -- 899
280✔
654
   -- XXX TeX has self:tryBreak() here. But this doesn't seem to work
655
   -- for us. If we call tryBreak(), we end up demoting all break points
656
   -- to veryLoose (possibly because the active width gets reset - why?).
657
   -- This means we end up doing unnecessary passes.
658
   -- However, there doesn't seem to be any downside to not calling it
659
   -- (how scary is that?) so I have removed it for now. With this
660
   -- "fix", we only perform hyphenation and emergency passes when necessary
661
   -- instead of every single time. If things go strange with the break
662
   -- algorithm in the future, this should be the first place to look!
663
   -- self:tryBreak()
664
   if self.activeListHead.next == self.activeListHead then
724✔
NEW
665
      return
×
666
   end
667
   self.r = self.activeListHead.next
724✔
668
   local fewestDemerits = awful_bad
724✔
669
   repeat
670
      if self.r.type ~= "delta" and self.r.totalDemerits < fewestDemerits then
1,025✔
671
         fewestDemerits = self.r.totalDemerits
725✔
672
         self.bestBet = self.r
725✔
673
      end
674
      self.r = self.r.next
1,025✔
675
   until self.r == self.activeListHead
1,025✔
676
   if param("looseness") == 0 then
1,448✔
677
      return true
724✔
678
   end
679
   -- XXX node 901 not implemented
NEW
680
   if self.actualLooseness == param("looseness") or self.finalpass then
×
NEW
681
      return true
×
682
   end
683
end
684

685
function lineBreak:doBreak (nodes, hsize, sideways)
280✔
686
   passSerial = 1
724✔
687
   debugging = SILE.debugFlags["break"]
724✔
688
   self.seenAlternatives = false
724✔
689
   self.nodes = nodes
724✔
690
   self.hsize = hsize
724✔
691
   self.sideways = sideways
724✔
692
   self:init()
724✔
693
   self.adjdemerits = param("adjdemerits")
1,448✔
694
   self.threshold = param("pretolerance")
1,448✔
695
   if self.threshold >= 0 then
724✔
696
      self.pass = "first"
724✔
697
      self.finalpass = false
724✔
698
   else
UNCOV
699
      self.threshold = param("tolerance")
×
NEW
700
      self.pass = "second"
×
NEW
701
      self.finalpass = param("emergencyStretch") <= 0
×
702
   end
703
   -- 889
704
   while 1 do
881✔
705
      if debugging then
881✔
NEW
706
         SU.debug("break", "@", self.pass, "pass")
×
707
      end
708
      if self.threshold > inf_bad then
881✔
NEW
709
         self.threshold = inf_bad
×
710
      end
711
      if self.pass == "second" then
881✔
712
         self.nodes = SILE.hyphenate(self.nodes)
214✔
713
         SILE.typesetter.state.nodes = self.nodes -- Horrible breaking of separation of concerns here. :-(
107✔
714
      end
715
      -- 890
716
      self.activeListHead = {
881✔
717
         sentinel = "START",
718
         type = "hyphenated",
719
         lineNumber = awful_bad,
881✔
720
         subtype = 0,
721
      } -- 846
881✔
722
      self.activeListHead.next = {
881✔
723
         sentinel = "END",
724
         type = "unhyphenated",
725
         fitness = "decent",
726
         next = self.activeListHead,
881✔
727
         lineNumber = param("prevGraf") + 1,
1,762✔
728
         totalDemerits = 0,
729
      }
881✔
730

731
      -- Not doing 1630
732
      self.activeWidth = SILE.types.length(self.background)
1,762✔
733

734
      self.place = 1
881✔
735
      while self.nodes[self.place] and self.activeListHead.next ~= self.activeListHead do
28,235✔
736
         self:checkForLegalBreak(self.nodes[self.place])
27,354✔
737
         self.place = self.place + 1
27,354✔
738
      end
739
      if self.place > #self.nodes then
881✔
740
         if self:tryFinalBreak() then
1,448✔
741
            break
724✔
742
         end
743
      end
744
      -- (Not doing 891)
745
      if self.pass ~= "second" then
157✔
746
         self.pass = "second"
107✔
747
         self.threshold = param("tolerance")
214✔
748
      else
749
         self.pass = "emergency"
50✔
750
         self.background.stretch:___add(param("emergencyStretch"))
100✔
751
         self.finalpass = true
50✔
752
      end
753
   end
754
   -- Not doing 1638
755
   return self:postLineBreak()
724✔
756
end
757

758
function lineBreak:postLineBreak () -- 903
280✔
759
   local p = self.bestBet
724✔
760
   local breaks = {}
724✔
761
   local line = 1
724✔
762

763
   local nbLines = 0
724✔
764
   local p2 = p
724✔
765
   repeat
766
      nbLines = nbLines + 1
1,289✔
767
      p2 = p2.prevBreak
1,289✔
768
   until not p2
1,289✔
769

770
   repeat
771
      local left, _, right
772
      -- SILE handles the actual line width differently than TeX,
773
      -- so below always return a width of self.hsize. Would they
774
      -- be needed at some point, the exact width are commented out
775
      -- below.
776
      if self.parShaping then
1,289✔
777
         left, _, right = self:parShapeCache(nbLines + 1 - line)
30✔
778
      else
779
         if self.hangAfter == 0 then
1,274✔
780
            -- width = self.hsize
781
            left = 0
1,213✔
782
            right = 0
1,213✔
783
         else
784
            local indent
785
            if self.hangAfter > 0 then
61✔
786
               -- width = line > nbLines - self.hangAfter and self.firstWidth or self.secondWidth
787
               indent = line > nbLines - self.hangAfter and 0 or self.hangIndent
16✔
788
            else
789
               -- width = line > nbLines + self.hangAfter and self.firstWidth or self.secondWidth
790
               indent = line > nbLines + self.hangAfter and self.hangIndent or 0
45✔
791
            end
792
            if indent > 0 then
61✔
793
               left = indent
26✔
794
               right = 0
26✔
795
            else
796
               left = 0
35✔
797
               right = -indent
35✔
798
            end
799
         end
800
      end
801

802
      table.insert(breaks, 1, {
2,578✔
803
         position = p.curBreak,
1,289✔
804
         width = self.hsize,
1,289✔
805
         left = left,
1,289✔
806
         right = right,
1,289✔
807
      })
808
      if p.alternates then
1,289✔
NEW
809
         for i = 1, #p.alternates do
×
NEW
810
            p.alternates[i].selected = p.altSelections[i]
×
NEW
811
            p.alternates[i].width = p.alternates[i].options[p.altSelections[i]].width
×
812
         end
813
      end
814
      p = p.prevBreak
1,289✔
815
      line = line + 1
1,289✔
816
   until not p
1,289✔
817
   self:parShapeCacheClear()
724✔
818
   return breaks
724✔
819
end
820

821
function lineBreak:dumpActiveRing ()
280✔
NEW
822
   local p = self.activeListHead
×
NEW
823
   if not SILE.quiet then
×
NEW
824
      io.stderr:write("\n")
×
825
   end
826
   repeat
NEW
827
      if not SILE.quiet then
×
NEW
828
         if p == self.r then
×
NEW
829
            io.stderr:write("-> ")
×
830
         else
NEW
831
            io.stderr:write("   ")
×
832
         end
833
      end
NEW
834
      SU.debug("break", lineBreak:describeBreakNode(p))
×
NEW
835
      p = p.next
×
NEW
836
   until p == self.activeListHead
×
837
end
838

839
return lineBreak
280✔
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