mirror of
https://github.com/JustAnyones/ktu-paper.git
synced 2026-11-04 00:30:18 +00:00
feat: add automatic management of figure breakability
This commit is contained in:
2 files changed
+115
-6
No files matched your search
+91
-6
@@ -374,6 +374,65 @@
|
|||||||
// Figure layout
|
// Figure layout
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Default layout per figure kind (keyed like `__figure-kinds`; custom kinds
|
||||||
|
/// and unlisted ones use `__FALLBACK_FIGURE_LAYOUT`).
|
||||||
|
/// - breakable: whether the body may be split across pages. The caption is then
|
||||||
|
/// attached to the start of the body (sticky), so it is never left alone at
|
||||||
|
/// the bottom of a page. If `false`, body and caption form one unbreakable
|
||||||
|
/// block that moves to the next page as a whole.
|
||||||
|
/// - caption: `top` or `bottom`.
|
||||||
|
#let __DEFAULT_FIGURE_LAYOUT = (
|
||||||
|
table: (breakable: true, caption: top),
|
||||||
|
raw: (breakable: true, caption: bottom),
|
||||||
|
)
|
||||||
|
#let __FALLBACK_FIGURE_LAYOUT = (breakable: false, caption: bottom)
|
||||||
|
|
||||||
|
/// Author overrides, set via `ktu-paper(figureLayout: ...)`.
|
||||||
|
#let __USER_FIGURE_LAYOUT = state("ktu-user-figure-layout", (:))
|
||||||
|
|
||||||
|
/// Stack of local overrides set by `breakable-figures` / `unbreakable-figures`.
|
||||||
|
#let __FIGURE_BREAK_OVERRIDE = state("ktu-figure-break-override", ())
|
||||||
|
|
||||||
|
/// Validates author-supplied layouts: `(kind: (breakable: bool, caption: top | bottom))`,
|
||||||
|
/// every field optional.
|
||||||
|
#let __normalize-figure-layout(layouts) = {
|
||||||
|
for (key, spec) in layouts {
|
||||||
|
for (field, value) in spec {
|
||||||
|
assert(
|
||||||
|
field in ("breakable", "caption"),
|
||||||
|
message: "figureLayout." + key + ": unknown option `" + field + "`, expected `breakable` or `caption`",
|
||||||
|
)
|
||||||
|
if field == "breakable" {
|
||||||
|
assert(type(value) == bool, message: "figureLayout." + key + ".breakable must be a bool")
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
value == top or value == bottom,
|
||||||
|
message: "figureLayout." + key + ".caption must be `top` or `bottom`",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layouts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the layout of a figure kind key. Must be called in a context.
|
||||||
|
#let __figure-layout(key) = {
|
||||||
|
let layout = __FALLBACK_FIGURE_LAYOUT + __DEFAULT_FIGURE_LAYOUT.at(key, default: (:))
|
||||||
|
layout = layout + __USER_FIGURE_LAYOUT.get().at(key, default: (:))
|
||||||
|
let overrides = __FIGURE_BREAK_OVERRIDE.get()
|
||||||
|
if overrides.len() > 0 {
|
||||||
|
layout.breakable = overrides.last()
|
||||||
|
}
|
||||||
|
layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs `body` with every figure's `breakable` setting forced to `mode`.
|
||||||
|
#let __with-break-override(mode, body) = {
|
||||||
|
__FIGURE_BREAK_OVERRIDE.update(stack => stack + (mode,))
|
||||||
|
body
|
||||||
|
__FIGURE_BREAK_OVERRIDE.update(stack => stack.slice(0, -1))
|
||||||
|
}
|
||||||
|
|
||||||
/// Formats a figure with a caption and number.
|
/// Formats a figure with a caption and number.
|
||||||
#let __format-figure(it) = {
|
#let __format-figure(it) = {
|
||||||
let caption = it.caption
|
let caption = it.caption
|
||||||
@@ -384,13 +443,39 @@
|
|||||||
set text(size: 11pt)
|
set text(size: 11pt)
|
||||||
|
|
||||||
let caption-line = [*#__caption-label(it)* #caption.body]
|
let caption-line = [*#__caption-label(it)* #caption.body]
|
||||||
|
let key = __figure-key(it)
|
||||||
|
|
||||||
// Tables have captions at the top
|
context {
|
||||||
if it.kind == table {
|
let layout = __figure-layout(key)
|
||||||
align(left, caption-line)
|
let on-top = layout.caption == top
|
||||||
it.body
|
// Entire table is by default left aligned, unlike other figures which are centered
|
||||||
|
let body = if key == "table" { align(left, it.body) } else { it.body }
|
||||||
|
let caption-block = block(
|
||||||
|
sticky: on-top and layout.breakable,
|
||||||
|
width: 100%,
|
||||||
|
align(if on-top { left } else { center }, caption-line),
|
||||||
|
)
|
||||||
|
if layout.breakable {
|
||||||
|
// The body may split across pages. A caption on top is sticky and stays
|
||||||
|
// with the first rows; a caption at the bottom cannot be glued to the end
|
||||||
|
// of a splittable body, so it may end up alone on the next page.
|
||||||
|
if on-top {
|
||||||
|
caption-block
|
||||||
|
body
|
||||||
} else {
|
} else {
|
||||||
it.body
|
body
|
||||||
align(center, caption-line)
|
caption-block
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
block(breakable: false, width: 100%, {
|
||||||
|
if on-top {
|
||||||
|
caption-block
|
||||||
|
body
|
||||||
|
} else {
|
||||||
|
body
|
||||||
|
caption-block
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+24
@@ -17,6 +17,9 @@
|
|||||||
__equation-numbering,
|
__equation-numbering,
|
||||||
__normalize-figure-kinds,
|
__normalize-figure-kinds,
|
||||||
__USER_FIGURE_KINDS,
|
__USER_FIGURE_KINDS,
|
||||||
|
__USER_FIGURE_LAYOUT,
|
||||||
|
__normalize-figure-layout,
|
||||||
|
__with-break-override,
|
||||||
|
|
||||||
__format-ref,
|
__format-ref,
|
||||||
__format-figure,
|
__format-figure,
|
||||||
@@ -286,6 +289,19 @@
|
|||||||
__ktu_outline(title: heading(title, outlined: true), target: figure.where(kind: kind))
|
__ktu_outline(title: heading(title, outlined: true), target: figure.where(kind: kind))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lets figures in `body` split across pages: the body may break, the caption stays
|
||||||
|
/// attached to its start (a caption at the bottom may end up alone, see `figureLayout`).
|
||||||
|
/// Use it to opt out of keeping a figure on one page, e.g. for a long code listing.
|
||||||
|
/// - body (content): Content containing figures.
|
||||||
|
/// -> content
|
||||||
|
#let breakable-figures(body) = __with-break-override(true, body)
|
||||||
|
|
||||||
|
/// Keeps every figure in `body` on one page: body and caption move to the next
|
||||||
|
/// page together. Use it to opt out of splitting, e.g. for a short table.
|
||||||
|
/// - body (content): Content containing figures.
|
||||||
|
/// -> content
|
||||||
|
#let unbreakable-figures(body) = __with-break-override(false, body)
|
||||||
|
|
||||||
/// `code` option defaults for the library entrypoint.
|
/// `code` option defaults for the library entrypoint.
|
||||||
#let __CODE_DEFAULTS = (
|
#let __CODE_DEFAULTS = (
|
||||||
font: "Hack",
|
font: "Hack",
|
||||||
@@ -516,6 +532,12 @@
|
|||||||
/// - `single-name`: full word used when the figure is the only one of its kind
|
/// - `single-name`: full word used when the figure is the only one of its kind
|
||||||
/// and is left unnumbered, e.g. "Komanda." and "(žr. komanda)". Defaults to `supplement`.
|
/// and is left unnumbered, e.g. "Komanda." and "(žr. komanda)". Defaults to `supplement`.
|
||||||
/// Currently only tables and images are left unnumbered when single, so this only matters for kinds added there.
|
/// Currently only tables and images are left unnumbered when single, so this only matters for kinds added there.
|
||||||
|
/// - figureLayout (dictionary): Page-break behaviour and caption position per figure kind,
|
||||||
|
/// `(kind: (breakable: bool, caption: top | bottom))`, every field optional. Kinds are
|
||||||
|
/// `table`, `image`, `raw` (code), or a key of `figureKinds`. Defaults: `table` and `raw`
|
||||||
|
/// are breakable; everything else is unbreakable. Example: `(raw: (breakable: false, caption: bottom))`.
|
||||||
|
/// A breakable figure's top caption stays attached to the start of the body.
|
||||||
|
/// Use `breakable-figures[...]` / `unbreakable-figures[...]` to override it locally.
|
||||||
/// - code (dictionary | none): Code block configuration, or `none` to leave raw blocks unstyled. Keys:
|
/// - code (dictionary | none): Code block configuration, or `none` to leave raw blocks unstyled. Keys:
|
||||||
/// - `font` (str): Monospace font. Defaults to "Hack".
|
/// - `font` (str): Monospace font. Defaults to "Hack".
|
||||||
/// - `spacing` (relative): Word spacing of code text. Defaults to `80%`.
|
/// - `spacing` (relative): Word spacing of code text. Defaults to `80%`.
|
||||||
@@ -529,6 +551,7 @@
|
|||||||
font: "Times New Roman",
|
font: "Times New Roman",
|
||||||
figureNumberingPerSection: false,
|
figureNumberingPerSection: false,
|
||||||
figureKinds: (:),
|
figureKinds: (:),
|
||||||
|
figureLayout: (:),
|
||||||
code: (:),
|
code: (:),
|
||||||
// Not sure about formal conformity of deviating
|
// Not sure about formal conformity of deviating
|
||||||
// from the defaults of the next options,
|
// from the defaults of the next options,
|
||||||
@@ -544,6 +567,7 @@
|
|||||||
)
|
)
|
||||||
__FIG_PER_SECTION.update(figureNumberingPerSection)
|
__FIG_PER_SECTION.update(figureNumberingPerSection)
|
||||||
__USER_FIGURE_KINDS.update(__normalize-figure-kinds(figureKinds))
|
__USER_FIGURE_KINDS.update(__normalize-figure-kinds(figureKinds))
|
||||||
|
__USER_FIGURE_LAYOUT.update(__normalize-figure-layout(figureLayout))
|
||||||
__UPPERCASE_SECTION_TITLES.update(uppercaseSectionTitles)
|
__UPPERCASE_SECTION_TITLES.update(uppercaseSectionTitles)
|
||||||
__setup-code(code, body)
|
__setup-code(code, body)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user