• 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

0.0
/packages/lists/init.lua
UNCOV
1
local base = require("packages.base")
×
2

UNCOV
3
local package = pl.class(base)
×
UNCOV
4
package._name = "lists"
×
5

6
--
7
-- Enumerations and bullet lists for SILE
8
-- Donated to the SILE typesetter - 2021-2022, Didier Willis
9
-- This a trimmed-down version of the feature-richer but more experimental
10
-- "enumitem" package (https://github.com/Omikhleia/omikhleia-sile-packages).
11
-- License: MIT
12
--
13
-- NOTE: Though not described explicitly in the documentation, the package supports
14
-- two nesting techniques:
15
-- The "simple" or compact one:
16
--    \begin{itemize}
17
--       \item{L1.1}
18
--       \begin{itemize}
19
--          \item{L2.1}
20
--       \end{itemize}
21
--    \end{itemize}
22
-- The "alternative" one, which consists in having the nested elements in an item:
23
--    \begin{itemize}
24
--       \item{L1.1
25
--         \begin{itemize}
26
--            \item{L2.1}
27
--         \end{itemize}}
28
--    \end{itemize}
29
-- The latter might be less readable, but is of course more powerful, as other
30
-- contents can be added to the item, as in:
31
--    \begin{itemize}
32
--       \item{L1.1
33
--         \begin{itemize}
34
--            \item{L2.1}
35
--         \end{itemize}%
36
--         This is still in L1.1}
37
--    \end{itemize}
38
-- But personally, for simple lists, I prefer the first "more readable" one.
39
-- Lists from Mardown, obviously, due to their structure, would need the
40
-- second technique.
41
--
42

UNCOV
43
local styles = {
×
UNCOV
44
   enumerate = {
×
UNCOV
45
      { display = "arabic", after = "." },
×
UNCOV
46
      { display = "roman", after = "." },
×
UNCOV
47
      { display = "alpha", after = "." },
×
UNCOV
48
      { display = "arabic", after = ")" },
×
UNCOV
49
      { display = "roman", after = ")" },
×
UNCOV
50
      { display = "alpha", after = ")" },
×
51
   },
UNCOV
52
   itemize = {
×
UNCOV
53
      { bullet = "•" }, -- black bullet
×
UNCOV
54
      { bullet = "â—¦" }, -- circle bullet
×
UNCOV
55
      { bullet = "–" }, -- en-dash
×
UNCOV
56
      { bullet = "•" }, -- black bullet
×
UNCOV
57
      { bullet = "â—¦" }, -- circle bullet
×
UNCOV
58
      { bullet = "–" }, -- en-dash
×
59
   },
60
}
61

62
local enforceListType = function (cmd)
UNCOV
63
   if cmd ~= "enumerate" and cmd ~= "itemize" and cmd ~= "BulletedList" and cmd ~= "OrderedList" then
×
64
      SU.error("Only items or lists are allowed as content in lists, found '" .. cmd .. "'")
×
65
   end
66
end
67

68
local lastListSpacing
69

70
local function getNestedDepth ()
UNCOV
71
   local itemize_level = SILE.settings:get("lists.current.itemize.depth")
×
UNCOV
72
   local enumerate_level = SILE.settings:get("lists.current.enumerate.depth")
×
UNCOV
73
   return itemize_level + enumerate_level
×
74
end
75

76
-- Push some list separation space provided we don't already have some
77
local function pushListSpacing ()
UNCOV
78
   if #SILE.typesetter.state.outputQueue ~= lastListSpacing then
×
UNCOV
79
      SILE.typesetter:pushVglue(SILE.settings:get("lists.parskip"))
×
UNCOV
80
      lastListSpacing = #SILE.typesetter.state.outputQueue
×
81
   end
82
end
83

84
-- Remove the last list separation space so it doen't interfere with parskip
85
local function popListSpacing ()
UNCOV
86
   if lastListSpacing then
×
UNCOV
87
      local node = SILE.typesetter.state.outputQueue[lastListSpacing]
×
UNCOV
88
      if not node.is_vglue then
×
89
         SU.error("Attempted to pop something other than vertical spacing", true)
×
90
      end
UNCOV
91
      table.remove(SILE.typesetter.state.outputQueue, lastListSpacing)
×
92
   end
93
end
94

95
local function maybeAddListSpacing (islist, entering, counter)
96
   -- Observed nesting depth for lists is zero based and hence N-1 vs. items in those lists
UNCOV
97
   local depth = getNestedDepth() + (islist and 1 or 0)
×
UNCOV
98
   local isitem = not islist
×
UNCOV
99
   local leaving = not entering
×
UNCOV
100
   SILE.typesetter:leaveHmode()
×
101
   -- All list items except the first one in the outermost list get leading space
UNCOV
102
   if entering and isitem and (counter ~= 1 or depth >= 2) then
×
UNCOV
103
      pushListSpacing()
×
104
   end
105
   -- All lists get trailing space (to cover nesting)
UNCOV
106
   if leaving and islist then
×
UNCOV
107
      pushListSpacing()
×
UNCOV
108
      if depth == 1 then
×
UNCOV
109
         popListSpacing()
×
110
      end
111
   end
112
end
113

UNCOV
114
function package:doItem (options, content)
×
UNCOV
115
   local enumStyle = content._lists_.style
×
UNCOV
116
   local counter = content._lists_.counter
×
UNCOV
117
   local indent = content._lists_.indent
×
118

UNCOV
119
   maybeAddListSpacing(false, true, counter)
×
120

UNCOV
121
   local mark = SILE.typesetter:makeHbox(function ()
×
UNCOV
122
      if enumStyle.display then
×
UNCOV
123
         if enumStyle.before then
×
124
            SILE.typesetter:typeset(enumStyle.before)
×
125
         end
UNCOV
126
         SILE.typesetter:typeset(self.class.packages.counters:formatCounter({
×
127
            value = counter,
128
            display = enumStyle.display,
129
         }))
UNCOV
130
         if enumStyle.after then
×
UNCOV
131
            SILE.typesetter:typeset(enumStyle.after)
×
132
         end
133
      else
UNCOV
134
         local bullet = options.bullet or enumStyle.bullet
×
UNCOV
135
         SILE.typesetter:typeset(bullet)
×
136
      end
137
   end)
138

139
   local stepback
UNCOV
140
   if enumStyle.display then
×
141
      -- The positioning is quite tentative... LaTeX would right justify the
142
      -- number (at least for roman numerals), i.e.
143
      --   i. Text
144
      --  ii. Text
145
      -- iii. Text.
146
      -- Other Office software do not do that...
UNCOV
147
      local labelIndent = SILE.settings:get("lists.enumerate.labelindent"):absolute()
×
UNCOV
148
      stepback = indent - labelIndent
×
149
   else
150
      -- Center bullets in the indentation space
UNCOV
151
      stepback = indent / 2 + mark.width / 2
×
152
   end
153

UNCOV
154
   SILE.call("kern", { width = -stepback })
×
155
   -- reinsert the mark with modified length
156
   -- using \rebox caused an issue sometimes, not sure why, with the bullets
157
   -- appearing twice in output... but we can avoid it:
158
   -- reboxing an hbox was dumb anyway. We just need to fix its width before
159
   -- reinserting it in the text flow.
UNCOV
160
   mark.width = SILE.types.length(stepback)
×
UNCOV
161
   SILE.typesetter:pushHbox(mark)
×
UNCOV
162
   SILE.process(content)
×
163

164
   -- In the event the list ends but paragraph continues we don't want a paragraph
165
   -- indent applied just because we yielded to the typesetter in vmode.
UNCOV
166
   SILE.settings:set("current.parindent", SILE.types.node.glue())
×
UNCOV
167
   maybeAddListSpacing(false, false, counter)
×
168
end
169

UNCOV
170
function package.doNestedList (_, listType, options, content)
×
171
   -- depth
UNCOV
172
   local depth = SILE.settings:get("lists.current." .. listType .. ".depth") + 1
×
173

174
   -- styling
UNCOV
175
   local enumStyle = styles[listType][(depth - 1) % 6 + 1]
×
176
   -- options may override the default styling
UNCOV
177
   enumStyle = pl.tablex.copy(enumStyle) -- shallow copy for possible overrides
×
UNCOV
178
   if enumStyle.display then
×
UNCOV
179
      if options.before or options.after then
×
180
         -- for before/after, don't mix default style and options
181
         enumStyle.before = options.before or ""
×
182
         enumStyle.after = options.after or ""
×
183
      end
UNCOV
184
      if options.display then
×
185
         enumStyle.display = options.display
×
186
      end
187
   else
UNCOV
188
      enumStyle.bullet = options.bullet or enumStyle.bullet
×
189
   end
190

191
   -- indent
UNCOV
192
   local baseIndent = (depth == 1) and SILE.settings:get("document.parindent").width:absolute()
×
UNCOV
193
      or SILE.types.measurement("0pt")
×
UNCOV
194
   local listIndent = SILE.settings:get("lists." .. listType .. ".leftmargin"):absolute()
×
195

UNCOV
196
   maybeAddListSpacing(true, true, nil)
×
UNCOV
197
   SILE.settings:temporarily(function ()
×
UNCOV
198
      SILE.settings:set("lists.current." .. listType .. ".depth", depth)
×
UNCOV
199
      SILE.settings:set("current.parindent", SILE.types.node.glue())
×
UNCOV
200
      SILE.settings:set("document.parindent", SILE.types.node.glue())
×
UNCOV
201
      local lskip = SILE.settings:get("document.lskip") or SILE.types.node.glue()
×
UNCOV
202
      SILE.settings:set("document.lskip", SILE.types.node.glue(lskip.width + (baseIndent + listIndent)))
×
203

UNCOV
204
      local counter = options.start and (SU.cast("integer", options.start) - 1) or 0
×
UNCOV
205
      for i = 1, #content do
×
UNCOV
206
         if type(content[i]) == "table" and #content[i] > 0 then
×
UNCOV
207
            if content[i].command == "item" or content[i].command == "ListItem" then
×
UNCOV
208
               counter = counter + 1
×
209
               -- Enrich the node with internal properties
UNCOV
210
               content[i]._lists_ = {
×
211
                  style = enumStyle,
212
                  counter = counter,
213
                  indent = listIndent,
214
               }
215
            else
UNCOV
216
               enforceListType(content[i].command)
×
217
            end
UNCOV
218
            SILE.process({ content[i] })
×
219
         -- Whitespace left around comment nodes is fine too
UNCOV
220
         elseif type(content[i]) == "table" and #content[i] == 0 then
×
221
            -- ignore whitespace leaking in from in front of indented comments
222
            assert(true)
×
UNCOV
223
         elseif type(content[i]) == "string" then
×
224
            -- All text nodes are ignored in structure tags, but just warn
225
            -- if there do not just consist in spaces.
UNCOV
226
            local text = pl.stringx.strip(content[i])
×
UNCOV
227
            if text ~= "" then
×
228
               SU.warn("Ignored standalone text (" .. text .. ")")
×
229
            end
230
         else
231
            SU.error("List structure error")
×
232
         end
233
      end
234
   end)
UNCOV
235
   maybeAddListSpacing(true, false, nil)
×
236
end
237

UNCOV
238
function package:_init ()
×
UNCOV
239
   base._init(self)
×
UNCOV
240
   self:loadPackage("counters")
×
241
end
242

UNCOV
243
function package.declareSettings (_)
×
UNCOV
244
   SILE.settings:declare({
×
245
      parameter = "lists.current.enumerate.depth",
246
      type = "integer",
247
      default = 0,
248
      help = "Current enumerate depth (nesting) - internal",
249
   })
250

UNCOV
251
   SILE.settings:declare({
×
252
      parameter = "lists.current.itemize.depth",
253
      type = "integer",
254
      default = 0,
255
      help = "Current itemize depth (nesting) - internal",
256
   })
257

UNCOV
258
   SILE.settings:declare({
×
259
      parameter = "lists.enumerate.leftmargin",
260
      type = "measurement",
261
      default = SILE.types.measurement("2em"),
262
      help = "Left margin (indentation) for enumerations",
263
   })
264

UNCOV
265
   SILE.settings:declare({
×
266
      parameter = "lists.enumerate.labelindent",
267
      type = "measurement",
268
      default = SILE.types.measurement("0.5em"),
269
      help = "Label indentation for enumerations",
270
   })
271

UNCOV
272
   SILE.settings:declare({
×
273
      parameter = "lists.itemize.leftmargin",
274
      type = "measurement",
275
      default = SILE.types.measurement("1.5em"),
276
      help = "Left margin (indentation) for bullet lists (itemize)",
277
   })
278

UNCOV
279
   SILE.settings:declare({
×
280
      parameter = "lists.parskip",
281
      type = "vglue",
282
      default = SILE.types.node.vglue("0pt plus 1pt"),
283
      help = "Leading between items in a list",
284
   })
285
end
286

UNCOV
287
function package:registerCommands ()
×
UNCOV
288
   self:registerCommand("enumerate", function (options, content)
×
UNCOV
289
      self:doNestedList("enumerate", options, content)
×
290
   end)
291

UNCOV
292
   self:registerCommand("itemize", function (options, content)
×
UNCOV
293
      self:doNestedList("itemize", options, content)
×
294
   end)
295

UNCOV
296
   self:registerCommand("item", function (options, content)
×
UNCOV
297
      if not content._lists_ then
×
298
         SU.error("The item command shall not be called outside a list")
×
299
      end
UNCOV
300
      self:doItem(options, content)
×
301
   end)
302
end
303

304
package.documentation = [[
305
\begin{document}
306
\font:add-fallback[family=Symbola]% HACK Gentium Plus (SILE default font) lacks the circle bullet :(
307
The \autodoc:package{lists} package provides enumerated and itemized (also known as \em{bulleted lists}) which can be nested together.
308

309
\smallskip
310
\noindent
311
\em{Itemized lists}
312
\novbreak
313

314
\indent
315
The \autodoc:environment{itemize} environment initiates a itemized list.
316
Each item, unsurprisingly, is wrapped in an \autodoc:command{\item} command.
317

318
The environment, as a structure or data model, can only contain \code{item} elements or other lists.
319
Any other element causes an error to be reported, and any text content is ignored with a warning.
320

321
\begin{itemize}
322
    \item{Lorem}
323
    \begin{itemize}
324
        \item{Ipsum}
325
        \begin{itemize}
326
            \item{Dolor}
327
        \end{itemize}
328
    \end{itemize}
329
\end{itemize}
330

331
On each level, the indentation is defined by the \autodoc:setting{lists.itemize.leftmargin} setting (defaults to \code{1.5em}) and the bullet is centered in that margin.
332
Note that if your document has a paragraph indent enabled at this point, it is also added to the first list level.
333

334
The package has a default bullet style for each level, but you can explicitly select a bullet symbol of your choice to be used by specifying the options \autodoc:parameter{bullet=<character>} on the \autodoc:environment{itemize} environment.
335
You can also force a specific bullet character to be used on a specific item with \autodoc:command{\item[bullet=<character>]}.
336

337
\smallskip
338
\noindent
339
\em{Enumerated lists}
340
\novbreak
341

342
\indent
343
The \autodoc:environment{enumerate} environment initiates an enumeration.
344
Each item shall, again, be wrapped in an \autodoc:command{\item} command.
345
This environment too is regarded as a structure, so the same rules as above apply.
346

347
The enumeration starts at one, unless you specify the \autodoc:parameter{start=<integer>} option (a numeric value, regardless of the display format).
348

349
\begin{enumerate}
350
    \item{Lorem}
351
    \begin{enumerate}
352
        \item{Ipsum}
353
        \begin{enumerate}
354
            \item{Dolor}
355
        \end{enumerate}
356
    \end{enumerate}
357
\end{enumerate}
358

359
On each level, the indentation is defined by the \autodoc:setting{lists.enumerate.leftmargin} setting (defaults to \code{2em}).
360
Note, again, that if your document has a paragraph indent enabled at this point, it is also added to the first list level.
361

362
% And… ah, at least something less repetitive than a raw list of features.
363
% \em{Quite obviously}, we cannot center the label.
364
% Roman numbers, folks, if any reason is required.
365

366
The \autodoc:setting{lists.enumerate.labelindent} setting specifies the distance between the label and the previous indentation level (defaults to \code{0.5em}).
367
Tune these settings at your convenience depending on your styles.
368
If there is a more general solution to this subtle issue, we accept patches.%
369
\footnote{TeX typesets the enumeration label ragged left. Most word processing software do not.}
370

371
The package has a default number style for each level, but you can explicitly select the display type (format) of the values (as \code{arabic}, \code{roman}, or \code{alpha}), and the text prepended or appended to them, by specifying the options \autodoc:parameter{display=<display>}, \autodoc:parameter{before=<string>}, and \autodoc:parameter{after=<string>} to the \autodoc:environment{enumerate} environment.
372

373
\smallskip
374
\noindent
375
\em{Nesting}
376
\novbreak
377

378
\indent
379
Both environments can be nested.
380
The way they do is best illustrated by an example.
381

382
\begin{enumerate}
383
    \item{Lorem}
384
    \begin{enumerate}
385
        \item{Ipsum}
386
        \begin{itemize}
387
            \item{Dolor}
388
            \begin{enumerate}
389
                \item{Sit amet}
390
                \begin{itemize}
391
                    \item{Consectetur}
392
                \end{itemize}
393
            \end{enumerate}
394
        \end{itemize}
395
    \end{enumerate}
396
\end{enumerate}
397

398
\smallskip
399
\noindent
400
\em{Vertical spaces}
401
\novbreak
402

403
\indent
404
The package outputs lists starting after a line break, but it does not enforce a paragraph break before or after the list.
405
If you want the usual value of \autodoc:setting{document.parskip} to apply before and/or after your list leave a blank line in your source document separating paragraphs as usual.
406
Between list items, however, the paragraph skip is switched to the value of the \autodoc:setting{lists.parskip} setting.
407

408
\smallskip
409
\noindent
410
\em{Other considerations}
411
\novbreak
412

413
\indent
414
Do not expect these fragile lists to work in any way in centered or ragged-right environments, or with fancy line-breaking features such as hanged or shaped paragraphs.
415
Please be a good typographer. Also, these lists have not yet been tried in right-to-left or vertical writing direction.
416

417
\font:remove-fallback
418
\end{document}
UNCOV
419
]]
×
420

UNCOV
421
return package
×
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