-- florilegium.lua -- the TeX-facing side of florilegium.sty: page geometry,
-- colors, seeds, the TikZ transform, and the dispatch of primitives.
-- Copyright (c) 2026 Kevin Raymond. MIT License: see LICENSE or florilegium.sty.

local ink = require("florilegium-ink")
local flora = require("florilegium-flora")
local M = { ink = ink, flora = flora }

-- The release this file belongs to. Every florilegium-*.lua says the same in its
-- M.version, and florilegium.sty says v<version> in \ProvidesPackage. TeX looks each
-- file up on its own, so files from two releases can meet (a set installed in ~/texmf,
-- a newer florilegium.sty and florilegium.lua beside the document); check_versions
-- stops such a mix before anything is drawn. A release changes all of them together.
M.version = "2.0.1"
local PARTS = { "ink", "flora", "grow", "authored", "figures", "theme-geometric", "theme-math", "initial", "svg", "species",
  "pgfmath", "native" }

local function where(file, format, module)     -- " (path)" for the message, if it can be found
  local ok, path = pcall(function() return kpse.find_file(file, format) end)
  if not (ok and path) and module and package.searchpath then path = package.searchpath(module, package.path) end
  return path and (" (" .. path .. ")") or ""
end
local function mixed(file, v)
  error(string.format("florilegium: %s is from %s, florilegium.lua from release %s. Copy florilegium.sty "
    .. "and every florilegium*.lua from one release.", file, v and ("release " .. v) or "an earlier release", M.version), 0)
end
-- every part of the package that is loaded, and inside LuaLaTeX florilegium.sty
function M.check_versions()
  for _, part in ipairs(PARTS) do
    local name = "florilegium-" .. part
    local m = package.loaded[name]
    if type(m) == "table" and m.version ~= M.version then mixed(name .. ".lua" .. where(name .. ".lua", "lua", name), m.version) end
  end
  -- the pure-TikZ library, if the document loaded it (\usetikzlibrary{florilegium})
  local lib = token and token.get_macro and token.get_macro("ver@tikzlibraryflorilegium.code.tex")
  if lib and lib:match("v(%d[%d.]*)") ~= M.version then
    mixed("tikzlibraryflorilegium.code.tex" .. where("tikzlibraryflorilegium.code.tex", "tex"), lib:match("v(%d[%d.]*)"))
  end
  local sty = token and token.get_macro and token.get_macro("ver@florilegium.sty")
  if sty then
    local v = sty:match("v(%d[%d.]*)")
    if v ~= M.version then mixed("florilegium.sty" .. where("florilegium.sty", "tex"), v) end
  end
end

local SP_CM = 2.54 / (72.27 * 65536)          -- cm per scaled point
local function cm(name) return tex.dimen[name] * SP_CM end

-- the page layout, in cm from the lower-left corner of the paper. The garden
-- grows in the wider side margin: when that is the right one (the outer margin
-- of a right-hand page in a two-sided document), the layout is made for the
-- mirrored page and drawn reflected (g.mirror; ink.page_transform).
M.twoside = false                             -- \if@twoside, set at each shipout
function M.geometry()
  local inch = 2.54
  local even = M.twoside and tex.count[0] % 2 == 0
  local g = {
    pw = cm("paperwidth"), ph = cm("paperheight"),
    textleft = inch + tex.hoffset * SP_CM + cm(even and "evensidemargin" or "oddsidemargin"),
  }
  g.textright = g.textleft + cm("textwidth")
  if g.pw - g.textright > g.textleft + 0.05 then
    g.mirror = true
    g.textleft, g.textright = g.pw - g.textright, g.pw - g.textleft
  end
  g.ytop = g.ph - inch - tex.voffset * SP_CM - cm("topmargin") - cm("headheight") - cm("headsep")
  g.ybot = g.ytop - cm("textheight")
  g.colright = g.textleft - 0.4
  g.headrule = g.ytop + cm("headsep") - 0.1
  g.footy = g.ybot - cm("footskip")
  return g
end

-- colors: `ink` sets the hue of the whole ramp, `accent` the copper
local current = { 0x24 / 255, 0x56 / 255, 0xC4 / 255, 0.71, 0.40, 0.18 }
function M.colors(ir, ig, ib, ar, ag, ab)
  current = { ir, ig, ib, ar, ag, ab }
  ink.set_ink(ir, ig, ib)
  ink.set_accent(ar, ag, ab)
end

-- a number the user wrote (density=, intensity=, an ornament's angle=, \florilegiumseed,
-- \florilegiumage): its value, or, when it is not a number, the default, with a warning
-- in the log (once for each text) that says what was used instead (`instead`, or the default)
local warned = {}
local function warn(msg)
  if not warned[msg] and luatexbase and luatexbase.module_warning then
    warned[msg] = true
    luatexbase.module_warning("florilegium", msg)
  end
end
function M.number(key, text, default, instead)
  local v = tonumber(text)
  if v and v - v == 0 then return v end         -- a finite number (inf - inf and nan - nan are nan)
  warn(string.format(key:sub(1, 1) == "\\" and "%s{%s} is not a number; using %s" or "%s=%s is not a number; using %s",
    key, text, instead or tostring(default)))
  return default
end
-- a seed as ink's generator takes it: a whole number of any size (its arithmetic wraps), or a
-- decimal for which seed * k (the largest factor it meets) is still one; a larger decimal
-- (1e20, 1e999) is no seed, nil with a warning, where it stopped the drawing with "number has
-- no integer representation"
local function seedvalue(v, k, key, instead)
  if math.type(v) == "float" and math.type(math.floor(v * k + 401)) ~= "integer" then
    warn(string.format("%s %g is too large for a seed; using %s", key, v, instead))
    return nil
  end
  return v
end

-- one rng per page; primitives drawn from TeX fork it in call order
local page_rng = ink.rng(1)
M.docseed = 0                                 -- \florilegiumseed, or 0: the species pool
function M.seed(page, fixed)
  fixed = fixed and seedvalue(fixed, 104729, "\\florilegiumseed", "the page's own seed")
  M.docseed = fixed or 0
  local s = fixed and (fixed * 7919 + page * 131) or (page * 97 + 1030)
  page_rng = ink.rng(s)
  return s
end

-- a drawing that fails (a helper given as a primitive, a number that is not one, a theme
-- that cannot be loaded) leaves ink as it found it, so that the next ornament or page draws
-- as always: what it drew is dropped, its ghost lift and pale are put back, and the error
-- goes to TeX as one message, without a Lua traceback
local function guarded(fn)
  return function(...)
    local lift, pale = ink.get_lift(), ink.set_pale(0)
    ink.set_pale(pale)
    local ok, err = pcall(fn, ...)
    if not ok then
      ink.take_recording() ink.drop() ink.set_lift(lift) ink.set_pale(pale)
      err = tostring(err)
      tex.error(err:find("^florilegium") and err or "florilegium: " .. err)
    end
  end
end

-- the current pgf transformation (TikZ scopes) as an ink transform: local cm -> page bp
local function sync_transform()
  local C = ink.CM
  local bp = 72 / 72.27 / 65536
  ink.set_transform({
    tonumber(token.get_macro("pgf@pt@aa")) * C, tonumber(token.get_macro("pgf@pt@ab")) * C,
    tonumber(token.get_macro("pgf@pt@ba")) * C, tonumber(token.get_macro("pgf@pt@bb")) * C,
    tex.dimen["pgf@pt@x"] * bp, tex.dimen["pgf@pt@y"] * bp })
end

-- \florilegiumdraw{name}{s, opts}: one primitive at the origin of the current TikZ
-- scope; the names of the current theme's primitives work too
local scaled = { leaf = true, leaflet = true, petal = true }
-- the theme in use: M.theme, unless it cannot be loaded, which is reported once (with the
-- first ornament or page after it was set) and dropped, so that the garden grows without it
local function usable_theme()
  if M.theme == "" or M.theme == "flora" then M.theme = nil end
  if M.theme then
    local ok, err = pcall(require("florilegium-grow").theme, M.theme)
    if not ok then
      M.theme = nil
      tex.error(tostring(err), { "The margin grows without a theme until another one is set." })
    end
  end
  return M.theme
end
local function primitives() return require("florilegium-grow").theme(usable_theme()) end
function M.draw(name, s, o)
  local F = primitives()
  local fn = F[name]
  if type(fn) ~= "function" then tex.error("florilegium: unknown primitive '" .. tostring(name) .. "'") return end
  sync_transform()
  local rng = page_rng.fork()
  if name == "ghost" then
    F.ghost(o and o.lift, function() M.draw_inner(s, o, rng) end)
  elseif scaled[name] then
    ink.scale(s or 1) fn(rng, o)
  else
    fn(rng, s, o)
  end
  ink.flush()
end
-- \florilegiumdraw{ghost}{"flower", {s=1.4}} draws any primitive pale
function M.draw_inner(name, o, rng)
  o = o or {}
  local F = primitives()
  local fn = F[name]
  if type(fn) ~= "function" then error("florilegium: unknown primitive '" .. tostring(name) .. "'", 0) end
  if scaled[name] then ink.scale(o.s or 1) fn(rng, o) else fn(rng, o.s or 1, o) end
end
M.draw = guarded(M.draw)

-- ---------------------------------------------------------------- ornaments in the text
-- \florilegiumornament[size, ink, accent, angle, seed]{name}: one primitive in a box
-- of side `size` (sp) on the baseline, like a letter (its depth 0.15 of the size).
-- Where each primitive sits in that box: its origin (x, y as fractions of the
-- side), its scale, its angle, and options for it.
local ORNAMENT = {
  leaf = { 0.2, 0.1, 1.02, -40 }, leaflet = { 0.2, 0.1, 1.0, -40 }, petal = { 0.3, 0.02, 1.0, -30 },
  bud = { 0.35, 0.0, 0.95, -20 }, bell = { 0.5, 0.97, 0.95, 0 }, sprig = { 0.4, 0.0, 1.0, -10 },
  cyme = { 0.35, 0.0, 0.95, -10, { side = -1 } }, umbel = { 0.5, 0.05, 0.8, 0 }, tendril = { 0.95, 0.5, 0.45, 0 },
  flower = { 0.5, 0.5, 0.47, 0 }, blossom = { 0.5, 0.5, 0.48, 0 }, floret = { 0.5, 0.5, 0.46, 0 },
  lily = { 0.5, 0.45, 0.6, 0 }, seedhead = { 0.5, 0.42, 0.42, 0, { drift = 0 } },
  butterfly = { 0.5, 0.5, 0.98, 0 }, bee = { 0.5, 0.5, 0.8, -30, { trail = false } },
  snail = { 0.5, 0.35, 0.55, -90 }, bird = { 0.5, 0.02, 0.95, 0 },
  sparkle = { 0.5, 0.5, 0.3, 0 }, rosette = { 0.5, 0.5, 0.38, 0 }, hedera = { 0.45, 0.62, 0.36, 0 },
}
local function hex(h) return tonumber(h:sub(1, 2), 16) / 255, tonumber(h:sub(3, 4), 16) / 255, tonumber(h:sub(5, 6), 16) / 255 end
-- an ornament's ink= or accent=: six hex digits, or "" for the current color; anything
-- else is an error, and the ornament keeps the current color
local function hexkey(key, h)
  if h == "" or h:match("^%x%x%x%x%x%x$") then return h end
  tex.error("florilegium: " .. key .. "=" .. h .. " is not six hex digits, like 5B2A1F",
    { "The ornament is drawn in the current " .. key .. " color." })
  return ""
end
local nornament = 0
local HELPER = { ghost = true, branch_path = true, mottle = true, streak = true, streak3 = true, walk = true, stem = true, motifs = true }
function M.ornament(name, size, inkhex, acchex, angle, seed)
  local F = primitives()
  local fn = F[name]
  if type(fn) ~= "function" or HELPER[name] then tex.error("florilegium: unknown primitive '" .. tostring(name) .. "'") return end
  nornament = nornament + 1
  inkhex, acchex = hexkey("ink", inkhex), hexkey("accent", acchex)
  -- the k-th ornament of the document always gets the same shape, unless a seed
  -- is given; seed=random gives a new one every time the document is built
  local s
  if seed == "random" then s = (os.time() % 999983) * 7919 + math.floor(os.clock() * 1e6) + nornament * 131
  else
    local n = seed ~= "" and seedvalue(M.number("seed", seed, nil, "the ornament's own"), 7919, "seed", "the ornament's own")
    s = n and n * 7919 + 17 or nornament * 7919 + 4243
  end
  if inkhex ~= "" or acchex ~= "" then
    local c = current
    M.colors(inkhex ~= "" and hex(inkhex) or c[1], inkhex ~= "" and select(2, hex(inkhex)) or c[2], inkhex ~= "" and select(3, hex(inkhex)) or c[3],
      acchex ~= "" and hex(acchex) or c[4], acchex ~= "" and select(2, hex(acchex)) or c[5], acchex ~= "" and select(3, hex(acchex)) or c[6])
    current = c
  end
  local S = size * SP_CM
  local p = ORNAMENT[name] or { 0.5, 0.5, 0.46, 0 }
  ink.set_transform({ ink.CM, 0, 0, ink.CM, 0, 0 })
  ink.translate(S * p[1], S * (p[2] - 0.15))
  ink.rotate(p[4] + (angle or 0))
  if scaled[name] then ink.scale(S * p[3]) fn(ink.rng(s), p[5] or {})
  else fn(ink.rng(s), S * p[3], p[5] or {}) end
  ink.flush()
  if inkhex ~= "" or acchex ~= "" then M.colors(table.unpack(current)) end
end
M.ornament = guarded(M.ornament)

-- the header: a copper ivy leaf (or the theme's fleuron) whose stem becomes the head rule
function M.head(g)
  ink.set_transform(ink.page_transform(g.mirror and g.pw))
  ink.push()
  ink.translate(g.colright - 0.55, g.headrule + 0.02)
  local x, y = primitives().hedera(page_rng.fork(), 0.36)
  if g.mirror then x = g.pw - x end           -- hedera answers on the page, not in the mirrored layout
  ink.pop()
  local e = g.textright
  ink.moveto(x, y) ink.curveto(x + 1.0, y, x + 1.5, g.headrule, x + 2.5, g.headrule) ink.lineto(e, g.headrule)
  ink.stroke(ink.accent(0), 0.4)
  ink.circle(e, g.headrule, 0.03) ink.fill(ink.accent(0))
  ink.flush()
end

-- ---------------------------------------------------------------- marks in the text
-- \florilegiumbloom[motif] and (option sections) the section headings leave a
-- whatsit in the text. The page box is searched for them at shipout, when the
-- garden is drawn, so the garden knows the height of those lines on the same
-- page in the same run.
local MARK = 0x464C4F                         -- "FLO"
local marks = {}
function M.mark(kind, arg)
  marks[#marks + 1] = { kind = kind, arg = arg }
  local n = node.new("whatsit", "user_defined")
  n.user_id, n.type, n.value = MARK, 100, #marks
  node.write(n)
end

local HLIST, VLIST, GLUE, KERN, RULE, WHATSIT = node.id("hlist"), node.id("vlist"), node.id("glue"), node.id("kern"), node.id("rule"), node.id("whatsit")
local USER = node.subtype("user_defined")
-- walk a box list; y grows downward from the top of the outer box, x rightward
local function walk(head, parent, x, y, vertical, out)
  for n in node.traverse(head) do
    local id = n.id
    if id == HLIST or id == VLIST then
      if vertical then
        local base = y + n.height
        if n.list then walk(n.list, n, x + n.shift, id == VLIST and y or base, id == VLIST, out) end
        y = base + n.depth
      else
        local base = y + n.shift
        if n.list then walk(n.list, n, x, id == VLIST and base - n.height or base, id == VLIST, out) end
        x = x + n.width
      end
    elseif id == GLUE then
      local w = node.effective_glue(n, parent)
      if vertical then y = y + w else x = x + w end
    elseif id == KERN then
      if vertical then y = y + n.kern else x = x + n.kern end
    elseif id == RULE then
      if vertical then y = y + n.height + n.depth else x = x + n.width end
    elseif id == WHATSIT and n.subtype == USER and n.user_id == MARK then
      local m = marks[n.value]
      if m then out[#out + 1] = { kind = m.kind, arg = m.arg, x = x, y = y } end
    elseif not vertical then
      x = x + node.dimensions(n, n.next)
    end
  end
end
-- the marks on the page being shipped out, in page cm from the lower-left corner
local function page_marks(box)
  local b = box and tex.box[box]
  if not b then return {} end
  local out = {}
  walk(b.list, b, 0, 0, b.id == VLIST, out)
  local ph, inch = tex.dimen.paperheight * SP_CM, 2.54
  for _, m in ipairs(out) do
    m.x = inch + tex.hoffset * SP_CM + m.x * SP_CM
    m.y = ph - inch - tex.voffset * SP_CM - m.y * SP_CM
  end
  return out
end

-- ---------------------------------------------------------------- initials
-- \florilegiuminitial: a tile of side `side` (sp) whose top is the cap height
-- above the current baseline and whose left edge is `hang` (sp) to the left of
-- the current point, the start of the paragraph's first, indented line.
M.initial_font = "lmroman10-bold.otf"
function M.initial(letter, side, hang, cap)
  local bp = 72 / 72.27 / 65536
  ink.set_transform({ ink.CM, 0, 0, ink.CM, -hang * bp, (cap - side) * bp })
  local initial = require("florilegium-initial")
  M.check_versions()                          -- loaded with the first initial, so checked here
  local ok, err = initial.draw(page_rng.fork(), side * SP_CM, letter, { font = M.initial_font })
  if not ok then tex.error("florilegium: " .. tostring(err)) end
  ink.flush()
end
M.initial = guarded(M.initial)
-- the letter is also typeset in the text, in PDF's invisible text rendering
-- mode, so that the paragraph still copies and searches as "This", not "his"
function M.hidden(on)
  local n = node.new("whatsit", "pdf_literal")
  n.mode = 1                                  -- page level, outside the text object
  n.data = on and "3 Tr" or "0 Tr"
  node.write(n)
end

-- ---------------------------------------------------------------- aging
-- a garden's age: 0 spring (buds), 0.5 summer, 1 autumn (seed heads, falling
-- petals); nil leaves it as it always was. \florilegiumage sets it; the option
-- aging spreads it over the document, from the page count of the last run.
M.age = nil
function M.auto_age(page, last) M.age = last > 1 and math.min(1, (page - 1) / (last - 1)) or 0.5 end

-- the growth animation: after \florilegiumframe[explain]{k}{n}, a grown page
-- shows frame k of n of its garden's growth (animation.tex, tools/animate.py).
-- The garden is recorded once and each frame writes the part grown so far.
local frame, recorded = nil, {}
function M.frame(k, n, explain) frame = { k = k, n = n, explain = explain == "explain" } end

-- text a theme sets in the margin (captions, glyphs, equations): TeX nodes in the
-- margin picture at {x, y} in layout cm, by their anchor (default north: the top
-- of the text centered there). Small ones in 5.5 pt, others in 6.5 pt or, with
-- `scale`, 10 pt scaled; `rotate` (degrees), `color` ({r, g, b}), `opacity`. On a
-- mirrored page east and west trade places and rotations turn the other way.
local function captions(g, list, shown)
  if not list or #list == 0 then return end
  local c = ink.tone(0.12)
  local out = { string.format("\\definecolor{flocaption}{rgb}{%.3f,%.3f,%.3f}", c[1], c[2], c[3]) }
  for _, cap in ipairs(list) do
    if not shown or shown(cap) then
      local x = g.mirror and g.pw - cap.x or cap.x
      local anchor = cap.anchor or "north"
      if g.mirror then anchor = anchor:gsub("east", "\0"):gsub("west", "east"):gsub("\0", "west") end
      local opts = { "anchor=" .. anchor, "inner sep=0pt", "align=center" }
      if cap.color then
        opts[#opts + 1] = string.format("text={rgb,255:red,%d;green,%d;blue,%d}", math.floor(cap.color[1] * 255 + 0.5),
          math.floor(cap.color[2] * 255 + 0.5), math.floor(cap.color[3] * 255 + 0.5))
      else opts[#opts + 1] = "text=flocaption" end
      if cap.scale then
        opts[#opts + 1] = string.format("font=\\fontsize{10}{12}\\selectfont,scale=%.3f,transform shape", cap.scale)
      else
        opts[#opts + 1] = "font=\\fontsize{" .. (cap.small and "5.5}{6.5" or "6.5}{7.5") .. "}\\selectfont"
      end
      if cap.rotate and cap.rotate ~= 0 then opts[#opts + 1] = string.format("rotate=%.1f", g.mirror and -cap.rotate or cap.rotate) end
      if cap.opacity then opts[#opts + 1] = string.format("text opacity=%.2f", cap.opacity) end
      out[#out + 1] = string.format("\\node[%s] at (%.2f,%.2f) {%s};", table.concat(opts, ","), x, cap.y, cap.text)
    end
  end
  tex.sprint(table.concat(out))
end

-- the captions of the last page drawn, in layout cm, for florilegium-svg.lua, which has no TeX to set them,
-- and what grew on it (florilegium-grow.lua's counts)
M.last_captions, M.last_stats = nil, nil

local function grown_frame(g)
  local key = string.format("%d %d %.3f %s %d %s %s %s %s", g.seed, g.spill, g.spilly, tostring(g.age), #g.marks, tostring(g.theme),
    tostring(g.mirror), tostring(g.density), tostring(g.docseed))
  if not recorded[key] then
    g.anim = true
    recorded = { [key] = require("florilegium-grow").page(g) }      -- keep only the latest garden
  end
  local r = recorded[key]
  -- the last frame is the page: every event finished (r.total itself may round below)
  local t = frame.k >= frame.n and math.huge or r.total * (frame.k - 1) / math.max(1, frame.n - 1)
  ink.emit(r.rec, function(tag)
    if tag.overlay and not frame.explain then return false end
    local e = tag[1]
    local p = (t - e.t0) / e.dur
    return p >= tag[2] and (tag[3] == nil or p < tag[3])
  end)
  captions(g, r.captions, function(cap) return t >= cap.event.t0 + cap.event.dur end)
end

-- the theme (option theme=...): nil is flora; otherwise florilegium-theme-<name>.lua replaces
-- some of the primitives (florilegium-grow.lua). In the document, \florilegiumtheme leaves a
-- mark, so the theme changes from the page that the mark is on.
M.theme = nil
local function themes(list)
  if M.theme == "" then M.theme = nil end   -- \florilegiumtheme{} in the preamble, as in the text
  local rest = {}
  for _, m in ipairs(list) do
    if m.kind == "theme" then M.theme = m.arg ~= "" and m.arg or nil else rest[#rest + 1] = m end
  end
  return rest
end

-- how much is in the margin (options density and intensity, 0..1): density thins
-- the grown garden out, intensity < 1 paints the whole margin paler, as the
-- ghost layer is painted (1 - intensity of the way to the paper, at most 85%)
M.density, M.intensity = 1, 1
local function pale() return 0.85 * (1 - math.max(0, math.min(1, M.intensity))) end

-- the grown margin's plants are species (florilegium-species.lua), drawn by
-- florilegium-native.lua: loaded, and its version checked, with the first grown page
local organs = false
local function use_organs()
  if organs then return end
  require("florilegium-native")
  M.check_versions()
  organs = true
end

-- the grown composition for this page; M.visitors = false (option novisitors)
-- leaves out the visitors. M.bleed (cm, florilegium-svg.lua's --bleed) lets the safety
-- clip reach past the paper's edge; LuaLaTeX never sets it
M.visitors = true
M.bleed = 0
function M.grown(seed, spill, spilly, box)
  local g = M.geometry()
  g.marks = themes(page_marks(box))
  g.seed, g.spill, g.spilly, g.visitors, g.age, g.theme = seed, spill, spilly, M.visitors, M.age, usable_theme()
  g.docseed = M.docseed
  if M.bleed > 0 then g.bleed = M.bleed end
  if not M.theme or M.theme == "flora" then use_organs() end
  if M.density < 1 then g.density = math.max(0.03, M.density) end
  local old = ink.set_pale(pale())
  if frame then grown_frame(g)
  else
    local r = require("florilegium-grow").page(g)
    M.last_captions, M.last_stats = r and r.captions, r and r.stats
    captions(g, r and r.captions)
  end
  M.head(g)
  ink.set_pale(old)
end
M.grown = guarded(M.grown)

-- the authored composition for this page (intensity applies; density does not,
-- everything here is placed by hand)
function M.authored(seed, box)
  themes(page_marks(box))
  local g = M.geometry()
  g.seed, g.visitors, g.theme = seed, M.visitors, usable_theme()
  if M.bleed > 0 then g.bleed = M.bleed end
  local old = ink.set_pale(pale())
  local r = require("florilegium-authored").page(g)
  M.last_captions, M.last_stats = r and r.captions, r and r.stats
  captions(g, r and r.captions)
  M.head(g)
  ink.set_pale(old)
end
M.authored = guarded(M.authored)

-- load the rest of the package now, so that a mixed set stops in the preamble and not
-- halfway through a page (florilegium-initial waits for the first initial; florilegium-svg
-- runs only outside TeX, where it loads this file and so is checked here too)
for _, part in ipairs({ "grow", "authored", "figures", "theme-geometric", "theme-math" }) do
  require("florilegium-" .. part)
end
M.check_versions()

return M
