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

sile-typesetter / sile / 6713098919

31 Oct 2023 10:21PM UTC coverage: 52.831% (-21.8%) from 74.636%
6713098919

push

github

web-flow
Merge d0a2a1ee9 into b185d4972

45 of 45 new or added lines in 3 files covered. (100.0%)

8173 of 15470 relevant lines covered (52.83%)

6562.28 hits per line

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

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

3
local package = pl.class(base)
×
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

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

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

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

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

74
local enforceListType = function (cmd)
75
  if cmd ~= "enumerate" and cmd ~= "itemize" then
×
76
    SU.error("Only 'enumerate', 'itemize' or 'item' are accepted in lists, found '"..cmd.."'")
×
77
  end
78
end
79

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

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

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

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

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

129
function package.doNestedList (_, listType, options, content)
×
130
  -- depth
131
  local depth = SILE.settings:get("lists.current."..listType..".depth") + 1
×
132

133
  -- styling
134
  local enumStyle = styles[listType][depth]
×
135
  if not enumStyle then SU.error("List nesting is too deep") end
×
136
  -- options may override the default styling
137
  enumStyle = pl.tablex.copy(enumStyle) -- shallow copy for possible overrides
×
138
  if enumStyle.display then
×
139
    if options.before or options.after then
×
140
      -- for before/after, don't mix default style and options
141
      enumStyle.before = options.before or ""
×
142
      enumStyle.after = options.after or ""
×
143
    end
144
    if options.display then enumStyle.display = options.display end
×
145
  else
146
    enumStyle.bullet = options.bullet or enumStyle.bullet
×
147
  end
148

149
  -- indent
150
  local baseIndent = (depth == 1) and SILE.settings:get("document.parindent").width:absolute() or SILE.measurement("0pt")
×
151
  local listIndent = SILE.settings:get("lists."..listType..".leftmargin"):absolute()
×
152

153
  -- processing
154
  if not SILE.typesetter:vmode() then
×
155
    SILE.call("par")
×
156
  end
157
  SILE.settings:temporarily(function ()
×
158
    SILE.settings:set("lists.current."..listType..".depth", depth)
×
159
    SILE.settings:set("current.parindent", SILE.nodefactory.glue())
×
160
    SILE.settings:set("document.parindent", SILE.nodefactory.glue())
×
161
    SILE.settings:set("document.parskip", SILE.settings:get("lists.parskip"))
×
162
    local lskip = SILE.settings:get("document.lskip") or SILE.nodefactory.glue()
×
163
    SILE.settings:set("document.lskip", SILE.nodefactory.glue(lskip.width + (baseIndent + listIndent)))
×
164

165
    local counter = options.start and (SU.cast("integer", options.start) - 1) or 0
×
166
    for i = 1, #content do
×
167
      if type(content[i]) == "table" then
×
168
        if content[i].command == "item" then
×
169
          counter = counter + 1
×
170
          -- Enrich the node with internal properties
171
          content[i]._lists_ = {
×
172
            style = enumStyle,
173
            counter = counter,
174
            indent = listIndent,
175
          }
176
        else
177
          enforceListType(content[i].command)
×
178
        end
179
        SILE.process({ content[i] })
×
180
        if not SILE.typesetter:vmode() then
×
181
          SILE.call("par")
×
182
        else
183
          SILE.typesetter:leaveHmode()
×
184
        end
185
      elseif type(content[i]) == "string" then
×
186
        -- All text nodes are ignored in structure tags, but just warn
187
        -- if there do not just consist in spaces.
188
        local text = trim(content[i])
×
189
        if text ~= "" then SU.warn("Ignored standalone text ("..text..")") end
×
190
      else
191
        SU.error("List structure error")
×
192
      end
193
    end
194
  end)
195

196
  if not SILE.typesetter:vmode() then
×
197
      SILE.call("par")
×
198
  else
199
    SILE.typesetter:leaveHmode()
×
200
    if not((SILE.settings:get("lists.current.itemize.depth")
×
201
        + SILE.settings:get("lists.current.enumerate.depth")) > 0)
×
202
    then
203
      local g = SILE.settings:get("document.parskip").height:absolute() - SILE.settings:get("lists.parskip").height:absolute()
×
204
      SILE.typesetter:pushVglue(g)
×
205
    end
206
  end
207
end
208

209
function package:_init ()
×
210
  base._init(self)
×
211
  self:loadPackage("counters")
×
212
end
213

214
function package.declareSettings (_)
×
215

216
  SILE.settings:declare({
×
217
    parameter = "lists.current.enumerate.depth",
218
    type = "integer",
219
    default = 0,
220
    help = "Current enumerate depth (nesting) - internal"
×
221
  })
222

223
  SILE.settings:declare({
×
224
    parameter = "lists.current.itemize.depth",
225
    type = "integer",
226
    default = 0,
227
    help = "Current itemize depth (nesting) - internal"
×
228
  })
229

230
  SILE.settings:declare({
×
231
    parameter = "lists.enumerate.leftmargin",
232
    type = "measurement",
233
    default = SILE.measurement("2em"),
234
    help = "Left margin (indentation) for enumerations"
×
235
  })
236

237
  SILE.settings:declare({
×
238
    parameter = "lists.enumerate.labelindent",
239
    type = "measurement",
240
    default = SILE.measurement("0.5em"),
241
    help = "Label indentation for enumerations"
×
242
  })
243

244
  SILE.settings:declare({
×
245
    parameter = "lists.itemize.leftmargin",
246
    type = "measurement",
247
    default = SILE.measurement("1.5em"),
248
    help = "Left margin (indentation) for bullet lists (itemize)"
×
249
  })
250

251
  SILE.settings:declare({
×
252
    parameter = "lists.parskip",
253
    type = "vglue",
254
    default = SILE.nodefactory.vglue("0pt plus 1pt"),
255
    help = "Leading between paragraphs and items in a list"
×
256
  })
257

258
end
259

260
function package:registerCommands ()
×
261

262
  self:registerCommand("enumerate", function (options, content)
×
263
    self:doNestedList("enumerate", options, content)
×
264
  end)
265

266
  self:registerCommand("itemize", function (options, content)
×
267
    self:doNestedList("itemize", options, content)
×
268
  end)
269

270
  self:registerCommand("item", function (options, content)
×
271
    if not content._lists_ then
×
272
      SU.error("The item command shall not be called outside a list")
×
273
    end
274
    self:doItem(options, content)
×
275
  end)
276

277
end
278

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

284
\smallskip
285
\noindent
286
\em{Itemized lists}
287
\novbreak
288

289
\indent
290
The \autodoc:environment{itemize} environment initiates a itemized list.
291
Each item, unsurprisingly, is wrapped in an \autodoc:command{\item} command.
292

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

296
\begin{itemize}
297
    \item{Lorem}
298
    \begin{itemize}
299
        \item{Ipsum}
300
        \begin{itemize}
301
            \item{Dolor}
302
        \end{itemize}
303
    \end{itemize}
304
\end{itemize}
305

306
The current implementation supports up to six indentation levels.
307

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

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

314
\smallskip
315
\noindent
316
\em{Enumerated lists}
317
\novbreak
318

319
\indent
320
The \autodoc:environment{enumerate} environment initiates an enumeration.
321
Each item shall, again, be wrapped in an \autodoc:command{\item} command.
322
This environment too is regarded as a structure, so the same rules as above apply.
323

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

326
\begin{enumerate}
327
    \item{Lorem}
328
    \begin{enumerate}
329
        \item{Ipsum}
330
        \begin{enumerate}
331
            \item{Dolor}
332
        \end{enumerate}
333
    \end{enumerate}
334
\end{enumerate}
335

336
The current implementation supports up to six indentation levels.
337

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

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

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

350
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.
351

352
\smallskip
353
\noindent
354
\em{Nesting}
355
\novbreak
356

357
\indent
358
Both environments can be nested.
359
The way they do is best illustrated by an example.
360

361
\begin{enumerate}
362
    \item{Lorem}
363
    \begin{enumerate}
364
        \item{Ipsum}
365
        \begin{itemize}
366
            \item{Dolor}
367
            \begin{enumerate}
368
                \item{Sit amet}
369
                \begin{itemize}
370
                    \item{Consectetur}
371
                \end{itemize}
372
            \end{enumerate}
373
        \end{itemize}
374
    \end{enumerate}
375
\end{enumerate}
376

377
\smallskip
378
\noindent
379
\em{Vertical spaces}
380
\novbreak
381

382
\indent
383
The package tries to ensure a paragraph is enforced before and after a list.
384
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.
385
Between list items, however, the paragraph skip is switched to the value of the \autodoc:setting{lists.parskip} setting.
386

387
\smallskip
388
\noindent
389
\em{Other considerations}
390
\novbreak
391

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

396
\font:remove-fallback
397
\end{document}
398
]]
×
399

400
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

© 2025 Coveralls, Inc