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

sile-typesetter / sile / 6473680551

10 Oct 2023 07:02PM UTC coverage: 74.295%. Remained the same
6473680551

push

github

web-flow
Merge pull request #1885 from sile-typesetter/spell

9 of 9 new or added lines in 5 files covered. (100.0%)

11723 of 15779 relevant lines covered (74.29%)

6975.7 hits per line

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

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

18
-- doubleHyphenDemerits
19
-- hyphenPenalty
20

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

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

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

100
local parShapeCache = {}
172✔
101

102
local grantLeftoverWidth = function (hsize, l, w, r)
103
  local width = SILE.measurement(w or hsize)
133✔
104
  if not w and l then width = width - SILE.measurement(l) end
149✔
105
  if not w and r then width = width - SILE.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)
172✔
114
  local cache = parShapeCache[n]
133✔
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(_)
172✔
124
  pl.tablex.clear(parShapeCache)
829✔
125
end
126

127
function lineBreak:setupLineLengths() -- 874
172✔
128
  self.parShaping = param("parShape") or false
1,658✔
129
  if self.parShaping then
829✔
130
    self.lastSpecialLine = nil
2✔
131
    self.easy_line = nil
2✔
132
  else
133
    self.hangAfter = param("hangAfter") or 0
1,654✔
134
    self.hangIndent = param("hangIndent"):tonumber()
2,481✔
135
    if self.hangIndent == 0 then
827✔
136
      self.lastSpecialLine = 0
822✔
137
      self.secondWidth = self.hsize or SU.error("No hsize")
822✔
138
    else -- 875
139
      self.lastSpecialLine = math.abs(self.hangAfter)
5✔
140
      if self.hangAfter < 0 then
5✔
141
        self.secondWidth = self.hsize or SU.error("No hsize")
3✔
142
        self.firstWidth = self.hsize - math.abs(self.hangIndent)
6✔
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,654✔
149
    -- self.easy_line = awful_bad
150
  end
151
end
152

153
function lineBreak:tryBreak() -- 855
172✔
154
  local pi, breakType
155
  local node = self.nodes[self.place]
15,551✔
156
  if not node then pi = ejectPenalty; breakType = "hyphenated"
15,551✔
157
  elseif node.is_discretionary then breakType = "hyphenated"; pi = param("hyphenPenalty")
19,399✔
158
  else breakType = "unhyphenated"; pi = node.penalty or 0 end
11,703✔
159
  if debugging then SU.debug("break", "Trying a ", breakType, "break p =", pi) end
15,551✔
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,551✔
161
  self.prev_prev_r = nil
15,551✔
162
  self.prev_r = self.activeListHead
15,551✔
163
  self.old_l = 0
15,551✔
164
  self.r = nil
15,551✔
165
  self.curActiveWidth = SILE.length(self.activeWidth)
31,102✔
166
  while true do
167
    while true do -- allows "break" to function as "continue"
168
      self.r = self.prev_r.next
54,806✔
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
54,806✔
170
      if self.r.type == "delta" then -- 858
54,806✔
171
        if debugging then SU.debug("break", " Adding delta node width of", self.r.width) end
10,318✔
172
        self.curActiveWidth:___add(self.r.width)
10,318✔
173
        self.prev_prev_r = self.prev_r
10,318✔
174
        self.prev_r = self.r
10,318✔
175
        break
10,318✔
176
      end
177
      -- 861
178
      if self.r.lineNumber > self.old_l then
44,488✔
179
        if debugging then SU.debug("break", "Minimum demerits = " .. self.minimumDemerits) end
31,129✔
180
        if self.minimumDemerits < awful_bad and (self.old_l ~= self.easy_line or self.r == self.activeListHead) then
31,129✔
181
          self:createNewActiveNodes(breakType)
2,894✔
182
        end
183
        if self.r == self.activeListHead then
31,129✔
184
          if debugging then SU.debug("break", "<- tryBreak") end
15,551✔
185
          return
15,551✔
186
        end
187
        -- 876
188
        if self.easy_line and self.r.lineNumber > self.easy_line then
15,578✔
189
          self.lineWidth = self.secondWidth
15,205✔
190
          self.old_l = awful_bad -1
15,205✔
191
        else
192
          self.old_l = self.r.lineNumber
373✔
193
          if self.lastSpecialLine and self.r.lineNumber > self.lastSpecialLine then
373✔
194
            self.lineWidth = self.secondWidth
×
195
          elseif self.parShaping then
373✔
196
            local _
197
            _, self.lineWidth, _ = self:parShapeCache(self.r.lineNumber)
236✔
198
          else
199
            self.lineWidth = self.firstWidth
255✔
200
          end
201
        end
202
        if debugging then SU.debug("break", "line width = " .. tostring(self.lineWidth)) end
15,578✔
203
      end
204
      if debugging then
28,937✔
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)
28,937✔
209
      if debugging then
28,937✔
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
28,937✔
222
  local badness, class
223
  local stretch = self.curActiveWidth.stretch.amount
28,937✔
224
  local shrink = self.curActiveWidth.shrink.amount
28,937✔
225
  if shortfall > 0 then
28,937✔
226
    if shortfall > 110 and stretch < 25 then
27,009✔
227
      badness = inf_bad
13,411✔
228
    else
229
      badness = SU.rateBadness(inf_bad, shortfall, stretch)
27,196✔
230
    end
231
    if     badness > 99 then class = "veryLoose"
27,009✔
232
    elseif badness > 12 then class = "loose"
5,996✔
233
    else                     class = "decent"
5,697✔
234
    end
235
  else
236
    shortfall = -shortfall
1,928✔
237
    if shortfall > shrink then
1,928✔
238
      badness = inf_bad + 1
1,400✔
239
    else
240
      badness = SU.rateBadness(inf_bad, shortfall, shrink)
1,056✔
241
    end
242
    if badness > 12 then class = "tight"
1,928✔
243
    else                 class = "decent"
271✔
244
    end
245
  end
246
  return badness, class
28,937✔
247
end
248

249
function lineBreak:tryAlternatives(from, to)
172✔
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
172✔
287
  self.artificialDemerits = false
28,937✔
288
  local nodeStaysActive = false
28,937✔
289
  -- self:dumpActiveRing()
290
  local shortfall = self.lineWidth - self.curActiveWidth
28,937✔
291
  if self.seenAlternatives then
28,937✔
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
28,937✔
295
  self.badness, self.fitClass = fitclass(self, shortfall)
57,874✔
296
  if debugging then SU.debug("break", self.badness .. " " .. self.fitClass) end
28,937✔
297
  if (self.badness > inf_bad or pi == ejectPenalty) then
28,937✔
298
    if self.finalpass and self.minimumDemerits == awful_bad and self.r.next == self.activeListHead and self.prev_r == self.activeListHead then
2,977✔
299
      self.artificialDemerits = true
136✔
300
    else
301
      if self.badness > self.threshold then
2,841✔
302
        self:deactivateR()
1,394✔
303
        return
1,394✔
304
      end
305
    end
306
  else
307
    self.prev_r = self.r
25,960✔
308
    if self.badness > self.threshold then return end
25,960✔
309
    nodeStaysActive = true
5,420✔
310
  end
311

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

323
function lineBreak:deactivateR() -- 886
172✔
324
  if debugging then SU.debug("break", " Deactivating r ("..self.r.type..")") end
2,977✔
325
  self.prev_r.next = self.r.next
2,977✔
326
  if self.prev_r == self.activeListHead then
2,977✔
327
    -- 887
328
    self.r = self.activeListHead.next
2,977✔
329
    if self.r.type == "delta" then
2,977✔
330
      self.activeWidth:___add(self.r.width)
1,790✔
331
      self.curActiveWidth = SILE.length(self.activeWidth)
3,580✔
332
      self.activeListHead.next = self.r.next
1,790✔
333
    end
334
    if debugging then SU.debug("break", "  Deactivate, branch 1"); end
2,977✔
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)
172✔
356
  if self.artificialDemerits then return 0 end
7,003✔
357
  local demerit = param("linePenalty") + self.badness
13,734✔
358
  if math.abs(demerit) >= 10000 then
6,867✔
359
    demerit = 100000000
×
360
  else
361
    demerit = demerit * demerit
6,867✔
362
  end
363
  if pi > 0 then
6,867✔
364
    demerit = demerit + pi * pi
709✔
365
  -- elseif pi == 0 then
366
  --   -- do nothing
367
  elseif pi > ejectPenalty then
6,158✔
368
    demerit = demerit - pi * pi
4,711✔
369
  end
370
  if breakType == "hyphenated" and self.r.type == "hyphenated" then
6,867✔
371
    if self.nodes[self.place] then
244✔
372
      demerit = demerit + param("doubleHyphenDemerits")
488✔
373
    else
374
      demerit = demerit + param("finalHyphenDemerits")
×
375
    end
376
  end
377
  -- XXX adjDemerits not added here
378
  return demerit
6,867✔
379
end
380

381
function lineBreak:recordFeasible(pi, breakType) -- 881
172✔
382
  local demerit = lineBreak:computeDemerits(pi, breakType)
7,003✔
383
  if debugging then
7,003✔
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
7,003✔
392
  if demerit <= self.bestInClass[self.fitClass].minimalDemerits then
7,003✔
393
    self.bestInClass[self.fitClass] = {
4,505✔
394
      minimalDemerits = demerit,
4,505✔
395
      node = self.r.serial and self.r,
4,505✔
396
      line = self.r.lineNumber
4,505✔
397
    }
4,505✔
398
    -- XXX do last line fit
399
    if demerit < self.minimumDemerits then self.minimumDemerits = demerit end
4,505✔
400
  end
401
end
402

403

404
function lineBreak:createNewActiveNodes(breakType) -- 862
172✔
405
  if self.no_break_yet then
2,894✔
406
    -- 863
407
    self.no_break_yet = false
2,894✔
408
    self.breakWidth = SILE.length(self.background)
5,788✔
409
    local place = self.place
2,894✔
410
    local node = self.nodes[place]
2,894✔
411
    if node and node.is_discretionary then -- 866
2,894✔
412
      self.breakWidth:___add(node:prebreakWidth())
906✔
413
      self.breakWidth:___add(node:postbreakWidth())
906✔
414
      self.breakWidth:___sub(node:replacementWidth())
906✔
415
    end
416
    while self.nodes[place] and not self.nodes[place].is_box do
7,126✔
417
      if self.sideways and self.nodes[place].height then
4,232✔
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
4,232✔
421
        self.breakWidth:___sub(self.nodes[place]:lineContribution())
8,464✔
422
      end
423
      place = place + 1
4,232✔
424
    end
425
    if debugging then SU.debug("break", "Value of breakWidth = " .. tostring(self.breakWidth)) end
2,894✔
426
  end
427
  -- 869 (Add a new delta node)
428
  if self.prev_r.type == "delta" then
2,894✔
429
    self.prev_r.width:___sub(self.curActiveWidth)
3✔
430
    self.prev_r.width:___add(self.breakWidth)
6✔
431
  elseif self.prev_r == self.activeListHead then
2,891✔
432
    self.activeWidth = SILE.length(self.breakWidth)
1,908✔
433
  else
434
    local newDelta = { next = self.r, type = "delta", width = self.breakWidth - self.curActiveWidth }
3,874✔
435
    if debugging then SU.debug("break", "Added new delta node = " .. tostring(newDelta.width)) end
1,937✔
436
    self.prev_r.next = newDelta
1,937✔
437
    self.prev_prev_r = self.prev_r
1,937✔
438
    self.prev_r = newDelta
1,937✔
439
  end
440
  if math.abs(self.adjdemerits) >= (awful_bad - self.minimumDemerits) then
2,894✔
441
    self.minimumDemerits = awful_bad - 1
×
442
  else
443
    self.minimumDemerits = self.minimumDemerits + math.abs(self.adjdemerits)
2,894✔
444
  end
445

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

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

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

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

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

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

491
function lineBreak:describeBreakNode(node)
172✔
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
172✔
505
  if debugging then SU.debug("break", "considering node "..node); end
31,628✔
506
  local previous = self.nodes[self.place - 1]
31,628✔
507
  if node.is_alternative then self.seenAlternatives = true end
31,628✔
508
  if self.sideways and node.is_box then
31,628✔
509
    self.activeWidth:___add(node.height)
×
510
    self.activeWidth:___add(node.depth)
×
511
  elseif self.sideways and node.is_vglue then
31,628✔
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
31,628✔
518
    self.activeWidth:___add(node:minWidth())
×
519
  elseif node.is_box then
31,628✔
520
    self.activeWidth:___add(node:lineContribution())
47,295✔
521
  elseif node.is_glue then
15,863✔
522
    -- 894 (We removed the auto_breaking parameter)
523
    if previous and previous.is_box then self:tryBreak() end
10,037✔
524
    self.activeWidth:___add(node.width)
20,074✔
525
  elseif node.is_kern then
5,826✔
526
    self.activeWidth:___add(node.width)
350✔
527
  elseif node.is_discretionary then -- 895
5,651✔
528
    self.activeWidth:___add(node:prebreakWidth())
7,696✔
529
    self:tryBreak()
3,848✔
530
    self.activeWidth:___sub(node:prebreakWidth())
7,696✔
531
    self.activeWidth:___add(node:replacementWidth())
11,544✔
532
  elseif node.is_penalty then
1,803✔
533
    self:tryBreak()
1,794✔
534
  end
535
end
536

537
function lineBreak:tryFinalBreak()      -- 899
172✔
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
829✔
549
  self.r = self.activeListHead.next
829✔
550
  local fewestDemerits = awful_bad
829✔
551
  repeat
552
    if self.r.type ~= "delta" and self.r.totalDemerits < fewestDemerits then
1,133✔
553
      fewestDemerits = self.r.totalDemerits
833✔
554
      self.bestBet = self.r
833✔
555
    end
556
    self.r = self.r.next
1,133✔
557
  until self.r == self.activeListHead
1,133✔
558
  if param("looseness") == 0 then return true end
1,658✔
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)
172✔
566
  passSerial = 1
829✔
567
  debugging = SILE.debugFlags["break"]
829✔
568
  self.seenAlternatives = false
829✔
569
  self.nodes = nodes
829✔
570
  self.hsize = hsize
829✔
571
  self.sideways = sideways
829✔
572
  self:init()
829✔
573
  self.adjdemerits = param("adjdemerits")
1,658✔
574
  self.threshold = param("pretolerance")
1,658✔
575
  if self.threshold >= 0 then
829✔
576
    self.pass = "first"
829✔
577
    self.finalpass = false
829✔
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
975✔
585
    if debugging then SU.debug("break", "@" .. self.pass .. "pass") end
975✔
586
    if self.threshold > inf_bad then self.threshold = inf_bad end
975✔
587
    if self.pass == "second" then
975✔
588
      self.nodes = SILE.hyphenate(self.nodes)
212✔
589
      SILE.typesetter.state.nodes = self.nodes -- Horrible breaking of separation of concerns here. :-(
106✔
590
    end
591
    -- 890
592
    self.activeListHead = {
975✔
593
      sentinel="START",
594
      type = "hyphenated",
595
      lineNumber = awful_bad,
975✔
596
      subtype = 0
×
597
    } -- 846
975✔
598
    self.activeListHead.next = {
975✔
599
      sentinel="END",
600
      type = "unhyphenated",
601
      fitness = "decent",
602
      next = self.activeListHead,
975✔
603
      lineNumber = param("prevGraf") + 1,
1,950✔
604
      totalDemerits = 0
×
605
    }
975✔
606

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

610
    self.place = 1
975✔
611
    while self.nodes[self.place] and self.activeListHead.next ~= self.activeListHead do
32,603✔
612
      self:checkForLegalBreak(self.nodes[self.place])
31,628✔
613
      self.place = self.place + 1
31,628✔
614
    end
615
    if self.place > #self.nodes then
975✔
616
      if self:tryFinalBreak() then break end
1,658✔
617
    end
618
    -- (Not doing 891)
619
    if self.pass ~= "second" then
146✔
620
      self.pass = "second"
106✔
621
      self.threshold = param("tolerance")
212✔
622
    else
623
      self.pass = "emergency"
40✔
624
      self.background.stretch:___add(param("emergencyStretch"))
80✔
625
      self.finalpass = true
40✔
626
    end
627
  end
628
  -- Not doing 1638
629
  return self:postLineBreak()
829✔
630
end
631

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

637
  local nbLines = 0
829✔
638
  local p2 = p
829✔
639
  repeat
640
    nbLines = nbLines + 1
1,481✔
641
    p2 = p2.prevBreak
1,481✔
642
  until not p2
1,481✔
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
1,481✔
651
      left, _, right = self:parShapeCache(nbLines + 1 - line)
30✔
652
    else
653
      if self.hangAfter == 0 then
1,466✔
654
        -- width = self.hsize
655
        left = 0
1,429✔
656
        right = 0
1,429✔
657
      else
658
        local indent
659
        if self.hangAfter > 0 then
37✔
660
          -- width = line > nbLines - self.hangAfter and self.firstWidth or self.secondWidth
661
          indent = line > nbLines - self.hangAfter and 0 or self.hangIndent
16✔
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
21✔
665
        end
666
        if indent > 0 then
37✔
667
          left = indent
10✔
668
          right = 0
10✔
669
        else
670
          left = 0
27✔
671
          right = -indent
27✔
672
        end
673
      end
674
    end
675

676
    table.insert(breaks, 1,  {
2,962✔
677
        position = p.curBreak,
1,481✔
678
        width = self.hsize,
1,481✔
679
        left = left,
1,481✔
680
        right = right
1,481✔
681
      })
682
    if p.alternates then
1,481✔
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
1,481✔
689
    line = line + 1
1,481✔
690
  until not p
1,481✔
691
  self:parShapeCacheClear()
829✔
692
  return breaks
829✔
693
end
694

695
function lineBreak:dumpActiveRing()
172✔
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
172✔
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