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

sile-typesetter / sile / 9324025270

31 May 2024 08:35PM UTC coverage: 66.168% (-8.0%) from 74.124%
9324025270

push

github

web-flow
Merge 235329972 into 70ff5c335

1753 of 2583 new or added lines in 108 files covered. (67.87%)

1498 existing lines in 74 files now uncovered.

11404 of 17235 relevant lines covered (66.17%)

2956.13 hits per line

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

88.46
/packages/lists/init.lua
1
local base = require("packages.base")
3✔
2

3
local package = pl.class(base)
3✔
4
package._name = "lists"
3✔
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

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

62
local trimLeft = function (str)
63
   return str:gsub("^%s*", "")
52✔
64
end
65

66
local trimRight = function (str)
67
   return str:gsub("%s*$", "")
52✔
68
end
69

70
local trim = function (str)
71
   return trimRight(trimLeft(str))
104✔
72
end
73

74
local enforceListType = function (cmd)
75
   if cmd ~= "enumerate" and cmd ~= "itemize" and cmd ~= "BulletedList" and cmd ~= "OrderedList" then
14✔
NEW
76
      SU.error("Only items or lists are allowed as content in lists, found '" .. cmd .. "'")
×
77
   end
78
end
79

80
function package:doItem (options, content)
3✔
81
   local enumStyle = content._lists_.style
28✔
82
   local counter = content._lists_.counter
28✔
83
   local indent = content._lists_.indent
28✔
84

85
   if not SILE.typesetter:vmode() then
56✔
86
      SILE.call("par")
×
87
   end
88

89
   local mark = SILE.typesetter:makeHbox(function ()
56✔
90
      if enumStyle.display then
28✔
91
         if enumStyle.before then
10✔
92
            SILE.typesetter:typeset(enumStyle.before)
×
93
         end
94
         SILE.typesetter:typeset(self.class.packages.counters:formatCounter({
30✔
95
            value = counter,
10✔
96
            display = enumStyle.display,
10✔
97
         }))
98
         if enumStyle.after then
10✔
99
            SILE.typesetter:typeset(enumStyle.after)
10✔
100
         end
101
      else
102
         local bullet = options.bullet or enumStyle.bullet
18✔
103
         SILE.typesetter:typeset(bullet)
18✔
104
      end
105
   end)
106

107
   local stepback
108
   if enumStyle.display then
28✔
109
      -- The positioning is quite tentative... LaTeX would right justify the
110
      -- number (at least for roman numerals), i.e.
111
      --   i. Text
112
      --  ii. Text
113
      -- iii. Text.
114
      -- Other Office software do not do that...
115
      local labelIndent = SILE.settings:get("lists.enumerate.labelindent"):absolute()
20✔
116
      stepback = indent - labelIndent
20✔
117
   else
118
      -- Center bullets in the indentation space
119
      stepback = indent / 2 + mark.width / 2
54✔
120
   end
121

122
   SILE.call("kern", { width = -stepback })
56✔
123
   -- reinsert the mark with modified length
124
   -- using \rebox caused an issue sometimes, not sure why, with the bullets
125
   -- appearing twice in output... but we can avoid it:
126
   -- reboxing an hbox was dumb anyway. We just need to fix its width before
127
   -- reinserting it in the text flow.
128
   mark.width = SILE.types.length(stepback)
56✔
129
   SILE.typesetter:pushHbox(mark)
28✔
130
   SILE.process(content)
28✔
131
end
132

133
function package.doNestedList (_, listType, options, content)
3✔
134
   -- depth
135
   local depth = SILE.settings:get("lists.current." .. listType .. ".depth") + 1
48✔
136

137
   -- styling
138
   local enumStyle = styles[listType][depth]
24✔
139
   if not enumStyle then
24✔
140
      SU.error("List nesting is too deep")
×
141
   end
142
   -- options may override the default styling
143
   enumStyle = pl.tablex.copy(enumStyle) -- shallow copy for possible overrides
48✔
144
   if enumStyle.display then
24✔
145
      if options.before or options.after then
10✔
146
         -- for before/after, don't mix default style and options
147
         enumStyle.before = options.before or ""
×
148
         enumStyle.after = options.after or ""
×
149
      end
150
      if options.display then
10✔
151
         enumStyle.display = options.display
×
152
      end
153
   else
154
      enumStyle.bullet = options.bullet or enumStyle.bullet
14✔
155
   end
156

157
   -- indent
158
   local baseIndent = (depth == 1) and SILE.settings:get("document.parindent").width:absolute()
34✔
159
      or SILE.types.measurement("0pt")
24✔
160
   local listIndent = SILE.settings:get("lists." .. listType .. ".leftmargin"):absolute()
48✔
161

162
   -- processing
163
   if not SILE.typesetter:vmode() then
48✔
164
      SILE.call("par")
7✔
165
   end
166
   SILE.settings:temporarily(function ()
48✔
167
      SILE.settings:set("lists.current." .. listType .. ".depth", depth)
24✔
168
      SILE.settings:set("current.parindent", SILE.types.node.glue())
48✔
169
      SILE.settings:set("document.parindent", SILE.types.node.glue())
48✔
170
      SILE.settings:set("document.parskip", SILE.settings:get("lists.parskip"))
48✔
171
      local lskip = SILE.settings:get("document.lskip") or SILE.types.node.glue()
48✔
172
      SILE.settings:set("document.lskip", SILE.types.node.glue(lskip.width + (baseIndent + listIndent)))
96✔
173

174
      local counter = options.start and (SU.cast("integer", options.start) - 1) or 0
24✔
175
      for i = 1, #content do
118✔
176
         if type(content[i]) == "table" and #content[i] > 0 then
94✔
177
            if content[i].command == "item" or content[i].command == "ListItem" then
42✔
178
               counter = counter + 1
28✔
179
               -- Enrich the node with internal properties
180
               content[i]._lists_ = {
28✔
181
                  style = enumStyle,
28✔
182
                  counter = counter,
28✔
183
                  indent = listIndent,
28✔
184
               }
28✔
185
            else
186
               enforceListType(content[i].command)
14✔
187
            end
188
            SILE.process({ content[i] })
42✔
189
            if not SILE.typesetter:vmode() then
84✔
190
               SILE.call("par")
52✔
191
            else
192
               SILE.typesetter:leaveHmode()
16✔
193
            end
194
         -- Whitespace left around comment nodes is fine too
195
         elseif type(content[i]) == "table" and #content[i] == 0 then
52✔
196
            -- ignore whitespace leaking in from in front of indented comments
NEW
197
            assert(true)
×
198
         elseif type(content[i]) == "string" then
52✔
199
            -- All text nodes are ignored in structure tags, but just warn
200
            -- if there do not just consist in spaces.
201
            local text = trim(content[i])
52✔
202
            if text ~= "" then
52✔
203
               SU.warn("Ignored standalone text (" .. text .. ")")
×
204
            end
205
         else
206
            SU.error("List structure error")
×
207
         end
208
      end
209
   end)
210

211
   if not SILE.typesetter:vmode() then
48✔
212
      SILE.call("par")
×
213
   else
214
      SILE.typesetter:leaveHmode()
24✔
215
      if
216
         not (
×
217
            (SILE.settings:get("lists.current.itemize.depth") + SILE.settings:get("lists.current.enumerate.depth")) > 0
72✔
218
         )
219
      then
220
         local g = SILE.settings:get("document.parskip").height:absolute()
12✔
221
            - SILE.settings:get("lists.parskip").height:absolute()
18✔
222
         SILE.typesetter:pushVglue(g)
6✔
223
      end
224
   end
225
end
226

227
function package:_init ()
3✔
228
   base._init(self)
3✔
229
   self:loadPackage("counters")
3✔
230
end
231

232
function package.declareSettings (_)
3✔
233
   SILE.settings:declare({
3✔
234
      parameter = "lists.current.enumerate.depth",
235
      type = "integer",
236
      default = 0,
237
      help = "Current enumerate depth (nesting) - internal",
238
   })
239

240
   SILE.settings:declare({
3✔
241
      parameter = "lists.current.itemize.depth",
242
      type = "integer",
243
      default = 0,
244
      help = "Current itemize depth (nesting) - internal",
245
   })
246

247
   SILE.settings:declare({
6✔
248
      parameter = "lists.enumerate.leftmargin",
249
      type = "measurement",
250
      default = SILE.types.measurement("2em"),
6✔
251
      help = "Left margin (indentation) for enumerations",
252
   })
253

254
   SILE.settings:declare({
6✔
255
      parameter = "lists.enumerate.labelindent",
256
      type = "measurement",
257
      default = SILE.types.measurement("0.5em"),
6✔
258
      help = "Label indentation for enumerations",
259
   })
260

261
   SILE.settings:declare({
6✔
262
      parameter = "lists.itemize.leftmargin",
263
      type = "measurement",
264
      default = SILE.types.measurement("1.5em"),
6✔
265
      help = "Left margin (indentation) for bullet lists (itemize)",
266
   })
267

268
   SILE.settings:declare({
6✔
269
      parameter = "lists.parskip",
270
      type = "vglue",
271
      default = SILE.types.node.vglue("0pt plus 1pt"),
6✔
272
      help = "Leading between paragraphs and items in a list",
273
   })
274
end
275

276
function package:registerCommands ()
3✔
277
   self:registerCommand("enumerate", function (options, content)
6✔
278
      self:doNestedList("enumerate", options, content)
10✔
279
   end)
280

281
   self:registerCommand("itemize", function (options, content)
6✔
282
      self:doNestedList("itemize", options, content)
14✔
283
   end)
284

285
   self:registerCommand("item", function (options, content)
6✔
286
      if not content._lists_ then
28✔
287
         SU.error("The item command shall not be called outside a list")
×
288
      end
289
      self:doItem(options, content)
28✔
290
   end)
291
end
292

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

298
\smallskip
299
\noindent
300
\em{Itemized lists}
301
\novbreak
302

303
\indent
304
The \autodoc:environment{itemize} environment initiates a itemized list.
305
Each item, unsurprisingly, is wrapped in an \autodoc:command{\item} command.
306

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

310
\begin{itemize}
311
    \item{Lorem}
312
    \begin{itemize}
313
        \item{Ipsum}
314
        \begin{itemize}
315
            \item{Dolor}
316
        \end{itemize}
317
    \end{itemize}
318
\end{itemize}
319

320
The current implementation supports up to six indentation levels.
321

322
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.
323
Note that if your document has a paragraph indent enabled at this point, it is also added to the first list level.
324

325
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.
326
You can also force a specific bullet character to be used on a specific item with \autodoc:command{\item[bullet=<character>]}.
327

328
\smallskip
329
\noindent
330
\em{Enumerated lists}
331
\novbreak
332

333
\indent
334
The \autodoc:environment{enumerate} environment initiates an enumeration.
335
Each item shall, again, be wrapped in an \autodoc:command{\item} command.
336
This environment too is regarded as a structure, so the same rules as above apply.
337

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

340
\begin{enumerate}
341
    \item{Lorem}
342
    \begin{enumerate}
343
        \item{Ipsum}
344
        \begin{enumerate}
345
            \item{Dolor}
346
        \end{enumerate}
347
    \end{enumerate}
348
\end{enumerate}
349

350
The current implementation supports up to six indentation levels.
351

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

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

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

364
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.
365

366
\smallskip
367
\noindent
368
\em{Nesting}
369
\novbreak
370

371
\indent
372
Both environments can be nested.
373
The way they do is best illustrated by an example.
374

375
\begin{enumerate}
376
    \item{Lorem}
377
    \begin{enumerate}
378
        \item{Ipsum}
379
        \begin{itemize}
380
            \item{Dolor}
381
            \begin{enumerate}
382
                \item{Sit amet}
383
                \begin{itemize}
384
                    \item{Consectetur}
385
                \end{itemize}
386
            \end{enumerate}
387
        \end{itemize}
388
    \end{enumerate}
389
\end{enumerate}
390

391
\smallskip
392
\noindent
393
\em{Vertical spaces}
394
\novbreak
395

396
\indent
397
The package tries to ensure a paragraph is enforced before and after a list.
398
In most cases, this implies paragraph skips to be inserted, with the usual \autodoc:setting{document.parskip} glue, whatever value it has at these points in the surrounding context of your document.
399
Between list items, however, the paragraph skip is switched to the value of the \autodoc:setting{lists.parskip} setting.
400

401
\smallskip
402
\noindent
403
\em{Other considerations}
404
\novbreak
405

406
\indent
407
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.
408
Please be a good typographer. Also, these lists have not yet been tried in right-to-left or vertical writing direction.
409

410
\font:remove-fallback
411
\end{document}
UNCOV
412
]]
×
413

UNCOV
414
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