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 trimLeft = function (str)
return str:gsub("^%s*", "")
end
local trimRight = function (str)
return str:gsub("%s*$", "")
end
local trim = function (str)
return trimRight(trimLeft(str))
end
local enforceListType = function (cmd)
if cmd ~= "enumerate" and cmd ~= "itemize" then
SU.error("Only 'enumerate', 'itemize' or 'item' are accepted in lists, found '"..cmd.."'")
end
end
function package:doItem (options, content)
local enumStyle = content._lists_.style
local counter = content._lists_.counter
local indent = content._lists_.indent
if not SILE.typesetter:vmode() then
SILE.call("par")
end
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.length(stepback)
SILE.typesetter:pushHbox(mark)
SILE.process(content)
end
function package.doNestedList (_, listType, options, content)
local depth = SILE.settings:get("lists.current."..listType..".depth") + 1
local enumStyle = styles[listType][depth]
if not enumStyle then SU.error("List nesting is too deep") end
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.measurement("0pt")
local listIndent = SILE.settings:get("lists."..listType..".leftmargin"):absolute()
if not SILE.typesetter:vmode() then
SILE.call("par")
end
SILE.settings:temporarily(function ()
SILE.settings:set("lists.current."..listType..".depth", depth)
SILE.settings:set("current.parindent", SILE.nodefactory.glue())
SILE.settings:set("document.parindent", SILE.nodefactory.glue())
SILE.settings:set("document.parskip", SILE.settings:get("lists.parskip"))
local lskip = SILE.settings:get("document.lskip") or SILE.nodefactory.glue()
SILE.settings:set("document.lskip", SILE.nodefactory.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" then
if content[i].command == "item" then
counter = counter + 1
content[i]._lists_ = {
style = enumStyle,
counter = counter,
indent = listIndent,
}
else
enforceListType(content[i].command)
end
SILE.process({ content[i] })
if not SILE.typesetter:vmode() then
SILE.call("par")
else
SILE.typesetter:leaveHmode()
end
elseif type(content[i]) == "string" then
local text = trim(content[i])
if text ~= "" then SU.warn("Ignored standalone text ("..text..")") end
else
SU.error("List structure error")
end
end
end)
if not SILE.typesetter:vmode() then
SILE.call("par")
else
SILE.typesetter:leaveHmode()
if not((SILE.settings:get("lists.current.itemize.depth")
+ SILE.settings:get("lists.current.enumerate.depth")) > 0)
then
local g = SILE.settings:get("document.parskip").height:absolute() - SILE.settings:get("lists.parskip").height:absolute()
SILE.typesetter:pushVglue(g)
end
end
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.measurement("2em"),
help = "Left margin (indentation) for enumerations"
})
SILE.settings:declare({
parameter = "lists.enumerate.labelindent",
type = "measurement",
default = SILE.measurement("0.5em"),
help = "Label indentation for enumerations"
})
SILE.settings:declare({
parameter = "lists.itemize.leftmargin",
type = "measurement",
default = SILE.measurement("1.5em"),
help = "Left margin (indentation) for bullet lists (itemize)"
})
SILE.settings:declare({
parameter = "lists.parskip",
type = "vglue",
default = SILE.nodefactory.vglue("0pt plus 1pt"),
help = "Leading between paragraphs and 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}
The current implementation supports up to six indentation levels.
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}
The current implementation supports up to six indentation levels.
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 tries to ensure a paragraph is enforced before and after a list.
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.
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