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

sile-typesetter / sile / 6915746301

18 Nov 2023 07:02PM UTC coverage: 56.433% (-12.3%) from 68.751%
6915746301

push

github

web-flow
Merge 8b3fdc301 into f64e235fa

8729 of 15468 relevant lines covered (56.43%)

932.75 hits per line

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

71.11
/core/break.lua
1
SILE.settings:declare({ parameter = "linebreak.parShape", type = "boolean", default = false,
34✔
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 })
34✔
4
SILE.settings:declare({ parameter = "linebreak.pretolerance", type = "integer or nil", default = 100 })
34✔
5
SILE.settings:declare({ parameter = "linebreak.hangIndent", type = "measurement", default = 0 })
34✔
6
SILE.settings:declare({ parameter = "linebreak.hangAfter", type = "integer or nil", default = nil })
34✔
7
SILE.settings:declare({ parameter = "linebreak.adjdemerits", type = "integer", default = 10000,
34✔
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 })
34✔
10
SILE.settings:declare({ parameter = "linebreak.prevGraf", type = "integer", default = 0 })
34✔
11
SILE.settings:declare({ parameter = "linebreak.emergencyStretch", type = "measurement", default = 0 })
34✔
12
SILE.settings:declare({ parameter = "linebreak.doLastLineFit", type = "boolean", default = false }) -- unimplemented
34✔
13
SILE.settings:declare({ parameter = "linebreak.linePenalty", type = "integer", default = 10 })
34✔
14
SILE.settings:declare({ parameter = "linebreak.hyphenPenalty", type = "integer", default = 50 })
34✔
15
SILE.settings:declare({ parameter = "linebreak.doubleHyphenDemerits", type = "integer", default = 10000 })
34✔
16
SILE.settings:declare({ parameter = "linebreak.finalHyphenDemerits", type = "integer", default = 5000 })
34✔
17

18
-- doubleHyphenDemerits
19
-- hyphenPenalty
20

21
local classes = { "tight"; "decent"; "loose"; "veryLoose" }
34✔
22
local passSerial = 0
34✔
23
local awful_bad = 1073741823
34✔
24
local inf_bad = 10000
34✔
25
local ejectPenalty = -inf_bad
34✔
26
local lineBreak = {}
34✔
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)
3,332✔
44
  return type(value) == "table" and value:absolute() or value
3,458✔
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
34✔
50

51
function lineBreak:init()
34✔
52
  self:trimGlue() -- 842
123✔
53
  -- 849
54
  self.activeWidth = SILE.length()
246✔
55
  self.curActiveWidth = SILE.length()
246✔
56
  self.breakWidth = SILE.length()
246✔
57
  -- 853
58
  local rskip = (SILE.settings:get("document.rskip") or SILE.nodefactory.glue()).width:absolute()
327✔
59
  local lskip = (SILE.settings:get("document.lskip") or SILE.nodefactory.glue()).width:absolute()
326✔
60
  self.background = rskip + lskip
246✔
61
  -- 860
62
  self.bestInClass = {}
123✔
63
  for i = 1, #classes do
615✔
64
    self.bestInClass[classes[i]] = {
492✔
65
      minimalDemerits = awful_bad
492✔
66
    }
492✔
67
  end
68
  self.minimumDemerits = awful_bad
123✔
69
  self:setupLineLengths()
123✔
70
end
71

72
function lineBreak:trimGlue() -- 842
34✔
73
  local nodes = self.nodes
123✔
74
  if nodes[#nodes].is_glue then nodes[#nodes] = nil end
123✔
75
  nodes[#nodes+1] = SILE.nodefactory.penalty(inf_bad)
246✔
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(_)
34✔
97
  return 0, self.hsize, 0
×
98
end
99

100
local parShapeCache = {}
34✔
101

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

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

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

127
function lineBreak:setupLineLengths() -- 874
34✔
128
  self.parShaping = param("parShape") or false
246✔
129
  if self.parShaping then
123✔
130
    self.lastSpecialLine = nil
×
131
    self.easy_line = nil
×
132
  else
133
    self.hangAfter = param("hangAfter") or 0
246✔
134
    self.hangIndent = param("hangIndent"):tonumber()
369✔
135
    if self.hangIndent == 0 then
123✔
136
      self.lastSpecialLine = 0
123✔
137
      self.secondWidth = self.hsize or SU.error("No hsize")
123✔
138
    else -- 875
139
      self.lastSpecialLine = math.abs(self.hangAfter)
×
140
      if self.hangAfter < 0 then
×
141
        self.secondWidth = self.hsize or SU.error("No hsize")
×
142
        self.firstWidth = self.hsize - math.abs(self.hangIndent)
×
143
      else
144
        self.firstWidth = self.hsize or SU.error("No hsize")
×
145
        self.secondWidth = self.hsize - math.abs(self.hangIndent)
×
146
      end
147
    end
148
    if param("looseness") == 0 then self.easy_line = self.lastSpecialLine else self.easy_line = awful_bad end
246✔
149
    -- self.easy_line = awful_bad
150
  end
151
end
152

153
function lineBreak:tryBreak() -- 855
34✔
154
  local pi, breakType
155
  local node = self.nodes[self.place]
1,573✔
156
  if not node then pi = ejectPenalty; breakType = "hyphenated"
1,573✔
157
  elseif node.is_discretionary then breakType = "hyphenated"; pi = param("hyphenPenalty")
1,792✔
158
  else breakType = "unhyphenated"; pi = node.penalty or 0 end
1,354✔
159
  if debugging then SU.debug("break", "Trying a ", breakType, "break p =", pi) end
1,573✔
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
1,573✔
161
  self.prev_prev_r = nil
1,573✔
162
  self.prev_r = self.activeListHead
1,573✔
163
  self.old_l = 0
1,573✔
164
  self.r = nil
1,573✔
165
  self.curActiveWidth = SILE.length(self.activeWidth)
3,146✔
166
  while true do
167
    while true do -- allows "break" to function as "continue"
168
      self.r = self.prev_r.next
9,321✔
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
9,321✔
170
      if self.r.type == "delta" then -- 858
9,321✔
171
        if debugging then SU.debug("break", " Adding delta node width of", self.r.width) end
2,731✔
172
        self.curActiveWidth:___add(self.r.width)
2,731✔
173
        self.prev_prev_r = self.prev_r
2,731✔
174
        self.prev_r = self.r
2,731✔
175
        break
2,731✔
176
      end
177
      -- 861
178
      if self.r.lineNumber > self.old_l then
6,590✔
179
        if debugging then SU.debug("break", "Minimum demerits = " .. self.minimumDemerits) end
3,146✔
180
        if self.minimumDemerits < awful_bad and (self.old_l ~= self.easy_line or self.r == self.activeListHead) then
3,146✔
181
          self:createNewActiveNodes(breakType)
536✔
182
        end
183
        if self.r == self.activeListHead then
3,146✔
184
          if debugging then SU.debug("break", "<- tryBreak") end
1,573✔
185
          return
1,573✔
186
        end
187
        -- 876
188
        if self.easy_line and self.r.lineNumber > self.easy_line then
1,573✔
189
          self.lineWidth = self.secondWidth
1,573✔
190
          self.old_l = awful_bad -1
1,573✔
191
        else
192
          self.old_l = self.r.lineNumber
×
193
          if self.lastSpecialLine and self.r.lineNumber > self.lastSpecialLine then
×
194
            self.lineWidth = self.secondWidth
×
195
          elseif self.parShaping then
×
196
            local _
197
            _, self.lineWidth, _ = self:parShapeCache(self.r.lineNumber)
×
198
          else
199
            self.lineWidth = self.firstWidth
×
200
          end
201
        end
202
        if debugging then SU.debug("break", "line width = " .. tostring(self.lineWidth)) end
1,573✔
203
      end
204
      if debugging then
5,017✔
205
        SU.debug("break", " ---> (2) cuaw is " .. tostring(self.curActiveWidth))
×
206
        SU.debug("break", " ---> aw is " .. tostring(self.activeWidth))
×
207
      end
208
      self:considerDemerits(pi, breakType)
5,017✔
209
      if debugging then
5,017✔
210
        SU.debug("break", " <--- cuaw is " .. tostring(self.curActiveWidth))
×
211
        SU.debug("break", " <--- aw is " .. tostring(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
5,017✔
222
  local badness, class
223
  local stretch = self.curActiveWidth.stretch.amount
5,017✔
224
  local shrink = self.curActiveWidth.shrink.amount
5,017✔
225
  if shortfall > 0 then
5,017✔
226
    if shortfall > 110 and stretch < 25 then
4,663✔
227
      badness = inf_bad
1,690✔
228
    else
229
      badness = SU.rateBadness(inf_bad, shortfall, stretch)
5,946✔
230
    end
231
    if     badness > 99 then class = "veryLoose"
4,663✔
232
    elseif badness > 12 then class = "loose"
1,905✔
233
    else                     class = "decent"
1,871✔
234
    end
235
  else
236
    shortfall = -shortfall
354✔
237
    if shortfall > shrink then
354✔
238
      badness = inf_bad + 1
251✔
239
    else
240
      badness = SU.rateBadness(inf_bad, shortfall, shrink)
206✔
241
    end
242
    if badness > 12 then class = "tight"
354✔
243
    else                 class = "decent"
46✔
244
    end
245
  end
246
  return badness, class
5,017✔
247
end
248

249
function lineBreak:tryAlternatives(from, to)
34✔
250
  local altSizes = {}
×
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)
×
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
34✔
287
  self.artificialDemerits = false
5,017✔
288
  local nodeStaysActive = false
5,017✔
289
  -- self:dumpActiveRing()
290
  local shortfall = self.lineWidth - self.curActiveWidth
5,017✔
291
  if self.seenAlternatives then
5,017✔
292
    self:tryAlternatives(self.r.prevBreak and self.r.prevBreak.curBreak or 1, self.r.curBreak and self.r.curBreak or 1, shortfall)
×
293
  end
294
  shortfall = self.lineWidth - self.curActiveWidth
5,017✔
295
  self.badness, self.fitClass = fitclass(self, shortfall)
10,034✔
296
  if debugging then SU.debug("break", self.badness .. " " .. self.fitClass) end
5,017✔
297
  if (self.badness > inf_bad or pi == ejectPenalty) then
5,017✔
298
    if self.finalpass and self.minimumDemerits == awful_bad and self.r.next == self.activeListHead and self.prev_r == self.activeListHead then
532✔
299
      self.artificialDemerits = true
6✔
300
    else
301
      if self.badness > self.threshold then
526✔
302
        self:deactivateR()
249✔
303
        return
249✔
304
      end
305
    end
306
  else
307
    self.prev_r = self.r
4,485✔
308
    if self.badness > self.threshold then return end
4,485✔
309
    nodeStaysActive = true
1,794✔
310
  end
311

312
  local _shortfall = shortfall:tonumber()
2,077✔
313
  local function shortfallratio (metric)
314
    local prop = self.curActiveWidth[metric]:tonumber()
2,077✔
315
    local factor = prop ~= 0 and prop or awful_bad
2,077✔
316
    return _shortfall / factor
2,077✔
317
  end
318
  self.lastRatio = shortfallratio(_shortfall > 0 and "stretch" or "shrink")
4,154✔
319
  self:recordFeasible(pi, breakType)
2,077✔
320
  if not nodeStaysActive then self:deactivateR() end
2,077✔
321
end
322

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

355
function lineBreak:computeDemerits(pi, breakType)
34✔
356
  if self.artificialDemerits then return 0 end
2,077✔
357
  local demerit = param("linePenalty") + self.badness
4,142✔
358
  if math.abs(demerit) >= 10000 then
2,071✔
359
    demerit = 100000000
×
360
  else
361
    demerit = demerit * demerit
2,071✔
362
  end
363
  if pi > 0 then
2,071✔
364
    demerit = demerit + pi * pi
130✔
365
  -- elseif pi == 0 then
366
  --   -- do nothing
367
  elseif pi > ejectPenalty then
1,941✔
368
    demerit = demerit - pi * pi
1,664✔
369
  end
370
  if breakType == "hyphenated" and self.r.type == "hyphenated" then
2,071✔
371
    if self.nodes[self.place] then
38✔
372
      demerit = demerit + param("doubleHyphenDemerits")
76✔
373
    else
374
      demerit = demerit + param("finalHyphenDemerits")
×
375
    end
376
  end
377
  -- XXX adjDemerits not added here
378
  return demerit
2,071✔
379
end
380

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

403

404
function lineBreak:createNewActiveNodes(breakType) -- 862
34✔
405
  if self.no_break_yet then
536✔
406
    -- 863
407
    self.no_break_yet = false
536✔
408
    self.breakWidth = SILE.length(self.background)
1,072✔
409
    local place = self.place
536✔
410
    local node = self.nodes[place]
536✔
411
    if node and node.is_discretionary then -- 866
536✔
412
      self.breakWidth:___add(node:prebreakWidth())
106✔
413
      self.breakWidth:___add(node:postbreakWidth())
106✔
414
      self.breakWidth:___sub(node:replacementWidth())
106✔
415
    end
416
    while self.nodes[place] and not self.nodes[place].is_box do
1,289✔
417
      if self.sideways and self.nodes[place].height then
753✔
418
        self.breakWidth:___sub(self.nodes[place].height)
×
419
        self.breakWidth:___sub(self.nodes[place].depth)
×
420
      elseif self.nodes[place].width then -- We use the fact that (a) nodes know if they have width and (b) width subtraction is polymorphic
753✔
421
        self.breakWidth:___sub(self.nodes[place]:lineContribution())
1,506✔
422
      end
423
      place = place + 1
753✔
424
    end
425
    if debugging then SU.debug("break", "Value of breakWidth = " .. tostring(self.breakWidth)) end
536✔
426
  end
427
  -- 869 (Add a new delta node)
428
  if self.prev_r.type == "delta" then
536✔
429
    self.prev_r.width:___sub(self.curActiveWidth)
×
430
    self.prev_r.width:___add(self.breakWidth)
×
431
  elseif self.prev_r == self.activeListHead then
536✔
432
    self.activeWidth = SILE.length(self.breakWidth)
252✔
433
  else
434
    local newDelta = { next = self.r, type = "delta", width = self.breakWidth - self.curActiveWidth }
820✔
435
    if debugging then SU.debug("break", "Added new delta node = " .. tostring(newDelta.width)) end
410✔
436
    self.prev_r.next = newDelta
410✔
437
    self.prev_prev_r = self.prev_r
410✔
438
    self.prev_r = newDelta
410✔
439
  end
440
  if math.abs(self.adjdemerits) >= (awful_bad - self.minimumDemerits) then
536✔
441
    self.minimumDemerits = awful_bad - 1
×
442
  else
443
    self.minimumDemerits = self.minimumDemerits + math.abs(self.adjdemerits)
536✔
444
  end
445

446
  for i = 1, #classes do
2,680✔
447
    local class = classes[i]
2,144✔
448
    local best = self.bestInClass[class]
2,144✔
449
    local value = best.minimalDemerits
2,144✔
450
    if debugging then SU.debug("break", "Class is "..class.." Best value here is " .. value) end
2,144✔
451

452
    if value <= self.minimumDemerits then
2,144✔
453
      -- 871: this is what creates new active notes
454
      passSerial = passSerial + 1
569✔
455

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

472
    end
473
    self.bestInClass[class] = { minimalDemerits = awful_bad }
2,144✔
474
  end
475

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

486
function lineBreak.dumpBreakNode(_, node)
34✔
487
  if not SU.debugging("break") then return end
1,138✔
488
  SU.debug("break", lineBreak:describeBreakNode(node))
×
489
end
490

491
function lineBreak:describeBreakNode(node)
34✔
492
  --print("@@" .. b.serial .. ": line " .. (b.lineNumber -1) .. "." .. b.fitness .. " " .. b.type .. " t=".. b.totalDemerits .. " -> @@ " .. (b.prevBreak and b.prevBreak.serial or "0") )
493
  if node.sentinel then return node.sentinel end
×
494
  if node.type == "delta" then return "delta "..node.width.."pt" end
×
495
  local before = self.nodes[node.curBreak-1]
×
496
  local after = self.nodes[node.curBreak+1]
×
497
  local from = node.prevBreak and node.prevBreak.curBreak or 1
×
498
  local to = node.curBreak
×
499
  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)
×
500
end
501

502
-- NOTE: this function is called many thousands of times even in single
503
-- page documents. Speed is more important than pretty code here.
504
function lineBreak:checkForLegalBreak(node) -- 892
34✔
505
  if debugging then SU.debug("break", "considering node "..node); end
3,149✔
506
  local previous = self.nodes[self.place - 1]
3,149✔
507
  if node.is_alternative then self.seenAlternatives = true end
3,149✔
508
  if self.sideways and node.is_box then
3,149✔
509
    self.activeWidth:___add(node.height)
×
510
    self.activeWidth:___add(node.depth)
×
511
  elseif self.sideways and node.is_vglue then
3,149✔
512
    if previous and previous.is_box then
×
513
      self:tryBreak()
×
514
    end
515
    self.activeWidth:___add(node.height)
×
516
    self.activeWidth:___add(node.depth)
×
517
  elseif node.is_alternative then
3,149✔
518
    self.activeWidth:___add(node:minWidth())
×
519
  elseif node.is_box then
3,149✔
520
    self.activeWidth:___add(node:lineContribution())
4,650✔
521
  elseif node.is_glue then
1,599✔
522
    -- 894 (We removed the auto_breaking parameter)
523
    if previous and previous.is_box then self:tryBreak() end
1,105✔
524
    self.activeWidth:___add(node.width)
2,210✔
525
  elseif node.is_kern then
494✔
526
    self.activeWidth:___add(node.width)
30✔
527
  elseif node.is_discretionary then -- 895
479✔
528
    self.activeWidth:___add(node:prebreakWidth())
438✔
529
    self:tryBreak()
219✔
530
    self.activeWidth:___sub(node:prebreakWidth())
438✔
531
    self.activeWidth:___add(node:replacementWidth())
657✔
532
  elseif node.is_penalty then
260✔
533
    self:tryBreak()
260✔
534
  end
535
end
536

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

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

607
    -- Not doing 1630
608
    self.activeWidth = SILE.length(self.background)
266✔
609

610
    self.place = 1
133✔
611
    while self.nodes[self.place] and self.activeListHead.next ~= self.activeListHead do
3,282✔
612
      self:checkForLegalBreak(self.nodes[self.place])
3,149✔
613
      self.place = self.place + 1
3,149✔
614
    end
615
    if self.place > #self.nodes then
133✔
616
      if self:tryFinalBreak() then break end
246✔
617
    end
618
    -- (Not doing 891)
619
    if self.pass ~= "second" then
10✔
620
      self.pass = "second"
7✔
621
      self.threshold = param("tolerance")
14✔
622
    else
623
      self.pass = "emergency"
3✔
624
      self.background.stretch:___add(param("emergencyStretch"))
6✔
625
      self.finalpass = true
3✔
626
    end
627
  end
628
  -- Not doing 1638
629
  return self:postLineBreak()
123✔
630
end
631

632
function lineBreak:postLineBreak() -- 903
34✔
633
  local p = self.bestBet
123✔
634
  local breaks = {}
123✔
635
  local line  = 1
123✔
636

637
  local nbLines = 0
123✔
638
  local p2 = p
123✔
639
  repeat
640
    nbLines = nbLines + 1
175✔
641
    p2 = p2.prevBreak
175✔
642
  until not p2
175✔
643

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

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

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

709
return lineBreak
34✔
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