///|
// Running content: the page header and footer (asciidoctor-pdf's
// `ink_running_content` and `allocate_running_content_layout`), from the
// theme's `header_*` and `footer_*` keys: each page side (recto, verso)
// has up to three columns (left, center, right) whose content is text with
// attribute references (`{page-number}`, `{chapter-title}`, ...).
///|
/// The geometry of a periphery on one page side: its box (from the page's
/// left and top edges), its padding and the content area inside.
priv struct Trim {
left : Double
top : Double
width : Double
height : Double
padding : (Double, Double, Double, Double)
}
///|
/// A trim margin or content margin value: `inherit` on the sides, else a
/// length.
fn trim_rect(v : @theme.Value) -> Array[@theme.Value] {
let items = match v {
Array(items) => items
Null => [Int(0), Str("inherit"), Int(0), Str("inherit")]
other => [other]
}
match items.length() {
0 => [Int(0), Int(0), Int(0), Int(0)]
1 => [items[0], items[0], items[0], items[0]]
2 => [items[0], items[1], items[0], items[1]]
3 => [items[0], items[1], items[2], items[1]]
_ => items[:4].to_owned()
}
}
///|
/// The (left, right) page margins of recto and verso pages when they
/// differ (`media=prepress`), else None.
let side_margins : Ref[((Double, Double), (Double, Double))?] = { val: None, }
///|
/// A periphery's box on a page side (`allocate_running_content_layout`).
fn periphery_trim(
periphery : String,
side : String,
page_size? : (Double, Double),
) -> Trim {
let setup = page_setup.val
let (page_w, page_h) = page_size.unwrap_or((setup.width, setup.height))
let (margin_left, margin_right) = match side_margins.val {
Some((recto, verso)) => if side == "verso" { verso } else { recto }
None => (setup.margin_left, setup.margin_right)
}
let page_margin = [
setup.margin_top,
margin_right,
setup.margin_bottom,
margin_left,
]
let side_key = (name : String) => {
match tv("\{periphery}_\{side}_\{name}") {
Null => tv("\{periphery}_\{name}")
v => v
}
}
let margin = trim_rect(side_key("margin")).mapi((i, v) => {
if i % 2 == 1 && v is Str("inherit") {
page_margin[i]
} else {
v.to_f()
}
})
let content_margin = trim_rect(side_key("content_margin")).mapi((i, v) => {
if i % 2 == 1 && v is Str("inherit") {
page_margin[i] - margin[i]
} else {
v.to_f()
}
})
let padding = match side_key("padding") {
Null => content_margin
v => {
let (t, r, b, l) = expand_rect(v)
[t, r, b, l].mapi((i, x) => x + content_margin[i])
}
}
let height = t_num_or(periphery + "_height", 0.0)
let top = if periphery == "header" {
margin[0]
} else {
page_h - height - margin[2]
}
{
left: margin[3],
top,
width: page_w - margin[3] - margin[1],
height,
padding: (padding[0], padding[1], padding[2], padding[3]),
}
}
///|
/// The columns of a periphery on a page side (`_columns`, e.g.
/// `<40% =20% >40%`): each position's alignment, offset and width in the
/// content area; without the key, each position spans it.
fn periphery_columns(
periphery : String,
side : String,
content_width : Double,
) -> Array[(String, Alignment, Double, Double)] {
let spec = match tv("\{periphery}_\{side}_columns") {
Null => tv("\{periphery}_columns")
v => v
}
match spec {
Null =>
[
("left", Left, 0.0, content_width),
("center", Center, 0.0, content_width),
("right", Right, 0.0, content_width),
]
v => {
let specs = v
.to_ruby_s()
.replace_all(old=",", new=" ")
.split(" ")
.filter(s => s != "")
.map(s => s.to_owned())
.collect()
let (l, c, r) = match specs.length() {
0 | 1 => ("0", specs.get(0).unwrap_or("100"), "0")
2 => (specs[0], "0", specs[1])
_ => (specs[0], specs[1], specs[2])
}
let parse = (s : String) => {
let first = s.get_char(0).unwrap_or('0')
if first >= '0' && first <= '9' {
(Left, @theme.ruby_string_to_f(s))
} else {
let align = match first {
'=' => Center
'>' => Right
_ => Left
}
(align, @theme.ruby_string_to_f(s[1:].to_owned()))
}
}
let cols = [("left", parse(l)), ("center", parse(c)), ("right", parse(r))]
let total = cols.fold(init=0.0, (acc, col) => acc + col.1.1)
let widths = cols.map(col => {
if total > 0.0 {
col.1.1 / total * content_width
} else {
0.0
}
})
[
("left", cols[0].1.0, 0.0, widths[0]),
("center", cols[1].1.0, widths[0], widths[1]),
("right", cols[2].1.0, widths[0] + widths[1], widths[2]),
]
}
}
}
///|
/// The title a section shows in running content (`_title_style`:
/// `toc` numbered, `basic` plain, else numbered formally).
fn running_title(section : @core.Node, periphery : String) -> String {
match t_str(periphery + "_title_style") {
Some("toc") => numbered_title(section)
Some("basic") => section.title().unwrap_or("")
_ => numbered_title(section, formal=true)
}
}
///|
/// asciidoctor-pdf's `apply_subs_discretely` with
/// `drop_lines_with_unresolved_attributes`: the normal substitutions with
/// missing attributes left as they are, then the lines still holding a
/// reference of the original dropped.
fn apply_subs_discretely(doc : @core.Node, value : String) -> String {
let saved_missing = doc.attr("attribute-missing")
ignore(doc.set_attr("attribute-missing", value=Str("skip")))
let escaped = value.contains("\\{")
let source = if escaped {
value.replace_all(old="\\{", new="\\\\\\{")
} else {
value
}
let result = doc.apply_subs(source, @core.normal_subs)
let result = if result.contains("{") {
let kept = []
for line in result.split("\n") {
let line = line.to_owned()
let mut drop = false
// a simple attribute reference left unresolved: `{name}`
let mut i = 0
while i < line.length() {
if line[i] == '{' {
match line[i + 1:].find("}") {
Some(j) => {
let name = line[i + 1:i + 1 + j].to_owned()
if name != "" &&
name
.iter()
.all(c => {
c.is_ascii_alphabetic() ||
c.is_ascii_digit() ||
c == '_' ||
c == '-'
}) &&
source.contains("{" + name + "}") {
drop = true
break
}
}
None => ()
}
}
i += 1
}
if !drop {
kept.push(line)
}
}
kept.join("\n")
} else {
result
}
let result = if escaped {
result.replace_all(old="\\{", new="{")
} else {
result
}
match saved_missing {
Some(m) => ignore(doc.set_attr("attribute-missing", value=Str(m)))
None => ignore(doc.remove_attr("attribute-missing"))
}
result
}
///|
/// A page's section context for running content: the part, chapter and
/// section it is in (their titles and numerals).
priv struct PageSections {
part : (String, String?)
chapter : (String, String?)
section : (String, String?)
}
///|
/// The part, chapter and section of each page (1-based), as
/// `ink_running_content` indexes them by the pages sections start on.
fn Converter::sections_by_page(
self : Converter,
periphery : String,
num_pages : Int,
body_start : Int,
) -> Array[PageSections] {
let doc = self.doc
let is_book = doc.doctype() == "book"
let sectlevels = t_num_or(periphery + "_sectlevels", 2.0).to_int()
let sections = doc.find_by(context=Section).filter(s => s.level <= sectlevels)
let part_starts : Map[Int, @core.Node?] = Map([])
let chapter_starts : Map[Int, @core.Node] = Map([])
let section_starts : Map[Int, @core.Node] = Map([])
let trailing_starts : Map[Int, @core.Node] = Map([])
for sect in sections {
let pgnum = match sect.attr("pdf-page-start") {
Some(n) => @string.parse_int(n) catch { _ => 0 }
None => 0
}
let is_part = sect.sectname == Some("part")
if is_book && (is_part || sect.level == 1) {
if is_part {
if !part_starts.contains(pgnum) {
part_starts[pgnum] = Some(sect)
}
} else {
if !chapter_starts.contains(pgnum) {
chapter_starts[pgnum] = sect
}
if sect.sectname == Some("appendix") && !part_starts.is_empty() {
part_starts[pgnum] = None
}
}
} else {
trailing_starts[pgnum] = sect
if !section_starts.contains(pgnum) {
section_starts[pgnum] = sect
}
}
}
let info = (s : @core.Node?) => {
match s {
Some(sect) => (running_title(sect, periphery), sect.numeral)
None => ("", None)
}
}
let toc_range = match self.toc_extent {
Some(extent) => Some((extent.from_page + 1, extent.to_page + 1))
None => None
}
let toc_title = doc.attr("toc-title").unwrap_or("")
let result = [{ part: ("", None), chapter: ("", None), section: ("", None), }]
let mut last_part : @core.Node? = None
let mut last_chap : @core.Node? = None
let mut pre = is_book
let mut last_sect : @core.Node? = None
let mut threshold = 1
for pgnum in 1..<=num_pages {
let part = part_starts.get(pgnum)
match part {
Some(p) => {
last_part = p
last_chap = None
pre = false
last_sect = None
}
None => ()
}
let chap = chapter_starts.get(pgnum)
match chap {
Some(c) => {
last_chap = Some(c)
pre = false
last_sect = None
}
None => ()
}
match section_starts.get(pgnum) {
Some(s) => last_sect = Some(s)
None =>
if part is Some(_) || chap is Some(_) {
threshold = pgnum
} else if last_sect is Some(_) {
for prev = pgnum - 1; prev >= threshold; prev = prev - 1 {
match trailing_starts.get(prev) {
Some(s) => {
last_sect = Some(s)
break
}
None => ()
}
}
}
}
let in_toc = match toc_range {
Some((from, to)) => pgnum >= from && pgnum <= to
None => false
}
let (chapter, section) = if in_toc {
if is_book {
((toc_title, None), ("", None))
} else {
match section_starts.get(pgnum) {
Some(s) => (("", None), info(Some(s)))
None => (("", None), (toc_title, None))
}
}
} else if pre && last_chap is None {
let title = if pgnum < body_start {
doc.doctitle().unwrap_or("")
} else {
match doc.attr("preface-title") {
Some(t) => t
None =>
if doc.attribute_locked("preface-title") {
""
} else {
"Preface"
}
}
}
((title, None), info(last_sect))
} else {
(info(last_chap), info(last_sect))
}
result.push({ part: info(last_part), chapter, section, })
}
result
}
///|
/// Ink the running content of `periphery` (`header`, `footer`) on every
/// page after the first `skip_pages`, numbering pages after the first
/// `skip_pagenums` (earlier ones in roman numerals).
fn Converter::ink_running_content(
self : Converter,
periphery : String,
skip_pages : Int,
skip_pagenums : Int,
body_start : Int,
) -> Unit {
let doc = self.doc
let flow = self.flow
let pages = flow.model.pages
let num_pages = pages.length()
if skip_pages >= num_pages {
return
}
let by_page = self.sections_by_page(periphery, num_pages, body_start)
// the document title, and the page count, as attributes
match doc.doctitle_partitioned(use_fallback=true) {
Some(t) => {
ignore(doc.set_attr("document-title", value=Str(t.main)))
match t.subtitle {
Some(sub) => ignore(doc.set_attr("document-subtitle", value=Str(sub)))
None => ()
}
}
None => ()
}
ignore(
doc.set_attr(
"page-count",
value=Str((num_pages - skip_pagenums).to_string()),
),
)
let pagenums = doc.has_attr("pagenums")
let (base, line_height) = category_font(periphery)
let bg = t_color(periphery + "_background_color")
let border_width = t_num_or(periphery + "_border_width", 0.0)
let border_color = if border_width > 0.0 {
match tv(periphery + "_border_color") {
Null => base_border_color()
Transparent => None
v => value_color(v)
}
} else {
None
}
if t_num_or(periphery + "_column_rule_width", 0.0) > 0.0 {
unsupported("theme", periphery + "_column_rule", doc)
}
let valign = t_str(periphery + "_vertical_align").unwrap_or("middle")
// images go by `_image_vertical_align`, else by the content's
let img_valign = match
t_str(periphery + "_image_vertical_align").or_some(Some(valign)) {
Some("middle") | None => "center"
Some(v) => v
}
// the image a column's content names, resolved once per side and column
let images : Map[String, Background] = Map([])
let face = flow.font(base)
let metrics = line_metrics(line_height, face, base.size)
// the footer's content moves down by half its border
let content_offset = if periphery == "footer" {
border_width * 0.5
} else {
0.0
}
let disable_on_pages = self.disable_running_content
.get(periphery)
.unwrap_or([])
for index in skip_pages.. ignore(doc.set_attr("part-numeral", value=Str(n)))
None => ignore(doc.remove_attr("part-numeral"))
}
ignore(doc.set_attr("chapter-title", value=Str(info.chapter.0)))
match info.chapter.1 {
Some(n) => ignore(doc.set_attr("chapter-numeral", value=Str(n)))
None => ignore(doc.remove_attr("chapter-numeral"))
}
ignore(doc.set_attr("section-title", value=Str(info.section.0)))
ignore(
doc.set_attr(
"section-or-chapter-title",
value=Str(
if info.section.0 != "" {
info.section.0
} else {
info.chapter.0
},
),
),
)
ignore(
doc.set_attr(
"page-layout",
value=Str(
if pages[index].width_pt > pages[index].height_pt {
"landscape"
} else {
"portrait"
},
),
),
)
let page = pages[index]
let trim = periphery_trim(
periphery,
side,
page_size=(page.width_pt, page.height_pt),
)
let (pad_top, pad_right, pad_bottom, pad_left) = trim.padding
match bg {
Some(color) =>
page.items.push(
rect_item(trim.left, trim.top, trim.width, trim.height, fill=color),
)
None => ()
}
match border_color {
Some(color) => {
let y = if periphery == "header" {
trim.top + trim.height
} else {
trim.top
}
page.items.push(
hrule(trim.left, trim.left + trim.width, y, border_width, color),
)
}
None => ()
}
let content_left = trim.left + pad_left
let content_width = trim.width - pad_left - pad_right
let content_height = trim.height - pad_top - pad_bottom - border_width * 0.5
let prose_height = content_height -
metrics.padding_top -
metrics.padding_bottom
let box_top = trim.top + pad_top + content_offset
for column in periphery_columns(periphery, side, content_width) {
let (position, align, x, width) = column
if width <= 0.0 {
continue
}
let key = "\{periphery}_\{side}_\{position}"
let mut raw = match tv(key + "_content") {
Null | Str("") => continue
v => v.to_ruby_s()
}
// content that is an image macro is an image in the column, aligned
// as the column is; one that cannot be read stays an inline image
// (its alt text)
match image_macro(raw) {
Some((_, attrlist)) => {
let resolved = match images.get(key) {
Some(r) => r
None => {
let r = self.resolve_box_image(
raw,
true,
(width, content_height),
background=false,
)
images[key] = r
r
}
}
match resolved {
Found(img) => {
let img = {
..img,
position: match align {
Center => "center"
Right => "right"
_ => "left"
},
vposition: img_valign,
}
match
self.box_image_items(
img,
content_left + x,
box_top,
width,
content_height,
) {
Some((drawn, iw, ih)) => {
page.items.append(drawn)
match img.link {
Some(link) if link != "" => {
let (ix, iy) = match drawn.get(0) {
Some(Image(i)) => (i.x_pt, i.y_pt)
Some(Graphic(g)) => (g.x_pt, g.y_pt)
_ => (content_left + x, box_top)
}
page.items.push(
Link({
x_pt: ix,
y_pt: iy,
w_pt: iw,
h_pt: ih,
target: if link.has_prefix("#") {
Named(pdf_anchor_name(link[1:].to_owned()))
} else {
Uri(link)
},
}),
)
}
_ => ()
}
continue
}
// an image that cannot be drawn shows its alt text
None =>
match img.alt {
Some(alt) => raw = alt
None => continue
}
}
}
Unreadable(path) => raw = "image:\{path}[\{attrlist}]"
_ => raw = "image:[\{attrlist}]"
}
}
None => ()
}
let (style, _) = theme_font(base, line_height, key)
let content = if raw == "{page-number}" {
if pagenums {
label
} else {
continue
}
} else {
apply_subs_discretely(doc, raw)
}
let content = match style.text_transform {
Some(transform) => transform_text(content, transform)
None => content
}
let style = { ..style, text_transform: None, }
let lines = typeset_lines(
parse_formatted(content, style),
flow.catalog,
width,
align,
)
if lines.is_empty() {
continue
}
let sface = flow.font(style)
// the lines that fit in the content area (`overflow: :truncate`),
// placed from its top, middle or bottom
let placed = []
let mut height = 0.0
let mut baseline = 0.0
for i, line in lines {
let (a, d, h) = line_extent(line, sface, style.size, false)
let next = if i == 0 { a } else { baseline + h + metrics.leading }
if next + d > prose_height + 0.0001 && i > 0 {
break
}
baseline = next
height = next + d
placed.push((line, next))
}
let shift = match valign {
"top" => 0.0
"bottom" => prose_height - height
// centred: the box starts half a descender lower
// (`font.descender * 0.5`), and Prawn centres the text half a
// descender higher (`@at[1] -= (@height - height + @descender) *
// 0.5`)
_ => (prose_height - height) / 2.0
}
for entry in placed {
let (line, at) = entry
page.items.append(
flow.line_items(line, content_left + x, box_top + shift + at),
)
}
}
}
}