///|
// Headings (asciidoctor-pdf's `ink_heading` and `arrange_heading` in the
// `theme_font :heading, level: n` font).
///|
/// The heading font of `level` and its line height; the heading's font
/// style is inherited by its text as markup would be (`apply_text_decoration
/// font_styles`), so it holds against the text's own font.
fn heading_font(level : Int) -> (Style, Double) {
let (style, line_height) = theme_font_heading(
base_style(),
base_line_height(),
level,
)
({ ..style, bold: style.doc_bold, italic: style.doc_italic, }, line_height)
}
///|
/// A heading level's key, else the `heading_*` one.
fn heading_key(level : Int, name : String) -> @theme.Value {
match tv("heading_h\{level}_\{name}") {
Null => tv("heading_\{name}")
v => v
}
}
///|
/// The alignment of a heading of `level`: `heading_h_text_align`, else
/// `heading_text_align`, else the base alignment.
fn Converter::heading_align(self : Converter, level : Int) -> Alignment {
match value_str(heading_key(level, "text_align")) {
Some(align) => alignment_of(align)
None => self.text_align
}
}
///|
/// asciidoctor-pdf's `@section_indent` (`section_indent`, expanded by
/// `expand_indent_value`): how far the body is indented from the left and
/// the right; section titles, the TOC title, footnotes, the abstract and
/// the index are outdented back.
fn section_indent() -> (Double, Double)? {
match tv("section_indent") {
Null | Bool(false) => None
Array(items) =>
Some(
(
items.get(0).map(v => v.to_f()).unwrap_or(0.0),
items.get(1).map(v => v.to_f()).unwrap_or(0.0),
),
)
v => Some((v.to_f(), v.to_f()))
}
}
///|
/// `outdent_section`: `body` outside the section indent (when `enabled`).
fn[T] Converter::outdent_section(
self : Converter,
enabled? : Bool = true,
body : () -> T,
) -> T {
match section_indent() {
Some((left, right)) if enabled => self.flow.indent(-left, -right, body)
_ => body()
}
}
///|
/// A heading at `level` (1 is the document title), asciidoctor-pdf's
/// `ink_heading` in the heading font: its top margin (none at the top of a
/// page, where `heading_margin_page_top` applies), the text, its bottom
/// margin; outside the section indent with `outdent`.
fn Converter::ink_heading(
self : Converter,
title : String,
level : Int,
align? : Alignment,
category? : String,
outdent? : Bool = false,
) -> Unit {
self.outdent_section(enabled=outdent, () => {
self.ink_heading_text(title, level, align?, category?)
})
}
///|
fn Converter::ink_heading_text(
self : Converter,
title : String,
level : Int,
align? : Alignment,
category? : String,
) -> Unit {
let (style, line_height) = heading_font(level)
// a heading of a category of its own (`toc_title`) in that font too
let (style, line_height) = match category {
Some(c) => {
let (s, lh) = theme_font(style, line_height, c)
({ ..s, bold: s.doc_bold, italic: s.doc_italic, }, lh)
}
None => (style, line_height)
}
let flow = self.flow
if flow.at_page_top() {
let page_top = value_num(heading_key(level, "margin_page_top")).unwrap_or(
0.0,
)
if page_top > 0.0 {
flow.move_down(page_top)
}
} else {
flow.margin(value_num(heading_key(level, "margin_top")).unwrap_or(0.0))
}
let title = match style.text_transform {
Some(transform) => transform_text(title, transform)
None => title
}
let style = { ..style, text_transform: None, }
let face = flow.font(style)
let category = "heading_h\{level}"
let start_y = flow.y
let start_page = flow.page
// `pad_box _padding`
let (pad_top, pad_right, pad_bottom, pad_left) = match
tv(category + "_padding") {
Null => (0.0, 0.0, 0.0, 0.0)
v => expand_rect(v)
}
flow.move_down(pad_top)
flow.indent(pad_left, pad_right, () => {
flow.typeset(
parse_formatted(title, style, normalize=false),
line_metrics(line_height, face, style.size),
style,
align=align.unwrap_or(self.heading_align(level)),
)
})
if tv(category + "_padding") != Null {
flow.pad_bottom(pad_bottom)
}
// the border around the heading and its padding
// (`theme_fill_and_stroke_bounds`), from where it started on this page
match tv(category + "_border_width") {
Null => ()
width => {
let top = if flow.page == start_page { start_y } else { flow.top }
let color = match tv(category + "_border_color") {
Null => base_border_color()
v => value_color(v)
}
match color {
Some(color) =>
flow
.items()
.append(
border_items(width, color, flow.left, flow.right, top, flow.y),
)
None => ()
}
}
}
flow.margin(value_num(heading_key(level, "margin_bottom")).unwrap_or(0.0))
}
///|
/// asciidoctor-pdf's `fill_and_stroke_bounds` without a fill: a border of
/// `width` (one width, or per side: [top, right, bottom, left], [vertical,
/// horizontal]) around the box; per-side borders are rules, projected by
/// half the adjacent sides' widths.
fn border_items(
width : @theme.Value,
color : Color,
left : Double,
right : Double,
top : Double,
bottom : Double,
) -> Array[@pagelayout.PageItem] {
match width {
Array(items) => {
let w = items.map(v => value_num(v).unwrap_or(0.0))
let w = if w.length() == 2 { [w[0], w[1], w[0], w[1]] } else { w }
let at = (i : Int) => w.get(i).unwrap_or(0.0)
let (wt, wr, wb, wl) = (at(0), at(1), at(2), at(3))
let items = []
if wt > 0.0 {
items.push(hrule(left - wl * 0.5, right - wr * 0.5, top, wt, color))
}
if wr > 0.0 {
items.push(vrule(right, top - wt * 0.5, bottom + wb * 0.5, wr, color))
}
if wb > 0.0 {
items.push(hrule(left - wl * 0.5, right - wr * 0.5, bottom, wb, color))
}
if wl > 0.0 {
items.push(vrule(left, top - wt * 0.5, bottom + wb * 0.5, wl, color))
}
items
}
v =>
match value_num(v) {
Some(w) if w > 0.0 =>
[
rect_item(
left,
top,
right - left,
bottom - top,
stroke=color,
stroke_width=w,
),
]
_ => []
}
}
}
///|
/// asciidoctor-pdf's `arrange_heading` with a numeric (or no)
/// `heading_min_height_after`: keep a heading with the content after it,
/// when there is some, by starting a new page unless the heading, its top
/// margin and that much room below it fit here.
fn Converter::arrange_heading(
self : Converter,
title : String,
hlevel : Int,
content_after : Bool,
) -> Unit {
let flow = self.flow
let (style, line_height) = heading_font(hlevel)
if style.size > flow.cursor() {
flow.advance_page()
return
}
let face = flow.font(style)
let min_height_after = match tv("heading_min_height_after") {
Str("auto") => {
unsupported("theme", "heading_min_height_after: auto", self.doc)
0.0
}
v => value_num(v).unwrap_or(0.0)
}
let space_below = if min_height_after > 0.0 && content_after {
min_height_after +
value_num(heading_key(hlevel, "margin_bottom")).unwrap_or(0.0)
} else {
0.0
}
let title = match style.text_transform {
Some(transform) => transform_text(title, transform)
None => title
}
let height = flow.height_of(
parse_formatted(title, { ..style, text_transform: None, }, normalize=false),
line_metrics(line_height, face, style.size),
style,
)
let margin_top = value_num(tv("heading_h\{hlevel}_margin_top"))
.or_some(t_num("heading_margin_top"))
.unwrap_or(0.0)
if flow.cursor() < height + margin_top + space_below {
flow.advance_page()
}
}