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

sile-typesetter / sile / 9428435077

08 Jun 2024 11:35AM UTC coverage: 64.56% (-9.9%) from 74.46%
9428435077

push

github

web-flow
Merge pull request #2047 from alerque/end-pars

23 of 46 new or added lines in 5 files covered. (50.0%)

1684 existing lines in 60 files now uncovered.

11145 of 17263 relevant lines covered (64.56%)

4562.45 hits per line

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

48.05
/packages/frametricks/init.lua
1
local base = require("packages.base")
5✔
2

3
local package = pl.class(base)
5✔
4
package._name = "frametricks"
5✔
5

6
local breakFrameHorizontalAt = function (offset)
UNCOV
7
   local cFrame = SILE.typesetter.frame
×
UNCOV
8
   if not offset or not (offset > SILE.types.length(0)) then
×
9
      SILE.typesetter:chuck()
×
10
      offset = SILE.typesetter.frame.state.cursorX
×
11
   end
UNCOV
12
   local newFrame = SILE.newFrame({
×
13
      bottom = cFrame:bottom(),
14
      top = cFrame:top(),
15
      left = cFrame:left() + offset,
16
      right = cFrame:right(),
17
      next = cFrame.next,
18
      previous = cFrame,
19
      id = cFrame.id .. "_",
20
   })
21
   -- if SILE.scratch.insertions and SILE.scratch.insertions.classes['footnote'] and SILE.scratch.insertions.classes['footnote'].stealFrom then
22
   -- SILE.scratch.insertions.classes['footnote'].stealFrom[newFrame.id] = 1
23
   -- end
UNCOV
24
   local oldLeft = cFrame:left()
×
UNCOV
25
   cFrame.next = newFrame.id
×
UNCOV
26
   cFrame:constrain("left", oldLeft)
×
UNCOV
27
   cFrame:constrain("right", oldLeft + offset)
×
28
   -- SILE.outputter:debugFrame(cFrame)
29
   -- SILE.outputter:debugFrame(newFrame)
UNCOV
30
   SILE.typesetter:initFrame(newFrame)
×
31
end
32

33
local shiftframeedge = function (frame, options)
UNCOV
34
   if options.left then
×
UNCOV
35
      frame:constrain("left", frame:left() + SILE.types.length(options.left))
×
36
   end
UNCOV
37
   if options.right then
×
38
      frame:constrain("right", frame:right() + SILE.types.length(options.right))
×
39
   end
40
end
41

42
local makecolumns = function (options)
43
   local cFrame = SILE.typesetter.frame
1✔
44
   local cols = options.columns
1✔
45
   local balanced = SU.boolean(options.balanced, true)
1✔
46
   local gutterWidth = options.gutter or "3%pw"
1✔
47
   local right = cFrame:right()
1✔
48
   local origId = cFrame.id
1✔
49
   for i = 1, cols - 1 do
4✔
50
      local gutter = SILE.newFrame({
6✔
51
         width = gutterWidth,
3✔
52
         left = "right(" .. cFrame.id .. ")",
3✔
53
         id = origId .. "_gutter" .. i,
3✔
54
      })
55
      cFrame:relax("right")
3✔
56
      cFrame:constrain("right", "left(" .. gutter.id .. ")")
3✔
57
      local newFrame = SILE.newFrame({
6✔
58
         top = cFrame:top(),
6✔
59
         bottom = cFrame:bottom(),
6✔
60
         id = origId .. "_col" .. i,
3✔
61
      })
62
      newFrame.balanced = balanced
3✔
63
      cFrame.balanced = balanced
3✔
64
      gutter:constrain("right", "left(" .. newFrame.id .. ")")
3✔
65
      newFrame:constrain("left", "right(" .. gutter.id .. ")")
3✔
66
      -- In the future we may way to allow for unequal columns
67
      -- but for now just assume they will be equal.
68
      newFrame:constrain("width", "width(" .. cFrame.id .. ")")
3✔
69
      cFrame.next = newFrame.id
3✔
70
      cFrame = newFrame
3✔
71
   end
72
   cFrame:constrain("right", right)
1✔
73
end
74

75
local mergeColumns = function ()
76
   -- 1) Balance all remaining material.
77

78
   -- 1.1) Run the pagebuilder once to clear out any full pages
79
   SILE.typesetter:buildPage()
×
80

81
   -- 1.2) Find out the shape of the columnset. (It will change after we balance it)
82
   local frame = SILE.typesetter.frame
×
83
   -- local left = frame:left()
84
   -- local bottom = frame:bottom()
85
   while frame.next and SILE.getFrame(frame.next).balanced do
×
86
      frame = SILE.getFrame(frame.next)
×
87
   end
88
   -- local right = frame:right()
89

90
   -- 1.3) Now force a balance, which will resize the frames
91
   SILE.call("balancecolumns")
×
92
   SILE.typesetter:buildPage()
×
93

94
   -- 2) Add a new frame, the width of the old frameset and the height of
95
   -- old frameset - new height, at the end of the current frame
96
   local newId = SILE.typesetter.frame.id .. "_"
×
97
   SILE.typesetter.frame.next = newId
×
98
   SILE.typesetter:initNextFrame()
×
99
end
100

101
function package.breakFrameVertical (_, after)
5✔
UNCOV
102
   local cFrame = SILE.typesetter.frame
×
103
   local totalHeight
UNCOV
104
   if after then
×
UNCOV
105
      totalHeight = after
×
106
   else
UNCOV
107
      totalHeight = SILE.types.length(0)
×
UNCOV
108
      SILE.typesetter:leaveHmode(1)
×
UNCOV
109
      local queue = SILE.typesetter.state.outputQueue
×
UNCOV
110
      for i = 1, #queue do
×
UNCOV
111
         totalHeight = totalHeight + queue[i].height + queue[i].depth
×
112
      end
UNCOV
113
      SILE.typesetter:chuck()
×
114
   end
115

UNCOV
116
   local newFrame = SILE.newFrame({
×
117
      bottom = cFrame:bottom(),
118
      left = cFrame:left(),
119
      right = cFrame:right(),
120
      next = cFrame.next,
121
      previous = cFrame,
122
      id = cFrame.id .. "_",
123
   })
124
   if
UNCOV
125
      SILE.scratch.insertions
×
UNCOV
126
      and SILE.scratch.insertions.classes["footnote"]
×
UNCOV
127
      and SILE.scratch.insertions.classes["footnote"].stealFrom
×
128
   then
UNCOV
129
      SILE.scratch.insertions.classes["footnote"].stealFrom[newFrame.id] = 1
×
130
   end
131

UNCOV
132
   cFrame:relax("bottom")
×
UNCOV
133
   cFrame:constrain("height", totalHeight)
×
UNCOV
134
   cFrame.next = newFrame.id
×
UNCOV
135
   SILE.documentState.thisPageTemplate.frames[newFrame.id] = newFrame
×
UNCOV
136
   newFrame:constrain("top", cFrame:top() + totalHeight)
×
UNCOV
137
   if after then
×
UNCOV
138
      SILE.typesetter:initFrame(cFrame)
×
139
   else
UNCOV
140
      SILE.typesetter:initFrame(newFrame)
×
141
   end
142
   -- SILE.outputter:debugFrame(cFrame)
143
   -- SILE.outputter:debugFrame(newFrame)
144
end
145

146
function package:_init ()
5✔
147
   base._init(self)
5✔
148
   self:loadPackage("balanced-frames")
5✔
149
   self:export("breakFrameVertical", self.breakFrameVertical)
5✔
150
end
151

152
function package:registerCommands ()
5✔
153
   self:registerCommand("mergecolumns", function (_, _)
10✔
154
      mergeColumns()
×
155
   end, "Merge multiple columns into one")
5✔
156

157
   self:registerCommand("showframe", function (options, _)
10✔
158
      local id = options.id or SILE.typesetter.frame.id
6✔
159
      if id == "all" then
6✔
160
         for _, frame in pairs(SILE.frames) do
28✔
161
            SILE.outputter:debugFrame(frame)
24✔
162
         end
163
      else
164
         SILE.outputter:debugFrame(SILE.getFrame(id))
4✔
165
      end
166
   end)
167

168
   self:registerCommand("shiftframeedge", function (options, _)
10✔
169
      local cFrame = SILE.typesetter.frame
×
170
      shiftframeedge(cFrame, options)
×
171
      SILE.typesetter:initFrame(cFrame)
×
172
      --SILE.outputter:debugFrame(cFrame)
173
   end, "Adjusts the edge of the frame horizontally by amounts specified in <left> and <right>")
5✔
174

175
   self:registerCommand(
10✔
176
      "breakframevertical",
5✔
177
      function (options, _)
UNCOV
178
         self:breakFrameVertical(options.offset)
×
179
      end,
180
      "Breaks the current frame in two vertically at the current location or at a point <offset> below the current location"
181
   )
5✔
182

183
   self:registerCommand("makecolumns", function (options, _)
10✔
184
      -- Set a default value for column count
185
      options.columns = options.columns or 2
1✔
186
      local current_frame = SILE.typesetter.frame
1✔
187
      local original_constraints = {}
1✔
188
      -- Collect existing constraints that may need updating after makecolumns() changes them
189
      for frameid in pairs(SILE.frames) do
5✔
190
         if frameid ~= current_frame.id then
4✔
191
            local frame = SILE.getFrame(frameid)
3✔
192
            for method in pairs(frame.constraints) do
15✔
193
               -- TODO: Remove the assumption about direction when makecolumns() takes into account frame advance direction
194
               if method == "right" then
12✔
195
                  if frame[method](frame) == current_frame[method](current_frame) then
9✔
196
                     table.insert(original_constraints, { frame = frame, method = method })
2✔
197
                  end
198
               end
199
            end
200
         end
201
      end
202
      makecolumns(options)
1✔
203
      for _, info in ipairs(original_constraints) do
3✔
204
         local frame, method = info.frame, info.method
2✔
205
         local final_column_id = ("%s_col%d"):format(current_frame.id, options.columns - 1)
2✔
206
         local final_comumn_frame = SILE.getFrame(final_column_id)
2✔
207
         frame:constrain(method, final_comumn_frame[method](final_comumn_frame))
4✔
208
      end
209
   end, "Split the current frame into multiple columns")
6✔
210

211
   self:registerCommand(
10✔
212
      "breakframehorizontal",
5✔
213
      function (options, _)
214
         breakFrameHorizontalAt(options.offset)
×
215
      end,
216
      "Breaks the current frame in two horizontally either at the current location or at a point <offset> from the left of the current frame"
217
   )
5✔
218

219
   self:registerCommand("float", function (options, content)
10✔
UNCOV
220
      SILE.typesetter:leaveHmode()
×
UNCOV
221
      local hbox = SILE.typesetter:makeHbox(content) -- HACK What about migrating nodes here?
×
UNCOV
222
      local heightOfPageSoFar = SILE.pagebuilder:collateVboxes(SILE.typesetter.state.outputQueue).height
×
UNCOV
223
      local overshoot = SILE.types.length(heightOfPageSoFar + hbox.height - SILE.typesetter:getTargetLength())
×
UNCOV
224
      if overshoot > SILE.types.length(0) then
×
225
         SILE.call("eject")
×
226
         SILE.typesetter:leaveHmode()
×
227
      end
UNCOV
228
      self:breakFrameVertical()
×
UNCOV
229
      local boundary = hbox.width + SILE.types.length(options.rightboundary):absolute()
×
UNCOV
230
      breakFrameHorizontalAt(boundary)
×
UNCOV
231
      SILE.typesetNaturally(SILE.typesetter.frame.previous, function ()
×
UNCOV
232
         table.insert(SILE.typesetter.state.nodes, hbox)
×
233
      end)
234
      -- SILE.settings:set("document.baselineskip", SILE.types.length("1ex") - SILE.settings:get("document.baselineskip").height)
235
      -- undoSkip.stretch = hbox.height
236
      -- SILE.typesetter:pushHbox({ value = {} })
237
      -- SILE.typesetter:pushVglue({ height = undoSkip })
UNCOV
238
      self:breakFrameVertical(hbox.height + SILE.types.length(options.bottomboundary):absolute())
×
UNCOV
239
      shiftframeedge(SILE.getFrame(SILE.typesetter.frame.next), { left = -boundary })
×
240
      --SILE.outputter:debugFrame(SILE.typesetter.frame)
241
   end, "Sets the given content in its own frame, flowing the remaining content around it")
5✔
242

243
   self:registerCommand("typeset-into", function (options, content)
10✔
244
      SU.required(options, "frame", "calling \\typeset-into")
×
245
      if not SILE.frames[options.frame] then
×
246
         SU.error("Can't find frame " .. options.frame .. " to typeset into")
×
247
      end
248
      SILE.typesetNaturally(SILE.frames[options.frame], function ()
×
249
         SILE.process(content)
×
250
      end)
251
   end)
252

253
   self:registerCommand("fit-frame", function (options, _)
10✔
254
      SU.required(options, "frame", "calling \\fit-frame")
×
255
      if not SILE.frames[options.frame] then
×
256
         SU.error("Can't find frame " .. options.frame .. " to fit")
×
257
      end
258
      local frame = SILE.frames[options.frame]
×
259
      local height = SILE.types.length()
×
260
      SILE.typesetNaturally(frame, function ()
×
261
         SILE.typesetter:leaveHmode()
×
262
         for i = 1, #SILE.typesetter.state.outputQueue do
×
263
            height = height + SILE.typesetter.state.outputQueue[i].height
×
264
         end
265
      end)
266
      frame:constrain("height", frame:height() + height)
×
267
   end)
268
end
269

270
package.documentation = [[
271
\begin{document}
272
The \autodoc:package{frametricks} package assists package authors by providing a number of commands to manipulate frames.
273

274
The most immediately useful is \autodoc:command{\showframe}.
275
This asks the output engine to draw a box and label around a particular frame.
276
It takes an optional parameter \autodoc:parameter{id=<frame id>}; if this is not supplied, the current frame is used.
277
If the ID is \code{all}, then all frames declared by the current class are displayed.
278

279
It’s possible to define frames such as sidebars which are not connected to the main text flow of a page.
280
We’ll see how to do that in a later chapter, but this raises the obvious question: if they’re not part of the text flow, how do we get stuff into them?
281
\autodoc:package{frametricks} provides the \autodoc:command{\typeset-into} command, which allows you to write text into a specified frame:
282

283
\begin[type=autodoc:codeblock]{raw}
284
\typeset-into[frame=sidebar]{ ... frame content here ... }
285
\end{raw}
286

287
\autodoc:package{frametricks} also provides a number of commands which, to be perfectly honest, we \em{thought} were going to be useful, but haven’t quite ended up being as useful as all that.
288

289
\breakframevertical\par
290
The command \autodoc:command{\breakframevertical} breaks the current frame in two at the specified location into an upper and lower frame.
291
If the frame initially had the ID \code{main}, then \code{main} becomes the upper frame (before the command) and the lower frame (after the command) is called \code{main_}.
292
We just issued a \autodoc:command{\breakframevertical} command at the start of this paragraph, and now we will issue the command \autodoc:command{\showframe}.
293
\showframe
294
As you can see, the current frame is called \code{\lua{SILE.typesetter:typeset(SILE.typesetter.frame.id)}} and now begins at the start of the paragraph.
295

296
Similarly, the \autodoc:command{\breakframehorizontal} command breaks the frame in two into a left and a right frame.
297
The command takes an optional parameter \autodoc:parameter{offset=<dimension>}, specifying where on the line the frame should be split.
298
If \autodoc:parameter{offset} is not supplied, the frame is split at the current position in the line.
299

300
The command \autodoc:command{\shiftframeedge} allows you to reposition the current frame left or right.
301
It takes \autodoc:parameter{left} and/or \autodoc:parameter{right} parameters, which can be positive or negative dimensions.
302
It should only be used at the top of a frame, as it reinitializes the typesetter object.
303

304
Combining all of these commands, the \autodoc:command{\float} command breaks the current frame, creates a small frame to hold a floating object, flows text into the surrounding frame, and then, once text has descended past the floating object, moves the frame back into place again.
305
It takes two optional parameters, \autodoc:parameter{bottomboundary=<dimension>} and/or \autodoc:parameter{rightboundary=<dimension>}, which open up additional space around the frame.
306

307
% At the start of this paragraph, I issued the command
308
%
309
% \begin[type=autodoc:codeblock]{raw}
310
% \float[bottomboundary=5pt]{\font[size=60pt]{C}}
311
% \end{raw}
312

313
To reiterate: we started playing around with frames like this in the early days of SILE and they have not really proved a good solution to the things we wanted to do with them.
314
They’re great for arranging where content should live on the page, but messing about with them dynamically seems to create more problems than it solves.
315
There’s probably a reason why InDesign and similar applications handle floats, drop caps, tables, and so on inside the context of a content frame rather than by messing with the frames themselves.
316
If you feel tempted to play with \autodoc:package{frametricks}, there’s almost always a better way to achieve what you want without it.
317
\end{document}
318
]]
5✔
319

320
return package
5✔
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