Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework simple and multilevel counters #1541

Merged
merged 6 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions packages/counters/counters_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
SILE = require("core.sile")
SILE.backend = "dummy"
SILE.init()
SILE.utilities.error = error

describe("#counters #package", function ()
-- We need to instantiate a class first, in order to load a package.
-- Minimally emulate what SILE does.
local constructor = require("classes.plain")
local class = constructor({})
SILE.documentState.documentClass = class
-- Then we load the package and can use it as actual code would do.
class:loadPackage("counters")

it("should create a simple counter", function ()
local count = class.packages.counters:formatCounter(class:getCounter("simple"))
assert.is.equal("0", count)
end)

it("should increment a simple counter", function ()
SILE.call("increment-counter", { id = "simple" })
local count = class.packages.counters:formatCounter(class:getCounter("simple"))
assert.is.equal("1", count)
end)

it("should increment a simple counter, changing its display", function ()
SILE.call("increment-counter", { id = "simple", display = "roman" })
local count = class.packages.counters:formatCounter(class:getCounter("simple"))
assert.is.equal("ii", count)
end)

it("should set a simple counter", function ()
SILE.call("set-counter", { id = "simple", value = 5 } )
local count = class.packages.counters:formatCounter(class:getCounter("simple"))
assert.is.equal("v", count)
end)

it("should set a simple counter, changing its display", function ()
SILE.call("set-counter", { id = "simple", display = "alpha" } )
local count = class.packages.counters:formatCounter(class:getCounter("simple"))
assert.is.equal("e", count)
end)

it("should create a multilevel counter", function ()
local count = class.packages.counters:formatMultilevelCounter(class:getMultilevelCounter("multi"))
assert.is.equal("0", count)
end)

it("should increment a multilevel counter at current level", function ()
SILE.call("increment-multilevel-counter", { id = "multi" })
local count = class.packages.counters:formatMultilevelCounter(class:getMultilevelCounter("multi"))
assert.is.equal("1", count)
end)

it("should increment a multilevel counter at level 2", function ()
SILE.call("increment-multilevel-counter", { id = "multi", level = 2 })
local count = class.packages.counters:formatMultilevelCounter(class:getMultilevelCounter("multi"))
assert.is.equal("1.1", count)
end)

it("should increment a multilevel counter at level 3", function ()
SILE.call("increment-multilevel-counter", { id = "multi", level = 3 })
local count = class.packages.counters:formatMultilevelCounter(class:getMultilevelCounter("multi"))
assert.is.equal("1.1.1", count)
end)

it("should increment a multilevel counter at level 2, clearing sub-levels", function ()
SILE.call("increment-multilevel-counter", { id = "multi", level = 2 })
local count = class.packages.counters:formatMultilevelCounter(class:getMultilevelCounter("multi"))
assert.is.equal("1.2", count)
end)

it("should set a multilevel counter", function ()
SILE.call("set-multilevel-counter", { id = "multi", level = 3, value = 5 } )
local count = class.packages.counters:formatMultilevelCounter(class:getMultilevelCounter("multi"))
assert.is.equal("1.2.5", count)
end)

it("should set a multilevel counter, changing its display", function ()
SILE.call("set-multilevel-counter", { id = "multi", level = 3, display = "alpha" } )
local count = class.packages.counters:formatMultilevelCounter(class:getMultilevelCounter("multi"))
assert.is.equal("1.2.e", count)
end)

it("should format a counter with or without leading zeros", function ()
SILE.call("set-multilevel-counter", { id = "multi", level = 1, value = 0 } )
SILE.call("increment-multilevel-counter", { id = "multi", level = 2 })
local count = class.packages.counters:formatMultilevelCounter(class:getMultilevelCounter("multi"))
assert.is.equal("0.1", count)
count = class.packages.counters:formatMultilevelCounter(class:getMultilevelCounter("multi"), { noleadingzeros = true })
assert.is.equal("1", count)
end)
end)
159 changes: 129 additions & 30 deletions packages/counters/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,31 @@ SILE.formatMultilevelCounter = function (counter, options)
end

local function getCounter (_, id)
if not SILE.scratch.counters[id] then
SILE.scratch.counters[id] = {
local counter = SILE.scratch.counters[id]
if not counter then
counter = {
value = 0,
display = "arabic",
format = package.formatCounter
}
SILE.scratch.counters[id] = counter
elseif type(counter.value) ~= "number" then
SU.error("Counter " .. id .. " is not a single-level counter")
end
return SILE.scratch.counters[id]
return counter
end

local function getMultilevelCounter (_, id)
local counter = SILE.scratch.counters[id]
if not counter then
counter = {
value= { 0 },
display= { "arabic" },
value = { 0 },
display = { "arabic" },
format = package.formatMultilevelCounter
}
SILE.scratch.counters[id] = counter
elseif type(counter.value) ~= "table" then
SU.error("Counter " .. id .. " is not a multi-level counter")
end
return counter
end
Expand All @@ -42,9 +48,14 @@ function package.formatCounter (_, counter)
end

function package:formatMultilevelCounter (counter, options)
local maxlevel = options and options.level or #counter.value
local minlevel = options and options.minlevel or 1
options = options or {}
local maxlevel = options.level and SU.min(SU.cast("integer", options.level), #counter.value) or #counter.value
-- Option minlevel is undocumented and should perhaps be deprecated: is there a real use case for it?
local minlevel = options.minlevel and SU.min(SU.cast("integer", options.minlevel, #counter.value)) or 1
local out = {}
if SU.boolean(options.noleadingzeros, false) then
while counter.value[minlevel] == 0 do minlevel = minlevel + 1 end -- skip leading zeros
end
for x = minlevel, maxlevel do
out[x - minlevel + 1] = self:formatCounter({ display = counter.display[x], value = counter.value[x] })
end
Expand All @@ -65,57 +76,125 @@ end
function package:registerCommands ()

self:registerCommand("increment-counter", function (options, _)
local counter = self.class:getCounter(options.id)
local id = SU.required(options, "id", "increment-counter")

local counter = self.class:getCounter(id)
if (options["set-to"]) then
counter.value = tonumber(options["set-to"])
SU.deprecated("\\increment-counter[set-to=...]", '\\set-counter[value=...]', "0.14.4", "0.16.0")
-- An increment command that does a set is plain weird...
counter.value = SU.cast("integer", options["set-to"])
else
counter.value = counter.value + 1
end
if options.display then counter.display = options.display end
end, "Increments the counter named by the <id> option")

self:registerCommand("set-counter", function (options, _)
local counter = self.class:getCounter(options.id)
if options.value then counter.value = tonumber(options.value) end
local id = SU.required(options, "id", "set-counter")

local counter = self.class:getCounter(id)
if options.value then counter.value = SU.cast("integer", options.value) end
if options.display then counter.display = options.display end
end, "Sets the counter named by the <id> option to <value>; sets its display type (roman/Roman/arabic) to type <display>.")


self:registerCommand("show-counter", function (options, _)
local counter = self.class:getCounter(options.id)
if options.display then counter.display = options.display end
SILE.typesetter:setpar(self:formatCounter(counter))
local id = SU.required(options, "id", "show-counter")

local counter = self.class:getCounter(id)
if options.display then
SU.deprecated("\\show-counter[display=...]", '\\set-counter[display=...]', "0.14.4", "0.16.0")
counter.display = options.display
end
SILE.typesetter:typeset(self:formatCounter(counter))
end, "Outputs the value of counter <id>, optionally displaying it with the <display> format.")

self:registerCommand("increment-multilevel-counter", function (options, _)
local counter = self.class:getMultilevelCounter(options.id)
local id = SU.required(options, "id", "increment-multilevel-counter")

local counter = self.class:getMultilevelCounter(id)
local currentLevel = #counter.value
local level = tonumber(options.level) or currentLevel
local level = SU.cast("integer", options.level or currentLevel)
local reset = SU.boolean(options.reset, true)
-- Option reset=false is undocumented and was previously somewhat broken.
-- It should perhaps be deprecated: is there a real use case for it?
if level == currentLevel then
counter.value[level] = counter.value[level] + 1
elseif level > currentLevel then
while level > currentLevel do
while level - 1 > currentLevel do
currentLevel = currentLevel + 1
counter.value[currentLevel] = (options.reset == false) and counter.value[currentLevel -1 ] or 1
counter.value[currentLevel] = 0
counter.display[currentLevel] = counter.display[currentLevel - 1]
end
currentLevel = currentLevel + 1
counter.value[level] = 1
counter.display[level] = counter.display[currentLevel - 1]
else -- level < currentLevel
counter.value[level] = counter.value[level] + 1
while currentLevel > level do
if not (options.reset == false) then counter.value[currentLevel] = nil end
counter.display[currentLevel] = nil
if reset then
counter.value[currentLevel] = nil
counter.display[currentLevel] = nil
end
currentLevel = currentLevel - 1
end
end
if options.display then counter.display[currentLevel] = options.display end
end)
end, "Increments the value of the multilevel counter <id> at the given <level> or the current level.")

self:registerCommand("set-multilevel-counter", function (options, _)
local level = SU.cast("integer", SU.required(options, "level", "set-multilevel-counter"))
local id = SU.required(options, "id", "set-multilevel-counter")

local counter = self.class:getMultilevelCounter(id)
local currentLevel = #counter.value
if options.value then
local value = SU.cast("integer", options.value)
if level == currentLevel then
-- e.g. set to x the level 3 of 1.2.3 => 1.2.x
counter.value[level] = value
elseif level > currentLevel then
-- Fill all missing levels in-between, assuming same display format.
-- e.g. set to x the level 3 of 1 => 1.0.x
while level - 1 > currentLevel do
currentLevel = currentLevel + 1
counter.value[currentLevel] = 0
counter.display[currentLevel] = counter.display[currentLevel - 1]
end
currentLevel = currentLevel + 1
counter.value[level] = value
counter.display[level] = counter.display[currentLevel - 1]
else -- level < currentLevel
-- Reset all upper levels
-- e.g. set to x the level 2 of 1.2.3 => 1.x
counter.value[level] = value
while currentLevel > level do
counter.value[currentLevel] = nil
counter.display[currentLevel] = nil
currentLevel = currentLevel - 1
end
end
end
if options.display then
if level <= #counter.value then
counter.display[level] = options.display
else
SU.warn("Ignoring attempt to set the display of a multilevel counter beyond its level")
end
end
end, "Sets the multilevel counter named by the <id> option to <value> at level <level>; optionally sets its display type at that level to <display>.")

self:registerCommand("show-multilevel-counter", function (options, _)
local counter = self.class:getMultilevelCounter(options.id)
if options.display then counter.display[#counter.value] = options.display end
local id = SU.required(options, "id", "show-multilevel-counter")

local counter = self.class:getMultilevelCounter(id)
if options.display then
SU.deprecated("\\show-multilevel-counter[display=...]", '\\set-multilevel-counter[display=...]', "0.14.4", "0.16.0")
counter.display[#counter.value] = options.display
end

SILE.typesetter:typeset(self:formatMultilevelCounter(counter, options))
end, "Outputs the value of the multilevel counter <id>, optionally displaying it with the <display> format.")
end, "Outputs the value of the multilevel counter <id>.")

end

Expand All @@ -127,12 +206,11 @@ The counters package allows you to set up, increment and typeset named counters.
It provides the following commands:

\begin{itemize}
\item{\autodoc:command{\set-counter[id=<counter-name>, value=<value>]} — sets the counter with the specified name to the given value.}
\item{\autodoc:command{\increment-counter[id=<counter-name>]} — does the same as \autodoc:command{\set-counter} except that when no \autodoc:parameter{value} parameter is given, the counter is incremented by one.}
\item{\autodoc:command{\show-counter[id=<counter-name>]} — this typesets the value of the counter according to the counter’s declared display type.}
\item{\autodoc:command{\set-counter[id=<counter-name>, value=<value>]} — sets the counter with the specified name to the given value. The command takes an optional \autodoc:parameter{display=<display-type>} parameter to set the display type of the counter (see below).}
\item{\autodoc:command{\increment-counter[id=<counter-name>]} — increments the counter by one. The command creates the counter if it does not exist and also accepts setting the display type.}
\item{\autodoc:command{\show-counter[id=<counter-name>]} — typesets the value of the counter according to the counter’s declared display type.}
\end{itemize}

All of the commands in the counters package take an optional \autodoc:parameter{display=<display-type>} parameter to set the \em{display type} of the counter.

The available built-in display types are:

Expand All @@ -155,8 +233,8 @@ So, for example, the following SILE code:
\\set-counter[id=mycounter, value=2]
\\show-counter[id=mycounter]

\\increment-counter[id=mycounter]
\\show-counter[id=mycounter, display=roman]
\\increment-counter[id=mycounter, display=roman]
\\show-counter[id=mycounter]
\line
\end{verbatim}

Expand All @@ -167,6 +245,27 @@ produces:

\noindent{}iii}
\line

The package also provides multi-level (hierarchical) counters, of the kind used in sectioning
commands:

\begin{itemize}
\item{\autodoc:command{\set-multilevel-counter[id=<counter-name>, level=<level>, value=<value>]}
— sets the multi-level counter with the specified name to the given value at the given level.
The command also takes an optional \autodoc:parameter{display=<display-type>}, also acting at the given level.}
\item{\autodoc:command{\increment-multilevel-counter[id=<counter-name>]}
— increments the counter by one at its current (deepest) level.
The command creates the counter if it does not exist.
If given the \autodoc:parameter{level=<level>} parameter, the command increments that level,
clearing any lower level (and filling previous levels with zeros, if they weren’t proprely set).
It also accepts setting the display type at the target level.}
\item{\autodoc:command{\show-multilevel-counter[id=<counter-name>]}
— typesets the value of the multi-level counter according to the counter’s declared display types
at each level. By default, all levels are output; option \autodoc:parameter{level=<level>} may be
used to display the counter up to a given level. Option \autodoc:parameter{noleadingzeros=true}
skips any leading zero (which may happen if a counter is at some level, without previous levels
having been set).}
\end{itemize}
\end{document}
]]

Expand Down
6 changes: 3 additions & 3 deletions tests/bug-522.expected
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ My 75.0822
Set font Gentium Plus;10;400;;normal;;LTR
T 19 w=4.6924 (0)
My 87.0822
T 19 17 20 17 20 w=18.6572 (0.1.1)
T 19 17 19 17 20 w=18.6572 (0.0.1)
My 99.0822
T 19 17 20 17 21 w=18.6572 (0.1.2)
T 19 17 19 17 21 w=18.6572 (0.0.2)
My 111.0822
T 19 17 21 w=11.6748 (0.2)
T 19 17 20 w=11.6748 (0.1)
Mx 195.4611
My 519.8019
T 20 w=4.6924 (1)
Expand Down
10 changes: 5 additions & 5 deletions tests/mlcounter.expected
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ Mx 48.3487
T 36 68 w=10.5420 (Aa)
Mx 40.9764
My 48.1944
T 20 17 20 17 20 17 20 w=25.6396 (1.1.1.1)
T 20 17 19 17 19 17 20 w=25.6396 (1.0.0.1)
Mx 69.2937
T 37 69 w=10.5762 (Bb)
Mx 40.9764
My 60.1944
T 20 17 20 17 20 17 21 w=25.6396 (1.1.1.2)
T 20 17 19 17 19 17 21 w=25.6396 (1.0.0.2)
Mx 69.2938
T 38 70 w=9.7314 (Cc)
Mx 40.9764
My 72.1944
T 20 17 20 17 20 17 22 w=25.6396 (1.1.1.3)
T 20 17 19 17 19 17 22 w=25.6396 (1.0.0.3)
Mx 69.2936
T 39 71 w=11.1279 (Dd)
Mx 40.9764
My 84.1944
T 20 17 21 w=11.6748 (1.2)
T 20 17 20 w=11.6748 (1.1)
Mx 55.3305
T 40 72 w=9.5801 (Ee)
Mx 40.9764
My 96.1944
T 20 17 21 17 20 17 20 17 20 w=32.6221 (1.2.1.1.1)
T 20 17 20 17 19 17 19 17 20 w=32.6221 (1.1.0.0.1)
Mx 76.2756
T 41 73 w=7.8906 (Ff)
Mx 207.4176
Expand Down