///|
// Delimited blocks drawn in a frame (code, example, sidebar) and the frame
// itself (asciidoctor-pdf's `theme_fill_and_stroke_block`), styled by the
// theme's `_*` keys.
///|
/// The frame of a block category: its background and border (width,
/// colour, radius), as `theme_fill_and_stroke_block` resolves them; None
/// when it has neither.
priv struct Frame {
background : Color?
border_width : Double
border_color : Color
/// how far the dashed continuation line stays from the sides
inset : Double
/// the colour of the dashed line where the block breaks across pages
gap_color : Color
/// how far the dashed line moves out of the block
shift : Double
}
///|
/// The frame of `category` (`_border_width`, `_border_color`,
/// `_border_radius`, `_background_color`, or `background` in place of the
/// last).
fn frame_of(category : String, background? : Color?) -> Frame? {
let mut radius_override : Double? = None
let mut width = match tv(category + "_border_width") {
Array(items) => {
radius_override = Some(0.0)
items.get(0).bind(value_num)
}
v => value_num(v)
}
if width.unwrap_or(0.0) <= 0.0 {
width = None
}
let bg = match background {
Some(bg) => bg
None => t_color(category + "_background_color")
}
if width is None && bg is None {
return None
}
let page_bg = page_background_color()
// `resolve_theme_color :_border_color, base_border_color,
// page_bg_color`: transparent is the page's colour
let border_color = match tv(category + "_border_color") {
Transparent => page_bg
Null =>
match tv("base_border_color") {
Transparent => page_bg
v => value_color(v).unwrap_or(page_bg)
}
v => value_color(v).unwrap_or(page_bg)
}
let radius = radius_override.unwrap_or(
t_num_or(category + "_border_radius", 0.0),
)
let (gap_color, shift, border_width) = match width {
Some(w) =>
if border_color == page_bg {
(page_bg, w * 0.5, w)
} else {
match bg {
Some(b) if b != border_color => (b, 0.0, w)
_ => (page_bg, 0.0, w)
}
}
None => (page_bg, 0.25, 0.0)
}
Some({
background: bg,
border_width,
border_color,
inset: radius + border_width,
gap_color,
shift,
})
}
///|
/// The items that paint `frame` on the part of a block from `top` to
/// `bottom` of one page: the background and border, and a dashed line in
/// the gap colour cutting into the edge where the block continues from or
/// onto another page.
fn Frame::items(
self : Frame,
left : Double,
right : Double,
top : Double,
bottom : Double,
first : Bool,
last : Bool,
) -> Array[@pagelayout.PageItem] {
let items = [
rect_item(
left,
top,
right - left,
bottom - top,
fill?=self.background,
stroke?=if self.border_width > 0.0 {
Some(self.border_color)
} else {
None
},
stroke_width=self.border_width,
),
]
let width = (if self.border_width > 0.0 { self.border_width } else { 0.5 }) *
1.2
if !first {
items.append(
dashed_hrule(
left + self.inset,
right - self.inset,
top - self.shift,
width,
self.gap_color,
),
)
}
if !last {
items.append(
dashed_hrule(
left + self.inset,
right - self.inset,
bottom + self.shift,
width,
self.gap_color,
),
)
}
items
}
///|
/// A category's padding (`_padding`): top, right, bottom, left.
fn padding_of(category : String) -> (Double, Double, Double, Double) {
t_padding(category + "_padding")
}
///|
/// Whether a category's captions go below the block (`_caption_end:
/// bottom`, formerly `_caption_side`).
fn caption_below(category : String) -> Bool {
t_str(category + "_caption_end") == Some("bottom")
}
///|
/// asciidoctor-pdf's `convert_code` (listing, literal and source blocks,
/// without syntax highlighting): the content in the `code` font, in the
/// code padding, over the code frame; a stem block's text is shown as it
/// is.
fn Converter::convert_code(
self : Converter,
node : @core.Node,
plain? : Bool = false,
) -> Unit {
let flow = self.flow
let (style, line_height) = theme_font(
base_style(),
base_line_height(),
"code",
)
let face = flow.font(style)
let metrics = line_metrics(line_height, face, style.size)
let text = guard_indentation(node.content())
let fragments = if plain {
[{ text, style, anchor: None, }]
} else {
parse_formatted(text, style, normalize=false)
}
let (pad_top, pad_right, pad_bottom, pad_left) = padding_of("code")
// `autofit` (`compute_autofit_font_size`): a smaller font when the widest
// line does not fit, no smaller than `code_font_size_min` (else
// `base_font_size_min`); the line metrics stay those of the code font
let (fragments, style) = if node.has_option("autofit") ||
node.document().has_attr("autofit-option") {
let widths = [0.0]
for fragment in fragments {
let parts = fragment.text.split("\n").collect()
for i, part in parts {
if i > 0 {
widths.push(0.0)
}
widths[widths.length() - 1] += flow.width_of(
part.to_owned(),
fragment.style,
)
}
}
let actual = widths.fold(init=0.0, @cmp.maximum)
let available = flow.width() - pad_left - pad_right
if actual > available {
let adjusted = (available * style.size / actual * 10000.0).floor() /
10000.0
let min = match tv("code_font_size_min") {
Null => tv("base_font_size_min")
v => v
}
let size = match resolve_font_size(min, style.size) {
Some(m) if adjusted < m => m
_ => adjusted
}
(
fragments.map(f => {
..f,
style: { ..f.style, size: f.style.size * size / style.size, },
}),
{ ..style, size, },
)
} else {
(fragments, style)
}
} else {
(fragments, style)
}
let below = caption_below("code")
// arrange_block: the caption and the background are tared, so the block
// starts on the next page when not even its first line (with the room
// its bottom padding needs, when it is the last) fits here
self.arrange_block(keep_together=node.has_option("unbreakable"), node~, fn(
c,
) {
let flow = c.flow
c.add_dest(node)
if node.has_title() && !below {
flow.tared(() => c.ink_caption(node.captioned_title(), category="code"))
}
let start = flow.mark()
let (left, right) = (flow.left, flow.right)
flow.move_down(pad_top)
flow.indent(pad_left, pad_right, fn() {
flow.typeset(fragments, metrics, style, bottom_gutter=pad_bottom)
})
flow.pad_bottom(pad_bottom)
match frame_of("code") {
Some(frame) =>
flow.underlay(start, fn(top, bottom, first, last) {
frame.items(left, right, top, bottom, first, last)
})
None => ()
}
})
if node.has_title() && below {
self.ink_caption(node.captioned_title(), bottom=true, category="code")
}
self.block_margin_bottom(node)
}
///|
/// A category's font (`theme_font `) over the base font: the
/// style and line height a block's content is set in.
fn category_font(category : String) -> (Style, Double) {
theme_font(base_style(), base_line_height(), category)
}
///|
/// The admonition label of `name` in its font (`theme_font_cascade
/// [:admonition_label, :admonition_label_]`): the label's style and
/// its text, transformed.
fn admonition_label_font(name : String, text : String) -> (Style, String) {
let (style, lh) = category_font("admonition_label")
let (style, _) = theme_font(style, lh, "admonition_label_" + name)
let text = match style.text_transform {
Some(transform) => transform_text(text, transform)
None => text
}
// the label is set as it is (`inline_format: false`), in the font
(
{
..style,
text_transform: None,
font_set: true,
bold: style.doc_bold,
italic: style.doc_italic,
},
text,
)
}
///|
/// asciidoctor-pdf's `AdmonitionIcons` merged with the theme's
/// `admonition_icon_` (`admonition_icon_data`): the icon set, icon
/// name, colour and size of an admonition type (a type without one takes
/// the note's icon, in the text colour).
fn admonition_icon(name : String) -> (String, String, Color, Double) {
let (default_name, default_color) = match name {
"caution" => ("fas-fire", Some(rgb_hex(0xBF3400)))
"important" => ("fas-exclamation-circle", Some(rgb_hex(0xBF0000)))
"note" => ("fas-info-circle", Some(rgb_hex(0x19407C)))
"tip" => ("far-lightbulb", Some(rgb_hex(0x111111)))
"warning" => ("fas-exclamation-triangle", Some(rgb_hex(0xBF6900)))
_ => ("fas-info-circle", None)
}
let data = match tv("admonition_icon_" + name) {
Hash(map, ..) => map
_ => Map([])
}
let icon_name = data.get("name").bind(value_str).unwrap_or(default_name)
let color = data
.get("stroke_color")
.bind(value_color)
.or_some(default_color)
.unwrap_or(base_font_color())
let size = data.get("size").bind(value_num).unwrap_or(24.0)
let (set, glyph_name) = match icon_name.find("-") {
Some(i) => (icon_name[:i].to_owned(), icon_name[i + 1:].to_owned())
None => ("fas", icon_name)
}
(set, glyph_name, color, size)
}
///|
/// asciidoctor-pdf's `convert_admonition`: the label (or, with
/// `icons=font`, the icon) in a column left of a rule
/// (`admonition_column_rule_*`), the content right of it in the
/// `admonition` font, all in the admonition padding and frame.
fn Converter::admonition_icon_image(
self : Converter,
node : @core.Node,
name : String,
) -> BoxImage? {
// `convert_admonition` with image icons: the `icon` attribute's image
// (or `/.`, `get_icon_image_path`) unless the
// theme's `admonition_icon_` has an `image`, resolved against the
// theme's directory
let doc = self.doc
let has_icon = node.has_attr("icon")
let theme_image = match tv("admonition_icon_\{name}") {
Hash(map, ..) =>
match map.get("image") {
Some(Null) | None => None
Some(v) => Some(v.to_ruby_s())
}
_ => None
}
let (path, format) = match theme_image {
Some(image) if !has_icon => {
let themesdir = current_theme.val.dir()
let path = with_attrs(doc, [("imagesdir", themesdir)], () => {
apply_subs_discretely(doc, image)
})
(
@theme.resolve_theme_asset(theme_files(doc), path, theme_dir=themesdir),
image_target_and_format(path, None).1,
)
}
_ => {
// the data-uri attribute is off while the icon's path is computed
let data_uri = doc.attr("data-uri")
if data_uri is Some(_) {
ignore(doc.remove_attr("data-uri"))
}
let uri = node.icon_uri(name)
match data_uri {
Some(v) => ignore(doc.set_attr("data-uri", value=Str(v)))
None => ()
}
let (_, format) = image_target_and_format(uri, None)
let path = if uri.has_prefix("/") {
uri
} else {
doc.normalize_system_path(uri)
}
(path, format)
}
}
match doc.vfs().read(path) {
Some(data) =>
Some({
data,
svg: format == "svg",
path,
fit: None,
width: None,
height: None,
position: "center",
vposition: "center",
link: None,
alt: None,
})
None => {
warning(
"admonition icon image\{if has_icon { "" } else { " for " + name.to_upper() }} not found or not readable: \{path}",
node,
)
None
}
}
}
///|
fn Converter::convert_admonition(self : Converter, node : @core.Node) -> Unit {
let flow = self.flow
let name = node.attr("name").unwrap_or("")
let label_align = t_align("admonition_label_text_align", Center)
let valign = t_str("admonition_label_vertical_align").unwrap_or("middle")
let min_width = t_num("admonition_label_min_width")
// `icons=font`: the admonition's icon in a column one and a half times as
// wide as it; other icons (images) are left out, and the label is text
let icon = match self.doc.attr("icons") {
Some("font") if !node.has_attr("icon") => {
let (set, icon_name, color, size) = admonition_icon(name)
match flow.catalog.icon_glyph(set, icon_name) {
Some(glyph) => Some((set, glyph, color, size))
None => None
}
}
_ => None
}
// other icons are images: the `icon` attribute's, else the theme's
// `admonition_icon_` image, else `/.`
let icon_image = match self.doc.attr("icons") {
Some(icons) if icon is None && (icons != "font" || node.has_attr("icon")) =>
self.admonition_icon_image(node, name)
_ => None
}
let (label_style, label) = admonition_label_font(
name,
sanitize(node.caption().unwrap_or(name)),
)
let label_lines = if icon is Some(_) {
[]
} else {
typeset_lines(
[{ text: label, style: label_style, anchor: None, }],
flow.catalog,
1.0e6,
Left,
)
}
let natural_width = match icon {
Some((_, _, _, size)) => size * 1.5
None =>
if label_lines.length() > 0 {
label_lines[0].fit_width
} else {
0.0
}
}
let label_width = match (icon_image, min_width) {
// an image takes `admonition_label_min_width`, else 36pt
(Some(_), Some(w)) => w
(Some(_), None) => 36.0
(None, Some(w)) if w > natural_width => w
_ => natural_width
}
let (pad_top, pad_right, pad_bottom, pad_left) = admonition_padding()
let (_, label_pad_right, _, label_pad_left) = match
tv("admonition_label_padding") {
Null => admonition_padding()
v => expand_rect(v)
}
let label_face = flow.font(label_style)
let rule_width = admonition_column_rule_width()
let rule_color = match tv("admonition_column_rule_color") {
Null => base_border_color()
Transparent => None
v => value_color(v)
}
let (content_style, content_lh) = category_font("admonition")
// arrange_block: the label and the rule are drawn once the extent is
// known, so they take no part in keeping the block on this page; the
// title is content
self.arrange_block(keep_together=node.has_option("unbreakable"), node~, fn(
c,
) {
let flow = c.flow
c.add_dest(node)
let start = flow.mark()
let (left, right) = (flow.left, flow.right)
let box_left = flow.left + label_pad_left
let rule_x = box_left + label_width + label_pad_right
flow.indent(label_pad_left, pad_right, fn() {
flow.move_down(pad_top)
flow.indent(label_width + label_pad_right + pad_left, 0.0, fn() {
if node.has_title() {
c.ink_caption(node.title().unwrap_or(""), category="admonition")
}
c.push_gutter(node, pad_bottom)
with_font(content_style, content_lh, () => c.traverse(node))
c.pop_gutter()
})
flow.pad_bottom(pad_bottom)
})
let frame = frame_of("admonition")
flow.underlay(start, fn(top, bottom, first, last) {
let items = match frame {
Some(frame) => frame.items(left, right, top, bottom, first, last)
None => []
}
match rule_color {
Some(color) if rule_width > 0.0 =>
items.push(vrule(rule_x, top, bottom, rule_width, color))
_ => ()
}
match icon {
Some((set, glyph, color, icon_size)) if first => {
// the icon, no taller than the label box, centred in it: moved
// down by half the room it leaves, then set as Prawn's `text`
// (the first baseline an ascender below the top), across the
// column (`fit_icon_to_bounds`, `icon`)
let height = bottom - top
let size = @cmp.minimum(height, icon_size)
let offset = if valign == "middle" {
@cmp.maximum((height - size) * 0.5, 0.0)
} else {
0.0
}
let style = {
..Style::new(family=set, size~, color~),
font_set: true,
}
let lines = typeset_lines(
[{ text: glyph, style, anchor: None, }],
flow.catalog,
label_width,
label_align,
)
let face = flow.font(style)
if lines.length() > 0 {
items.append(
flow.line_items(
lines[0],
box_left,
top + offset + face.ascender_pt(size),
),
)
}
}
_ if first &&
(match icon_image {
Some(img) => {
// the image in the label box, as wide as the label column
// (a raster image no wider than its natural size), no taller
// than the box
let height = bottom - top
let img = match image_info(img.data) {
Some((px_w, px_h, _)) if !img.svg => {
let mut w = @cmp.minimum(px_w.to_double() * 0.75, label_width)
let h = w * px_h.to_double() / px_w.to_double()
if h > height {
w = w * height / h
}
{ ..img, width: Some(w), }
}
_ => { ..img, width: Some(label_width), height: Some(height), }
}
let img = {
..img,
position: match label_align {
Center => "center"
Right => "right"
_ => "left"
},
vposition: if valign == "middle" {
"center"
} else {
valign
},
}
match c.box_image_items(img, box_left, top, label_width, height) {
Some((drawn, _, _)) => {
items.append(drawn)
true
}
None => false
}
}
None => false
}) => ()
_ =>
if first && label_lines.length() > 0 {
// the label is centred vertically on the part of the block on
// its first page, and across its column
// (`height_of_typeset_text label, line_height: 1`: the line gap
// above the text, then its ascender and descender)
let a = label_face.ascender_pt(label_style.size)
let d = label_face.descender_pt(label_style.size)
let gap = label_face.line_gap_pt(label_style.size)
let centring = if valign == "middle" {
@cmp.maximum((bottom - top - (gap + a + d)) / 2.0, 0.0)
} else {
0.0
}
let offset = centring + gap
let x = match label_align {
Center =>
box_left + (label_width - label_lines[0].fit_width) / 2.0
Right => box_left + label_width - label_lines[0].fit_width
_ => box_left
}
items.append(flow.line_items(label_lines[0], x, top + offset + a))
}
}
items
})
})
self.block_margin_bottom(node)
}
///|
fn Converter::convert_example(self : Converter, node : @core.Node) -> Unit {
if node.has_option("collapsible") {
self.convert_collapsible(node)
return
}
self.framed_block(node, "example", caption=true)
}
///|
fn Converter::convert_sidebar(self : Converter, node : @core.Node) -> Unit {
self.framed_block(node, "sidebar", caption=false)
}
///|
/// asciidoctor-pdf's `convert_pass`: the content as it is, in the code font
/// and the base colour, with no box around it.
fn Converter::convert_pass(self : Converter, node : @core.Node) -> Unit {
let flow = self.flow
let (style, line_height) = category_font("code")
let style = { ..style, color: base_font_color(), }
let face = flow.font(style)
flow.typeset(
[{ text: guard_indentation(node.content()), style, anchor: None, }],
line_metrics(line_height, face, style.size),
style,
)
self.block_margin_bottom(node)
}
///|
/// asciidoctor-pdf's `convert_video` and `convert_audio` without a poster
/// image: a play symbol (the `fas-play` icon with `icons=font`), the link
/// to the media and its kind, then the caption below.
fn Converter::convert_media(
self : Converter,
node : @core.Node,
kind : String,
) -> Unit {
let target = node.attr("target").unwrap_or("")
let (path, what) = match node.attr("poster") {
Some("youtube") if kind == "video" =>
("https://www.youtube.com/watch?v=\{target}", "YouTube video")
Some("vimeo") if kind == "video" =>
("https://vimeo.com/\{target}", "Vimeo video")
Some(poster) if kind == "video" && poster != "" => {
unsupported("block", "video poster image", node)
(node.media_uri(target), kind)
}
_ => (node.media_uri(target), kind)
}
self.add_dest(node)
let play = match
(self.doc.attr("icons"), self.flow.catalog.icon_glyph("fas", "play")) {
(Some("font"), Some(glyph)) => "\{glyph}"
_ => "\u{25ba}"
}
self.ink_prose(
"\{play}\u{a0}\{path} (\{what})",
normalize=false,
margin_bottom=0.0,
)
if node.has_title() {
self.ink_caption(node.title().unwrap_or(""), bottom=true)
}
self.block_margin_bottom(node)
}
///|
/// asciidoctor-pdf's `convert_abstract`: its title in the `abstract_title`
/// font, then its paragraphs in the `abstract` font, the first line of the
/// first one in `abstract_first_line_font_style` (and colour); other
/// blocks as usual.
fn Converter::convert_abstract(self : Converter, node : @core.Node) -> Unit {
self.add_dest(node)
self.outdent_section(() => self.ink_abstract(node))
}
///|
fn Converter::ink_abstract(self : Converter, node : @core.Node) -> Unit {
let flow = self.flow
if t_set("abstract_padding") {
unsupported("theme", "abstract_padding", node)
}
if node.has_title() {
let (style, _) = category_font("abstract_title")
flow.margin(heading_margin_top())
self.ink_prose(
node.title().unwrap_or(""),
style~,
align=t_align("abstract_title_text_align", self.text_align),
margin_bottom=heading_margin_bottom(),
line_height=t_num("heading_line_height").unwrap_or(base_line_height()),
)
}
let (style, line_height) = category_font("abstract")
let align = t_align("abstract_text_align", self.text_align)
// the first line's style (`abstract_first_line_font_style`, `_font_color`)
let first = {
let mut first = style
match t_str("abstract_first_line_font_style") {
Some(fs) => {
let (b, i) = font_style_of(fs)
first = match fs {
"normal" =>
{
..first,
bold: false,
italic: false,
font_set: false,
doc_bold: false,
doc_italic: false,
}
"normal_italic" => { ..first, doc_bold: false, doc_italic: true, }
_ =>
{
..first,
doc_bold: first.doc_bold || b,
doc_italic: first.doc_italic || i,
}
}
}
None => ()
}
match t_color("abstract_first_line_font_color") {
Some(color) => first = { ..first, color, }
None => ()
}
match t_str("abstract_first_line_text_transform") {
Some(t) => first = { ..first, text_transform: Some(t), }
None => ()
}
first
}
let has_first = t_set("abstract_first_line_font_style") ||
t_set("abstract_first_line_font_color") ||
t_set("abstract_first_line_text_transform")
let mut first_line = has_first
if node.has_blocks() {
for child in node.blocks {
if child.context != Paragraph {
self.convert_block(child)
continue
}
child.document().playback_attributes(child.attributes)
self.add_dest(child)
if child.has_title() {
self.ink_caption(child.title().unwrap_or(""))
}
self.ink_prose(
child.content(),
style~,
align=text_align_from_roles(child.roles(), query_theme=true).unwrap_or(
align,
),
margin_bottom=0.0,
line_height~,
bottom_gutter=self.gutter(child),
first_line_style?=if first_line { Some(first) } else { None },
// convert_paragraph: the indent goes by the role's alignment, else
// the document's (not the abstract's)
text_indent=paragraph_text_indent(
child,
text_align_from_roles(child.roles(), query_theme=true).unwrap_or(
self.text_align,
),
),
)
first_line = false
// convert_paragraph's insert_margin_bottom
match next_enclosed_block(child) {
Some(next) =>
match t_num("prose_margin_inner") {
Some(inner) if next.context == Paragraph => flow.margin(inner)
_ => flow.margin(prose_margin_bottom())
}
None => ()
}
}
} else {
let text = node.content()
if !text.is_empty() {
let align = role_align(node.roles(), align)
self.ink_prose(
text,
style~,
align~,
margin_bottom=0.0,
line_height~,
first_line_style?=if has_first { Some(first) } else { None },
text_indent=if align is (Justify | Left) {
t_num_or("prose_text_indent", 0.0)
} else {
0.0
},
)
}
}
self.block_margin_bottom(node)
}
///|
/// asciidoctor-pdf's `convert_collapsible`: an example block with the
/// `collapsible` option is its title (default `Details`) after a
/// down-pointing triangle, as a caption, then its content indented by the
/// triangle's width; arranged when it has an id.
fn Converter::convert_collapsible(self : Converter, node : @core.Node) -> Unit {
let marker = "\u{25bc} "
let title = marker + node.title().unwrap_or("Details")
let (caption_style, _) = category_font("caption")
let indent_by = self.flow.width_of(marker, caption_style)
let body = fn(c : Converter) {
c.add_dest(node)
c.flow.tared(() => c.ink_caption(title))
c.flow.indent(indent_by, 0.0, () => c.traverse(node))
}
if !self.flow.at_page_top() &&
(node.id is Some(_) || node.has_option("unbreakable")) {
self.arrange_block(
keep_together=node.has_option("unbreakable"),
node~,
body,
)
} else {
body(self)
}
self.block_margin_bottom(node)
}
///|
/// A padded block in a frame (example, sidebar), in `arrange_block`: its
/// destination, then its caption (example) or title (sidebar, in the
/// `sidebar_title` font), both tared, then its content in the category's
/// font; the frame is the category's (`theme_fill_and_stroke_block`).
fn Converter::framed_block(
self : Converter,
node : @core.Node,
category : String,
caption~ : Bool,
) -> Unit {
let (pad_top, pad_right, pad_bottom, pad_left) = padding_of(category)
let below = caption && caption_below(category)
let (content_style, content_lh) = category_font(category)
self.arrange_block(keep_together=node.has_option("unbreakable"), node~, fn(
c,
) {
let flow = c.flow
c.add_dest(node)
if caption && !below && node.has_title() {
flow.tared(() => c.ink_caption(node.captioned_title(), category~))
}
let start = flow.mark()
let (left, right) = (flow.left, flow.right)
flow.move_down(pad_top)
flow.indent(pad_left, pad_right, fn() {
if !caption && node.has_title() {
let (style, _) = category_font(category + "_title")
flow.tared(() => {
c.ink_prose(
node.title().unwrap_or(""),
style~,
align=match t_str(category + "_title_text_align") {
Some(a) => alignment_of(a)
None => t_align("heading_text_align", c.text_align)
},
margin_bottom=heading_margin_bottom(),
line_height=t_num("heading_line_height").unwrap_or(
base_line_height(),
),
)
})
}
c.push_gutter(node, pad_bottom)
with_font(content_style, content_lh, () => c.traverse(node))
c.pop_gutter()
})
flow.pad_bottom(pad_bottom)
match frame_of(category) {
Some(frame) =>
flow.underlay(start, fn(top, bottom, first, last) {
frame.items(left, right, top, bottom, first, last)
})
None => ()
}
})
if below && node.has_title() {
self.ink_caption(node.captioned_title(), bottom=true, category~)
}
self.block_margin_bottom(node)
}
///|
/// asciidoctor-pdf's `convert_quote_or_verse`: the content in the
/// category's padding and font, a rule down its left side
/// (`_border_left_width`, `_border_color`) or a frame, then the
/// attribution in the `_cite` font.
fn Converter::convert_quote(self : Converter, node : @core.Node) -> Unit {
let category = if node.context == Verse { "verse" } else { "quote" }
let (pad_top, pad_right, pad_bottom, pad_left) = padding_of(category)
let border_color = match tv(category + "_border_color") {
Null => base_border_color()
Transparent => None
v => value_color(v)
}
let left_width = match t_num(category + "_border_left_width") {
Some(w) if w > 0.0 && border_color is Some(_) => Some(w)
_ => None
}
let attribution = node.attr("attribution")
let (style, line_height) = category_font(category)
// arrange_block: the destination, the caption (tared), then the content
// in the padding, whose bottom the last line keeps clear of unless an
// attribution follows
self.arrange_block(keep_together=node.has_option("unbreakable"), node~, fn(
c,
) {
let flow = c.flow
c.add_dest(node)
if node.has_title() {
flow.tared(() => c.ink_caption(node.title().unwrap_or(""), category~))
}
let start = flow.mark()
let (left, right) = (flow.left, flow.right)
flow.move_down(pad_top)
flow.indent(pad_left, pad_right, fn() {
if attribution is None {
c.push_gutter(node, pad_bottom)
} else {
c.gutters.push(None)
}
if node.context == Verse {
c.ink_prose(
guard_indentation(node.content()),
style~,
align=role_align(node.roles(), Left),
margin_bottom=0.0,
line_height~,
normalize=false,
bottom_gutter=c.gutter(node),
)
} else if !node.has_blocks() {
// a quoted paragraph (`[quote]` on a paragraph) carries its text
// itself (`traverse`)
let text = node.content()
if !text.is_empty() {
c.ink_prose(
text,
style~,
align=role_align(node.roles(), c.text_align),
margin_bottom=0.0,
line_height~,
bottom_gutter=c.gutter(node),
)
}
} else {
with_font(style, line_height, () => c.traverse(node))
}
c.pop_gutter()
match attribution {
Some(attribution) => {
flow.margin(block_margin_bottom())
let cite = match node.attr("citetitle") {
Some(title) => "\{attribution}, \{title}"
None => attribution
}
let (cite_style, cite_lh) = theme_font(
base_style(),
base_line_height(),
category + "_cite",
)
c.ink_prose(
"\u{2014} \{cite}",
style=cite_style,
align=Left,
margin_bottom=0.0,
line_height=cite_lh,
normalize=false,
)
}
None => ()
}
})
flow.pad_bottom(pad_bottom)
let frame = if left_width is None { frame_of(category) } else { None }
flow.underlay(start, fn(top, bottom, first, last) {
let items = match frame {
Some(frame) => frame.items(left, right, top, bottom, first, last)
None => []
}
match (left_width, border_color) {
(Some(w), Some(color)) =>
items.push(vrule(left + w / 2.0, top, bottom, w, color))
_ => ()
}
items
})
})
self.block_margin_bottom(node)
}
///|
fn Converter::convert_open(self : Converter, node : @core.Node) -> Unit {
if node.style == Some("abstract") {
self.convert_abstract(node)
return
}
// a titled open block, or one with an id, is arranged: its caption is
// tared, so it moves on with the content after it
if !self.flow.at_page_top() &&
(node.has_title() || node.id is Some(_) || node.has_option("unbreakable")) {
self.arrange_block(keep_together=node.has_option("unbreakable"), node~, fn(
c,
) {
c.add_dest(node)
if node.has_title() {
c.flow.tared(() => c.ink_caption(node.title().unwrap_or("")))
}
c.traverse(node)
})
} else {
self.add_dest(node)
if node.has_title() {
self.ink_caption(node.title().unwrap_or(""))
}
self.traverse(node)
}
}
///|
/// asciidoctor-pdf's `convert_thematic_break`: a rule
/// (`thematic_break_border_*`) in the thematic break padding.
fn Converter::convert_thematic_break(
self : Converter,
node : @core.Node,
) -> Unit {
let flow = self.flow
let (top, right, bottom, left) = match tv("thematic_break_padding") {
Null =>
(
t_num_or("thematic_break_margin_top", 0.0),
0.0,
t_num_or("thematic_break_margin_top", 0.0),
0.0,
)
v => expand_rect(v)
}
flow.move_down(top)
match tv("thematic_break_border_color") {
Transparent | Null => ()
v =>
match value_color(v) {
Some(color) =>
flow.push(
hrule(
flow.left + left,
flow.right - right,
flow.y,
t_num_or("thematic_break_border_width", 0.5),
color,
),
)
None => ()
}
}
flow.move_down(bottom)
self.block_margin_bottom(node)
}
///|
/// Footnotes not yet inked (asciidoctor-pdf's `ink_footnotes`), in the
/// `footnotes` font `footnotes_item_spacing` apart: at the bottom of the
/// page with `footnotes_margin_top: auto`, else that far below the
/// content; after the document, and in a book after each chapter. Labels
/// count from the first footnote inked here.
fn Converter::ink_footnotes(self : Converter, node : @core.Node) -> Unit {
let footnotes = self.doc.footnotes()
if footnotes.length() <= session().rendered_footnotes {
return
}
let pending = footnotes[session().rendered_footnotes:].to_owned()
let flow = self.flow
let is_last = match self.doc.blocks.last() {
Some(last) => physical_equal(last, node)
None => false
}
if node.context == Document || is_last {
flow.margin(block_margin_bottom())
}
let bottom_aligned = match tv("footnotes_margin_top") {
Str("auto") => true
v => {
flow.margin(value_num(v).unwrap_or(0.0))
false
}
}
let (style, line_height) = category_font("footnotes")
let spacing = t_num_or("footnotes_item_spacing", 0.0)
let face = flow.font(style)
let metrics = line_metrics(line_height, face, style.size)
let offset = session().rendered_footnotes
let texts = pending.map(fn(fn_) {
"[\{fn_.index - offset}] \{fn_.text}"
})
if bottom_aligned {
let mut height = 0.0
for text in texts {
height += flow.height_of(parse_formatted(text, style), metrics, style) +
spacing
}
if height < flow.cursor() {
flow.move_down(flow.cursor() - height - 0.0001)
}
}
if self.doc.has_attr("footnotes-title") {
unsupported("attribute", "footnotes-title", node)
}
for text in texts {
self.ink_prose(text, style~, margin_bottom=spacing, line_height~)
}
if !flow.scratch {
// a later reference to one of these (an xref footnote) shows its label,
// with the chapter it is in
if node.context == Section {
let xreftext = node
.xreftext(xrefstyle?=self.doc.attr("xrefstyle"))
.unwrap_or("")
for fn_ in pending {
session().footnote_labels[fn_.index] = "\{fn_.index - offset} - \{xreftext}"
}
}
session().rendered_footnotes = footnotes.length()
}
}