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

sile-typesetter / sile / 8288578143

14 Mar 2024 09:39PM UTC coverage: 64.155% (-10.6%) from 74.718%
8288578143

Pull #1904

github

alerque
chore(core): Fixup ec6ed657 which didn't shim old pack styles properly
Pull Request #1904: Merge develop into master (commit to next release being breaking)

1648 of 2421 new or added lines in 107 files covered. (68.07%)

1843 existing lines in 77 files now uncovered.

10515 of 16390 relevant lines covered (64.15%)

3306.56 hits per line

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

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

18
-- doubleHyphenDemerits
19
-- hyphenPenalty
20

21
local classes = { "tight"; "decent"; "loose"; "veryLoose" }
183✔
22
local passSerial = 0
366✔
23
local awful_bad = 1073741823
366✔
24
local inf_bad = 10000
366✔
25
local ejectPenalty = -inf_bad
366✔
26
local lineBreak = {}
366✔
27

28
--[[
29
  Basic control flow:
30
  doBreak:
31
    init
32
    for each node:
33
      checkForLegalBreak
34
        tryBreak
35
          createNewActiveNodes
36
          considerDemerits
37
            deactivateR (or) recordFeasible
38
    tryFinalBreak
39
    postLineBreak
40
]]
41

42
local param = function (key)
43
  local value = SILE.settings:get("linebreak."..key)
11,479✔
44
  return type(value) == "table" and value:absolute() or value
17,986✔
45
end
46

47
-- Routines here will be called thousands of times; we micro-optimize
48
-- to avoid debugging and concat calls.
49
local debugging = false
183✔
50

51
function lineBreak:init()
183✔
52
  self:trimGlue() -- 842
687✔
53
  -- 849
54
  self.activeWidth = SILE.types.length()
1,008✔
55
  self.curActiveWidth = SILE.types.length()
1,692✔
56
  self.breakWidth = SILE.types.length()
1,692✔
57
  -- 853
58
  local rskip = (SILE.settings:get("document.rskip") or SILE.types.node.glue()).width:absolute()
1,407✔
59
  local lskip = (SILE.settings:get("document.lskip") or SILE.types.node.glue()).width:absolute()
2,341✔
60
  self.background = rskip + lskip
1,982✔
61
  -- 860
62
  self.bestInClass = {}
504✔
63
  for i = 1, #classes do
2,862✔
64
    self.bestInClass[classes[i]] = {
3,726✔
65
      minimalDemerits = awful_bad
3,384✔
66
    }
3,384✔
67
  end
68
  self.minimumDemerits = awful_bad
504✔
69
  self:setupLineLengths()
846✔
70
end
71

72
function lineBreak:trimGlue() -- 842
183✔
73
  local nodes = self.nodes
687✔
74
  if nodes[#nodes].is_glue then nodes[#nodes] = nil end
846✔
75
  nodes[#nodes+1] = SILE.types.node.penalty(inf_bad)
1,350✔
76
end
77

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

100
local parShapeCache = {}
183✔
101

102
local grantLeftoverWidth = function (hsize, l, w, r)
103
  local width = SILE.types.measurement(w or hsize)
133✔
104
  if not w and l then width = width - SILE.types.measurement(l) end
149✔
105
  if not w and r then width = width - SILE.types.measurement(r) end
149✔
106
  local remaining = hsize:tonumber() - width:tonumber()
399✔
107
  local left = SU.cast("number", l or (r and (remaining - SU.cast("number", r))) or 0)
139✔
108
  local right = SU.cast("number", r or (l and (remaining - SU.cast("number", l))) or remaining)
169✔
109
  return left, width, right
133✔
110
end
111

112
-- Wrap linebreak:parShape in a memoized table for fast access
113
function lineBreak:parShapeCache(n)
183✔
114
  local cache = parShapeCache[n]
316✔
115
  if not cache then
133✔
116
    local l, w, r = self:parShape(n)
133✔
117
    local left, width, right = grantLeftoverWidth(self.hsize, l, w, r)
133✔
118
    cache = { left, width, right }
133✔
119
  end
120
  return cache[1], cache[2], cache[3]
133✔
121
end
122

123
function lineBreak.parShapeCacheClear(_)
183✔
124
  pl.tablex.clear(parShapeCache)
687✔
125
end
126

127
function lineBreak:setupLineLengths() -- 874
183✔
128
  self.parShaping = param("parShape") or false
1,191✔
129
  if self.parShaping then
1,188✔
130
    self.lastSpecialLine = nil
344✔
131
    self.easy_line = nil
2✔
132
  else
133
    self.hangAfter = param("hangAfter") or 0
1,004✔
134
    self.hangIndent = param("hangIndent"):tonumber()
2,190✔
135
    if self.hangIndent == 0 then
1,528✔
136
      self.lastSpecialLine = 0
833✔
137
      self.secondWidth = self.hsize or SU.error("No hsize")
833✔
138
    else -- 875
139
      self.lastSpecialLine = math.abs(self.hangAfter)
11✔
140
      if self.hangAfter < 0 then
11✔
141
        self.secondWidth = self.hsize or SU.error("No hsize")
9✔
142
        self.firstWidth = self.hsize - math.abs(self.hangIndent)
18✔
143
      else
144
        self.firstWidth = self.hsize or SU.error("No hsize")
2✔
145
        self.secondWidth = self.hsize - math.abs(self.hangIndent)
4✔
146
      end
147
    end
148
    if param("looseness") == 0 then self.easy_line = self.lastSpecialLine else self.easy_line = awful_bad end
1,004✔
149
    -- self.easy_line = awful_bad
150
  end
151
end
152

153
function lineBreak:tryBreak() -- 855
183✔
154
  local pi, breakType
155
  local node = self.nodes[self.place]
15,103✔
156
  if not node then pi = ejectPenalty; breakType = "hyphenated"
15,103✔
157
  elseif node.is_discretionary then breakType = "hyphenated"; pi = param("hyphenPenalty")
16,834✔
158
  else breakType = "unhyphenated"; pi = node.penalty or 0 end
15,011✔
159
  if debugging then SU.debug("break", "Trying a", breakType, "break p =", pi) end
13,464✔
160
  self.no_break_yet = true -- We have to store all this state crap in the object, or it's global variables all the way
15,103✔
161
  self.prev_prev_r = nil
15,103✔
162
  self.prev_r = self.activeListHead
15,103✔
163
  self.old_l = 0
15,103✔
164
  self.r = nil
15,103✔
165
  self.curActiveWidth = SILE.types.length(self.activeWidth)
23,715✔
166
  while true do
167
    while true do -- allows "break" to function as "continue"
168
      self.r = self.prev_r.next
53,022✔
169
      if debugging then SU.debug("break", "We have moved the link  forward, ln is now", self.r.type == "delta" and "XX" or self.r.lineNumber) end
53,022✔
170
      if self.r.type == "delta" then -- 858
53,022✔
171
        if debugging then SU.debug("break", " Adding delta node width of", self.r.width) end
25,421✔
172
        self.curActiveWidth:___add(self.r.width)
9,975✔
173
        self.prev_prev_r = self.prev_r
9,975✔
174
        self.prev_r = self.r
9,975✔
175
        break
9,975✔
176
      end
177
      -- 861
178
      if self.r.lineNumber > self.old_l then
27,601✔
179
        if debugging then SU.debug("break", "Minimum demerits =", self.minimumDemerits) end
32,735✔
180
        if self.minimumDemerits < awful_bad and (self.old_l ~= self.easy_line or self.r == self.activeListHead) then
30,271✔
181
          self:createNewActiveNodes(breakType)
14,795✔
182
        end
183
        if self.r == self.activeListHead then
17,289✔
184
          if debugging then SU.debug("break", "<- tryBreak") end
21,594✔
185
          return
15,103✔
186
        end
187
        -- 876
188
        if self.easy_line and self.r.lineNumber > self.easy_line then
8,677✔
189
          self.lineWidth = self.secondWidth
14,490✔
190
          self.old_l = awful_bad -1
14,490✔
191
        else
192
          self.old_l = self.r.lineNumber
678✔
193
          if self.lastSpecialLine and self.r.lineNumber > self.lastSpecialLine then
678✔
194
            self.lineWidth = self.secondWidth
×
195
          elseif self.parShaping then
678✔
196
            local _
197
            _, self.lineWidth, _ = self:parShapeCache(self.r.lineNumber)
236✔
198
          else
199
            self.lineWidth = self.firstWidth
560✔
200
          end
201
        end
202
        if debugging then SU.debug("break", "line width =",  self.lineWidth) end
8,677✔
203
      end
204
      if debugging then
18,989✔
205
        SU.debug("break", " ---> (2) cuaw is", self.curActiveWidth)
8,955✔
NEW
206
        SU.debug("break", " ---> aw is", self.activeWidth)
×
207
      end
208
      self:considerDemerits(pi, breakType)
18,989✔
209
      if debugging then
27,944✔
210
        SU.debug("break", " <--- cuaw is", self.curActiveWidth)
8,955✔
NEW
211
        SU.debug("break", " <--- aw is ", self.activeWidth)
×
212
      end
213
    end
214
  end
215
end
216

217
-- Note: This function gets called a lot and to optimize it we're assuming that
218
-- the lengths being passed are already absolutized. This is not a safe
219
-- assumption to make universally.
220
local function fitclass(self, shortfall)
221
  shortfall = shortfall.amount
18,989✔
222
  local badness, class
223
  local stretch = self.curActiveWidth.stretch.amount
27,944✔
224
  local shrink = self.curActiveWidth.shrink.amount
27,944✔
225
  if shortfall > 0 then
27,944✔
226
    if shortfall > 110 and stretch < 25 then
26,646✔
227
      badness = inf_bad
16,825✔
228
    else
229
      badness = SU.rateBadness(inf_bad, shortfall, stretch)
18,440✔
230
    end
231
    if     badness > 99 then class = "veryLoose"
17,691✔
232
    elseif badness > 12 then class = "loose"
13,116✔
233
    else                     class = "decent"
5,708✔
234
    end
235
  else
236
    shortfall = -shortfall
1,298✔
237
    if shortfall > shrink then
1,899✔
238
      badness = inf_bad + 1
1,524✔
239
    else
240
      badness = SU.rateBadness(inf_bad, shortfall, shrink)
750✔
241
    end
242
    if badness > 12 then class = "tight"
1,298✔
243
    else                 class = "decent"
814✔
244
    end
245
  end
246
  return badness, class
18,989✔
247
end
248

249
function lineBreak:tryAlternatives(from, to)
183✔
250
  local altSizes = {}
183✔
251
  local alternates = {}
×
252
  for i = from, to do
×
253
    if self.nodes[i] and self.nodes[i].is_alternative then
×
254
      alternates[#alternates+1] = self.nodes[i]
×
255
      altSizes[#altSizes+1] = #(self.nodes[i].options)
×
256
    end
257
  end
258
  if #alternates == 0 then return end
×
259
  local localMinimum = awful_bad
×
260
  -- local selectedShortfall
261
  local shortfall = self.lineWidth - self.curActiveWidth
×
262
  if debugging then SU.debug("break", "Shortfall was ", shortfall) end
×
263
  for combination in SU.allCombinations(altSizes) do
×
264
    local addWidth = 0
×
265
    for i = 1, #(alternates) do local alternative = alternates[i]
×
266
      addWidth = (addWidth + alternative.options[combination[i]].width - alternative:minWidth())
×
267
      if debugging then SU.debug("break", alternative.options[combination[i]], " width", addWidth) end
×
268
    end
269
    local ss = shortfall - addWidth
×
270
    -- Warning, assumes abosolute
271
    local badness = SU.rateBadness(inf_bad, ss.length.amount, self.curActiveWidth[ss > 0 and "stretch" or "shrink"].length.amount)
×
NEW
272
    if debugging then SU.debug("break", "  badness of", ss, "(", self.curActiveWidth, ") is", badness) end
×
273
    if badness < localMinimum then
×
274
      self.r.alternates = alternates
×
275
      self.r.altSelections = combination
×
276
      -- selectedShortfall = addWidth
277
      localMinimum = badness
×
278
    end
279
  end
280
  if debugging then SU.debug("break", "Choosing ", alternates[1].options[self.r.altSelections[1]]) end
×
281
  -- self.curActiveWidth:___add(selectedShortfall)
282
  shortfall = self.lineWidth - self.curActiveWidth
×
283
  if debugging then SU.debug("break", "Is now ", shortfall) end
×
284
end
285

286
function lineBreak:considerDemerits(pi, breakType) -- 877
183✔
287
  self.artificialDemerits = false
19,172✔
288
  local nodeStaysActive = false
27,944✔
289
  -- self:dumpActiveRing()
290
  if self.seenAlternatives then
18,989✔
291
    self:tryAlternatives(self.r.prevBreak and self.r.prevBreak.curBreak or 1, self.r.curBreak and self.r.curBreak or 1)
8,955✔
292
  end
293
  local shortfall = self.lineWidth - self.curActiveWidth
18,989✔
294
  self.badness, self.fitClass = fitclass(self, shortfall)
46,933✔
295
  if debugging then SU.debug("break", self.badness, self.fitClass) end
36,899✔
296
  if (self.badness > inf_bad or pi == ejectPenalty) then
27,944✔
297
    if self.finalpass and self.minimumDemerits == awful_bad and self.r.next == self.activeListHead and self.prev_r == self.activeListHead then
10,842✔
298
      self.artificialDemerits = true
1,159✔
299
    else
300
      if self.badness > self.threshold then
1,806✔
301
        self:deactivateR()
1,911✔
302
        return
1,370✔
303
      end
304
    end
305
  else
306
    self.prev_r = self.r
17,102✔
307
    if self.badness > self.threshold then return end
24,979✔
308
    nodeStaysActive = true
12,350✔
309
  end
310

311
  local _shortfall = shortfall:tonumber()
5,450✔
312
  local function shortfallratio (metric)
313
    local prop = self.curActiveWidth[metric]:tonumber()
5,450✔
314
    local factor = prop ~= 0 and prop or awful_bad
6,881✔
315
    return _shortfall / factor
6,881✔
316
  end
317
  self.lastRatio = shortfallratio(_shortfall > 0 and "stretch" or "shrink")
12,331✔
318
  self:recordFeasible(pi, breakType)
8,312✔
319
  if not nodeStaysActive then self:deactivateR() end
6,881✔
320
end
321

322
function lineBreak:deactivateR() -- 886
183✔
323
  if debugging then SU.debug("break", " Deactivating r ("..self.r.type..")") end
2,070✔
324
  self.prev_r.next = self.r.next
2,965✔
325
  if self.prev_r == self.activeListHead then
2,965✔
326
    -- 887
327
    self.r = self.activeListHead.next
1,880✔
328
    if self.r.type == "delta" then
2,958✔
329
      self.activeWidth:___add(self.r.width)
2,218✔
330
      self.curActiveWidth = SILE.types.length(self.activeWidth)
2,860✔
331
      self.activeListHead.next = self.r.next
2,300✔
332
    end
333
    if debugging then SU.debug("break", "  Deactivate, branch 1"); end
1,880✔
334
  else
335
    if self.prev_r.type == "delta" then
7✔
336
      self.r = self.prev_r.next
7✔
337
      if self.r == self.activeListHead then
7✔
338
        self.curActiveWidth:___sub(self.prev_r.width)
×
339
        -- FIXME It was crashing here, so changed from:
340
        -- self.curActiveWidth:___sub(self.r.width)
341
        -- But I'm not so sure reading Knuth here...
342
        self.prev_prev_r.next = self.activeListHead
×
343
        self.prev_r = self.prev_prev_r
×
344
      elseif self.r.type == "delta" then
7✔
345
        self.curActiveWidth:___add(self.r.width)
7✔
346
        self.prev_r.width:___add(self.r.width)
7✔
347
        self.prev_r.next = self.r.next
7✔
348
      end
349
    end
350
    if debugging then SU.debug("break", "  Deactivate, branch 2"); end
7✔
351
  end
352
end
353

354
function lineBreak:computeDemerits(pi, breakType)
183✔
355
  if self.artificialDemerits then return 0 end
5,633✔
356
  local demerit = param("linePenalty") + self.badness
12,169✔
357
  if math.abs(demerit) >= 10000 then
8,077✔
358
    demerit = 100000000
1,354✔
359
  else
360
    demerit = demerit * demerit
5,369✔
361
  end
362
  if pi > 0 then
5,369✔
363
    demerit = demerit + pi * pi
1,792✔
364
  -- elseif pi == 0 then
365
  --   -- do nothing
366
  elseif pi > ejectPenalty then
4,931✔
367
    demerit = demerit - pi * pi
5,164✔
368
  end
369
  if breakType == "hyphenated" and self.r.type == "hyphenated" then
5,369✔
370
    if self.nodes[self.place] then
1,503✔
371
      demerit = demerit + param("doubleHyphenDemerits")
378✔
372
    else
373
      demerit = demerit + param("finalHyphenDemerits")
×
374
    end
375
  end
376
  -- XXX adjDemerits not added here
377
  return demerit
5,369✔
378
end
379

380
function lineBreak:recordFeasible(pi, breakType) -- 881
183✔
381
  local demerit = lineBreak:computeDemerits(pi, breakType)
5,633✔
382
  if debugging then
6,881✔
383
    if self.nodes[self.place] then
1,431✔
NEW
384
      SU.debug("break", "@", self.nodes[self.place], "via @@", (self.r.serial or "0"), "badness =", self.badness, "demerit =", demerit) -- 882
×
385
    else
386
      SU.debug("break", "@ \\par via @@")
×
387
    end
NEW
388
    SU.debug("break", " fit class =", self.fitClass)
×
389
  end
390
  demerit = demerit + self.r.totalDemerits
5,450✔
391
  if demerit <= self.bestInClass[self.fitClass].minimalDemerits then
6,881✔
392
    self.bestInClass[self.fitClass] = {
4,685✔
393
      minimalDemerits = demerit,
4,389✔
394
      node = self.r.serial and self.r,
4,389✔
395
      line = self.r.lineNumber
4,389✔
396
    }
4,389✔
397
    -- XXX do last line fit
398
    if demerit < self.minimumDemerits then self.minimumDemerits = demerit end
3,254✔
399
  end
400
end
401

402

403
function lineBreak:createNewActiveNodes(breakType) -- 862
183✔
404
  if self.no_break_yet then
1,996✔
405
    -- 863
406
    self.no_break_yet = false
1,813✔
407
    self.breakWidth = SILE.types.length(self.background)
4,670✔
408
    local place = self.place
3,901✔
409
    local node = self.nodes[place]
2,857✔
410
    if node and node.is_discretionary then -- 866
2,857✔
411
      self.breakWidth:___add(node:prebreakWidth())
1,544✔
412
      self.breakWidth:___add(node:postbreakWidth())
822✔
413
      self.breakWidth:___sub(node:replacementWidth())
822✔
414
    end
415
    while self.nodes[place] and not self.nodes[place].is_box do
4,420✔
416
      if self.sideways and self.nodes[place].height then
5,256✔
417
        self.breakWidth:___sub(self.nodes[place].height)
1,408✔
418
        self.breakWidth:___sub(self.nodes[place].depth)
×
419
      elseif self.nodes[place].width then -- We use the fact that (a) nodes know if they have width and (b) width subtraction is polymorphic
2,607✔
420
        self.breakWidth:___sub(self.nodes[place]:lineContribution())
6,622✔
421
      end
422
      place = place + 1
2,607✔
423
    end
424
    if debugging then SU.debug("break", "Value of breakWidth =", self.breakWidth) end
1,813✔
425
  end
426
  -- 869 (Add a new delta node)
427
  if self.prev_r.type == "delta" then
1,813✔
428
    self.prev_r.width:___sub(self.curActiveWidth)
948✔
429
    self.prev_r.width:___add(self.breakWidth)
24✔
430
  elseif self.prev_r == self.activeListHead then
1,801✔
431
    self.activeWidth = SILE.types.length(self.breakWidth)
2,074✔
432
  else
433
    local newDelta = { next = self.r, type = "delta", width = self.breakWidth - self.curActiveWidth }
2,464✔
434
    if debugging then SU.debug("break", "Added new delta node =", newDelta.width) end
2,428✔
435
    self.prev_r.next = newDelta
1,830✔
436
    self.prev_prev_r = self.prev_r
1,830✔
437
    self.prev_r = newDelta
1,830✔
438
  end
439
  if math.abs(self.adjdemerits) >= (awful_bad - self.minimumDemerits) then
1,813✔
440
    self.minimumDemerits = awful_bad - 1
936✔
441
  else
442
    self.minimumDemerits = self.minimumDemerits + math.abs(self.adjdemerits)
1,813✔
443
  end
444

445
  for i = 1, #classes do
9,065✔
446
    local class = classes[i]
11,932✔
447
    local best = self.bestInClass[class]
10,996✔
448
    local value = best.minimalDemerits
10,996✔
449
    if debugging then SU.debug("break", "Class is", class, "Best value here is", value) end
10,996✔
450

451
    if value <= self.minimumDemerits then
7,252✔
452
      -- 871: this is what creates new active notes
453
      passSerial = passSerial + 1
1,883✔
454

455
      local newActive = {
1,883✔
456
        type = breakType,
2,824✔
457
        next = self.r,
2,824✔
458
        curBreak = self.place,
2,824✔
459
        prevBreak = best.node,
2,824✔
460
        serial = passSerial,
2,824✔
461
        ratio = self.lastRatio,
2,824✔
462
        lineNumber = best.line + 1,
2,824✔
463
        fitness = class,
2,824✔
464
        totalDemerits = value
2,824✔
465
      }
941✔
466
      -- DoLastLineFit? 1636 XXX
467
      self.prev_r.next = newActive
1,883✔
468
      self.prev_r = newActive
2,824✔
469
      self:dumpBreakNode(newActive)
2,824✔
470

471
    end
472
    self.bestInClass[class] = { minimalDemerits = awful_bad }
7,252✔
473
  end
474

475
  self.minimumDemerits = awful_bad
1,813✔
476
  -- 870
477
  if self.r ~= self.activeListHead then
1,813✔
478
    local newDelta = { next = self.r, type = "delta", width = self.curActiveWidth - self.breakWidth }
960✔
479
    self.prev_r.next = newDelta
12✔
480
    self.prev_prev_r = self.prev_r
12✔
481
    self.prev_r = newDelta
12✔
482
  end
483
end
484

485
function lineBreak.dumpBreakNode(_, node)
183✔
486
  if not SU.debugging("break") then return end
3,914✔
487
  SU.debug("break", lineBreak:describeBreakNode(node))
1,882✔
488
end
489

490
function lineBreak:describeBreakNode(node)
183✔
491
  --SU.debug("break", "@@", b.serial, ": line", b.lineNumber - 1, ".", b.fitness, b.type, "t=", b.totalDemerits, "-> @@", b.prevBreak and b.prevBreak.serial or "0")
492
  if node.sentinel then return node.sentinel end
×
493
  if node.type == "delta" then return "delta "..node.width.."pt" end
×
494
  local before = self.nodes[node.curBreak-1]
×
495
  local after = self.nodes[node.curBreak+1]
×
496
  local from = node.prevBreak and node.prevBreak.curBreak or 1
×
497
  local to = node.curBreak
×
498
  return ("b %s-%s \"%s | %s\" [%s, %s]"):format(from, to, before and before:toText() or "", after and after:toText() or "", node.totalDemerits, node.fitness)
×
499
end
500

501
-- NOTE: this function is called many thousands of times even in single
502
-- page documents. Speed is more important than pretty code here.
503
function lineBreak:checkForLegalBreak(node) -- 892
183✔
504
  if debugging then SU.debug("break", "considering node "..node); end
17,618✔
505
  local previous = self.nodes[self.place - 1]
29,704✔
506
  if node.is_alternative then self.seenAlternatives = true end
29,704✔
507
  if self.sideways and node.is_box then
29,704✔
508
    self.activeWidth:___add(node.height)
12,234✔
509
    self.activeWidth:___add(node.depth)
×
510
  elseif self.sideways and node.is_vglue then
17,470✔
511
    if previous and previous.is_box then
12,234✔
512
      self:tryBreak()
×
513
    end
514
    self.activeWidth:___add(node.height)
×
515
    self.activeWidth:___add(node.depth)
×
516
  elseif node.is_alternative then
17,470✔
517
    self.activeWidth:___add(node:minWidth())
12,234✔
518
  elseif node.is_box then
17,470✔
519
    self.activeWidth:___add(node:lineContribution())
37,752✔
520
  elseif node.is_glue then
27,570✔
521
    -- 894 (We removed the auto_breaking parameter)
522
    if previous and previous.is_box then self:tryBreak() end
5,888✔
523
    self.activeWidth:___add(node.width)
15,496✔
524
  elseif node.is_kern then
10,516✔
525
    self.activeWidth:___add(node.width)
2,814✔
526
  elseif node.is_discretionary then -- 895
3,003✔
527
    self.activeWidth:___add(node:prebreakWidth())
5,685✔
528
    self:tryBreak()
4,985✔
529
    self.activeWidth:___sub(node:prebreakWidth())
5,089✔
530
    self.activeWidth:___add(node:replacementWidth())
8,447✔
531
  elseif node.is_penalty then
5,975✔
532
    self:tryBreak()
1,681✔
533
  end
534
end
535

536
function lineBreak:tryFinalBreak()      -- 899
183✔
537
  -- XXX TeX has self:tryBreak() here. But this doesn't seem to work
538
  -- for us. If we call tryBreak(), we end up demoting all break points
539
  -- to veryLoose (possibly because the active width gets reset - why?).
540
  -- This means we end up doing unnecessary passes.
541
  -- However, there doesn't seem to be any downside to not calling it
542
  -- (how scary is that?) so I have removed it for now. With this
543
  -- "fix", we only perform hyphenation and emergency passes when necessary
544
  -- instead of every single time. If things go strange with the break
545
  -- algorithm in the future, this should be the first place to look!
546
  -- self:tryBreak()
547
  if self.activeListHead.next == self.activeListHead then return end
504✔
548
  self.r = self.activeListHead.next
779✔
549
  local fewestDemerits = awful_bad
779✔
550
  repeat
551
    if self.r.type ~= "delta" and self.r.totalDemerits < fewestDemerits then
1,070✔
552
      fewestDemerits = self.r.totalDemerits
876✔
553
      self.bestBet = self.r
780✔
554
    end
555
    self.r = self.r.next
699✔
556
  until self.r == self.activeListHead
1,070✔
557
  if param("looseness") == 0 then return true end
1,379✔
558
  -- XXX node 901 not implemented
559
  if self.actualLooseness == param("looseness") or self.finalpass then
×
560
    return true
×
561
  end
562
end
563

564
function lineBreak:doBreak (nodes, hsize, sideways)
183✔
565
  passSerial = 1
652✔
566
  debugging = SILE.debugFlags["break"]
779✔
567
  self.seenAlternatives = false
779✔
568
  self.nodes = nodes
779✔
569
  self.hsize = hsize
779✔
570
  self.sideways = sideways
779✔
571
  self:init()
779✔
572
  self.adjdemerits = param("adjdemerits")
1,283✔
573
  self.threshold = param("pretolerance")
1,558✔
574
  if self.threshold >= 0 then
1,054✔
575
    self.pass = "first"
779✔
576
    self.finalpass = false
779✔
577
  else
578
    self.threshold = param("tolerance")
×
579
    self.pass = "second"
×
580
    self.finalpass = param("emergencyStretch") <= 0
×
581
  end
582
  -- 889
583
  while 1 do
606✔
584
    if debugging then SU.debug("break", "@", self.pass, "pass") end
949✔
585
    if self.threshold > inf_bad then self.threshold = inf_bad end
949✔
586
    if self.pass == "second" then
949✔
587
      self.nodes = SILE.hyphenate(self.nodes)
487✔
588
      SILE.typesetter.state.nodes = self.nodes -- Horrible breaking of separation of concerns here. :-(
162✔
589
    end
590
    -- 890
591
    self.activeListHead = {
606✔
592
      sentinel="START",
343✔
593
      type = "hyphenated",
594
      lineNumber = awful_bad,
606✔
595
      subtype = 0
343✔
596
    } -- 846
606✔
597
    self.activeListHead.next = {
949✔
598
      sentinel="END",
343✔
599
      type = "unhyphenated",
600
      fitness = "decent",
601
      next = self.activeListHead,
606✔
602
      lineNumber = param("prevGraf") + 1,
1,555✔
603
      totalDemerits = 0
686✔
604
    }
606✔
605

606
    -- Not doing 1630
607
    self.activeWidth = SILE.types.length(self.background)
1,212✔
608

609
    self.place = 1
606✔
610
    while self.nodes[self.place] and self.activeListHead.next ~= self.activeListHead do
18,419✔
611
      self:checkForLegalBreak(self.nodes[self.place])
30,047✔
612
      self.place = self.place + 1
29,704✔
613
    end
614
    if self.place > #self.nodes then
606✔
615
      if self:tryFinalBreak() then break end
1,351✔
616
    end
617
    -- (Not doing 891)
618
    if self.pass ~= "second" then
102✔
619
      self.pass = "second"
140✔
620
      self.threshold = param("tolerance")
189✔
621
    else
622
      self.pass = "emergency"
30✔
623
      self.background.stretch:___add(param("emergencyStretch"))
83✔
624
      self.finalpass = true
76✔
625
    end
626
  end
627
  -- Not doing 1638
628
  return self:postLineBreak()
504✔
629
end
630

631
function lineBreak:postLineBreak() -- 903
183✔
632
  local p = self.bestBet
652✔
633
  local breaks = {}
779✔
634
  local line  = 1
779✔
635

636
  local nbLines = 0
504✔
637
  local p2 = p
779✔
638
  repeat
639
    nbLines = nbLines + 1
1,388✔
640
    p2 = p2.prevBreak
1,388✔
641
  until not p2
1,388✔
642

643
  repeat
644
    local left, _, right
645
    -- SILE handles the actual line width differently than TeX,
646
    -- so below always return a width of self.hsize. Would they
647
    -- be needed at some point, the exact width are commented out
648
    -- below.
649
    if self.parShaping then
848✔
650
      left, _, right = self:parShapeCache(nbLines + 1 - line)
570✔
651
    else
652
      if self.hangAfter == 0 then
833✔
653
        -- width = self.hsize
654
        left = 0
772✔
655
        right = 0
1,312✔
656
      else
657
        local indent
658
        if self.hangAfter > 0 then
61✔
659
          -- width = line > nbLines - self.hangAfter and self.firstWidth or self.secondWidth
660
          indent = line > nbLines - self.hangAfter and 0 or self.hangIndent
16✔
661
        else
662
          -- width = line > nbLines + self.hangAfter and self.firstWidth or self.secondWidth
663
          indent = line > nbLines + self.hangAfter and self.hangIndent or 0
45✔
664
        end
665
        if indent > 0 then
61✔
666
          left = indent
26✔
667
          right = 0
26✔
668
        else
669
          left = 0
35✔
670
          right = -indent
35✔
671
        end
672
      end
673
    end
674

675
    table.insert(breaks, 1,  {
1,696✔
676
        position = p.curBreak,
1,928✔
677
        width = self.hsize,
1,388✔
678
        left = left,
1,388✔
679
        right = right
1,388✔
680
      })
540✔
681
    if p.alternates then
848✔
682
      for i = 1, #p.alternates do
540✔
683
        p.alternates[i].selected = p.altSelections[i]
×
684
        p.alternates[i].width = p.alternates[i].options[p.altSelections[i]].width
×
685
      end
686
    end
687
    p = p.prevBreak
848✔
688
    line = line + 1
1,388✔
689
  until not p
1,388✔
690
  self:parShapeCacheClear()
1,044✔
691
  return breaks
779✔
692
end
693

694
function lineBreak:dumpActiveRing()
183✔
695
  local p = self.activeListHead
148✔
696
  if not SILE.quiet then
×
697
    io.stderr:write("\n")
×
698
  end
699
  repeat
700
    if not SILE.quiet then
×
701
      if p == self.r then io.stderr:write("-> ") else io.stderr:write("   ") end
×
702
    end
703
    SU.debug("break", lineBreak:describeBreakNode(p))
×
704
    p = p.next
×
705
  until p == self.activeListHead
×
706
end
707

708
return lineBreak
183✔
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