local base = require("packages.base")
local package = pl.class(base)
package._name = "lists"
local styles = {
enumerate = {
{ display = "arabic", after = "." },
{ display = "roman", after = "." },
{ display = "alpha", after = "." },
{ display = "arabic", after = ")" },
{ display = "roman", after = ")" },
{ display = "alpha", after = ")" },
},
itemize = {
{ bullet = "•" }, { bullet = "◦" }, { bullet = "–" }, { bullet = "•" }, { bullet = "◦" }, { bullet = "–" }, },
}
local enforceListType = function (cmd)
if cmd ~= "enumerate" and cmd ~= "itemize" and cmd ~= "BulletedList" and cmd ~= "OrderedList" then
SU.error("Only items or lists are allowed as content in lists, found '" .. cmd .. "'")
end
end
local lastListSpacing
local function getNestedDepth ()
local itemize_level = SILE.settings:get("lists.current.itemize.depth")
local enumerate_level = SILE.settings:get("lists.current.enumerate.depth")
return itemize_level + enumerate_level
end
local function pushListSpacing ()
if #SILE.typesetter.state.outputQueue ~= lastListSpacing then
SILE.typesetter:pushVglue(SILE.settings:get("lists.parskip"))
lastListSpacing = #SILE.typesetter.state.outputQueue
end
end
local function popListSpacing ()
if lastListSpacing then
local node = SILE.typesetter.state.outputQueue[lastListSpacing]
if not node.is_vglue then
SU.error("Attempted to pop something other than vertical spacing", true)
end
table.remove(SILE.typesetter.state.outputQueue, lastListSpacing)
end
end
local function maybeAddListSpacing (islist, entering, counter)
local depth = getNestedDepth() + (islist and 1 or 0)
local isitem = not islist
local leaving = not entering
SILE.typesetter:leaveHmode()
if entering and isitem and (counter ~= 1 or depth >= 2) then
pushListSpacing()
end
if leaving and islist then
pushListSpacing()
if depth == 1 then
popListSpacing()
end
end
end
function package:doItem (options, content)
local enumStyle = content._lists_.style
local counter = content._lists_.counter
local indent = content._lists_.indent
maybeAddListSpacing(false, true, counter)
local mark = SILE.typesetter:makeHbox(function ()
if enumStyle.display then
if enumStyle.before then
SILE.typesetter:typeset(enumStyle.before)
end
SILE.typesetter:typeset(self.class.packages.counters:formatCounter({
value = counter,
display = enumStyle.display,
}))
if enumStyle.after then
SILE.typesetter:typeset(enumStyle.after)
end
else
local bullet = options.bullet or enumStyle.bullet
SILE.typesetter:typeset(bullet)
end
end)
local stepback
if enumStyle.display then
local labelIndent = SILE.settings:get("lists.enumerate.labelindent"):absolute()
stepback = indent - labelIndent
else
stepback = indent / 2 + mark.width / 2
end
SILE.call("kern", { width = -stepback })
mark.width = SILE.types.length(stepback)
SILE.typesetter:pushHbox(mark)
SILE.process(content)
SILE.settings:set("current.parindent", SILE.types.node.glue())
maybeAddListSpacing(false, false, counter)
end
function package.doNestedList (_, listType, options, content)
local depth = SILE.settings:get("lists.current." .. listType .. ".depth") + 1
local enumStyle = styles[listType][(depth - 1) % 6 + 1]
enumStyle = pl.tablex.copy(enumStyle) if enumStyle.display then
if options.before or options.after then
enumStyle.before = options.before or ""
enumStyle.after = options.after or ""
end
if options.display then
enumStyle.display = options.display
end
else
enumStyle.bullet = options.bullet or enumStyle.bullet
end
local baseIndent = (depth == 1) and SILE.settings:get("document.parindent").width:absolute()
or SILE.types.measurement("0pt")
local listIndent = SILE.settings:get("lists." .. listType .. ".leftmargin"):absolute()
maybeAddListSpacing(true, true, nil)
SILE.settings:temporarily(function ()
SILE.settings:set("lists.current." .. listType .. ".depth", depth)
SILE.settings:set("current.parindent", SILE.types.node.glue())
SILE.settings:set("document.parindent", SILE.types.node.glue())
local lskip = SILE.settings:get("document.lskip") or SILE.types.node.glue()
SILE.settings:set("document.lskip", SILE.types.node.glue(lskip.width + (baseIndent + listIndent)))
local counter = options.start and (SU.cast("integer", options.start) - 1) or 0
for i = 1, #content do
if type(content[i]) == "table" and #content[i] > 0 then
if content[i].command == "item" or content[i].command == "ListItem" then
counter = counter + 1
content[i]._lists_ = {
style = enumStyle,
counter = counter,
indent = listIndent,
}
else
enforceListType(content[i].command)
end
SILE.process({ content[i] })
elseif type(content[i]) == "table" and #content[i] == 0 then
assert(true)
elseif type(content[i]) == "string" then
local text = pl.stringx.strip(content[i])
if text ~= "" then
SU.warn("Ignored standalone text (" .. text .. ")")
end
else
SU.error("List structure error")
end
end
end)
maybeAddListSpacing(true, false, nil)
end
function package:_init ()
base._init(self)
self:loadPackage("counters")
end
function package.declareSettings (_)
SILE.settings:declare({
parameter = "lists.current.enumerate.depth",
type = "integer",
default = 0,
help = "Current enumerate depth (nesting) - internal",
})
SILE.settings:declare({
parameter = "lists.current.itemize.depth",
type = "integer",
default = 0,
help = "Current itemize depth (nesting) - internal",
})
SILE.settings:declare({
parameter = "lists.enumerate.leftmargin",
type = "measurement",
default = SILE.types.measurement("2em"),
help = "Left margin (indentation) for enumerations",
})
SILE.settings:declare({
parameter = "lists.enumerate.labelindent",
type = "measurement",
default = SILE.types.measurement("0.5em"),
help = "Label indentation for enumerations",
})
SILE.settings:declare({
parameter = "lists.itemize.leftmargin",
type = "measurement",
default = SILE.types.measurement("1.5em"),
help = "Left margin (indentation) for bullet lists (itemize)",
})
SILE.settings:declare({
parameter = "lists.parskip",
type = "vglue",
default = SILE.types.node.vglue("0pt plus 1pt"),
help = "Leading between items in a list",
})
end
function package:registerCommands ()
self:registerCommand("enumerate", function (options, content)
self:doNestedList("enumerate", options, content)
end)
self:registerCommand("itemize", function (options, content)
self:doNestedList("itemize", options, content)
end)
self:registerCommand("item", function (options, content)
if not content._lists_ then
SU.error("The item command shall not be called outside a list")
end
self:doItem(options, content)
end)
end
package.documentation = [[
\begin{document}
\font:add-fallback[family=Symbola]% HACK Gentium Plus (SILE default font) lacks the circle bullet :(
The \autodoc:package{lists} package provides enumerated and itemized (also known as \em{bulleted lists}) which can be nested together.
\smallskip
\noindent
\em{Itemized lists}
\novbreak
\indent
The \autodoc:environment{itemize} environment initiates a itemized list.
Each item, unsurprisingly, is wrapped in an \autodoc:command{\item} command.
The environment, as a structure or data model, can only contain \code{item} elements or other lists.
Any other element causes an error to be reported, and any text content is ignored with a warning.
\begin{itemize}
\item{Lorem}
\begin{itemize}
\item{Ipsum}
\begin{itemize}
\item{Dolor}
\end{itemize}
\end{itemize}
\end{itemize}
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.
Note that if your document has a paragraph indent enabled at this point, it is also added to the first list level.
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.
You can also force a specific bullet character to be used on a specific item with \autodoc:command{\item[bullet=<character>]}.
\smallskip
\noindent
\em{Enumerated lists}
\novbreak
\indent
The \autodoc:environment{enumerate} environment initiates an enumeration.
Each item shall, again, be wrapped in an \autodoc:command{\item} command.
This environment too is regarded as a structure, so the same rules as above apply.
The enumeration starts at one, unless you specify the \autodoc:parameter{start=<integer>} option (a numeric value, regardless of the display format).
\begin{enumerate}
\item{Lorem}
\begin{enumerate}
\item{Ipsum}
\begin{enumerate}
\item{Dolor}
\end{enumerate}
\end{enumerate}
\end{enumerate}
On each level, the indentation is defined by the \autodoc:setting{lists.enumerate.leftmargin} setting (defaults to \code{2em}).
Note, again, that if your document has a paragraph indent enabled at this point, it is also added to the first list level.
% And… ah, at least something less repetitive than a raw list of features.
% \em{Quite obviously}, we cannot center the label.
% Roman numbers, folks, if any reason is required.
The \autodoc:setting{lists.enumerate.labelindent} setting specifies the distance between the label and the previous indentation level (defaults to \code{0.5em}).
Tune these settings at your convenience depending on your styles.
If there is a more general solution to this subtle issue, we accept patches.%
\footnote{TeX typesets the enumeration label ragged left. Most word processing software do not.}
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.
\smallskip
\noindent
\em{Nesting}
\novbreak
\indent
Both environments can be nested.
The way they do is best illustrated by an example.
\begin{enumerate}
\item{Lorem}
\begin{enumerate}
\item{Ipsum}
\begin{itemize}
\item{Dolor}
\begin{enumerate}
\item{Sit amet}
\begin{itemize}
\item{Consectetur}
\end{itemize}
\end{enumerate}
\end{itemize}
\end{enumerate}
\end{enumerate}
\smallskip
\noindent
\em{Vertical spaces}
\novbreak
\indent
The package outputs lists starting after a line break, but it does not enforce a paragraph break before or after the list.
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.
Between list items, however, the paragraph skip is switched to the value of the \autodoc:setting{lists.parskip} setting.
\smallskip
\noindent
\em{Other considerations}
\novbreak
\indent
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.
Please be a good typographer. Also, these lists have not yet been tried in right-to-left or vertical writing direction.
\font:remove-fallback
\end{document}
]]
return package