///|
// Image floats (asciidoctor-pdf's `init_float_box` and
// `ink_paragraph_in_float_box`): a block image with `float=left` or
// `float=right` narrower than the content area leaves the space beside it
// to the paragraphs that follow, which wrap into it; text that does not
// fit continues below at the full width.
///|
/// The space beside a floated image, in flow coordinates (y down).
priv struct FloatBox {
page : Int
/// the top of the image (its caption included)
top : Double
/// the bottom of the box: the image's bottom plus the bottom gap, or the
/// page's bottom when that comes first
bottom : Double
left : Double
width : Double
height : Double
/// the bottom gap (`image_float_gap`'s second value)
gap_bottom : Double
}
///|
/// Whether a block wraps around a float (`supports_float_wrapping?`).
fn supports_float_wrapping(node : @core.Node) -> Bool {
node.context == Paragraph
}
///|
/// `init_float_box`: the box beside a `block_width` × `block_height` block
/// floated to `float_to`, inked above the cursor; the cursor goes back up
/// to the block's top.
fn Converter::init_float_box(
self : Converter,
block_width : Double,
block_height : Double,
float_to : String,
) -> Unit {
let flow = self.flow
let (gap_side, gap_bottom) = match tv("image_float_gap") {
Array(items) =>
(
items.get(0).bind(value_num).unwrap_or(12.0),
items.get(1).bind(value_num).unwrap_or(6.0),
)
v =>
match value_num(v) {
Some(g) => (g, g)
None => (12.0, 6.0)
}
}
let float_w = block_width + gap_side
let float_h = block_height + gap_bottom
let left = flow.left + (if float_to == "right" { 0.0 } else { float_w })
let top = flow.y - block_height
let width = flow.width() - float_w
// the cursor at the block's top, as Prawn measures it
let height = @cmp.minimum(flow.bottom - top, float_h)
flow.y = top
self.float_box = Some({
page: flow.page,
top,
bottom: top + height,
left,
width,
height,
gap_bottom,
})
}
///|
/// Prawn's formatted text box with a height (`text_box`): the lines of
/// `fragments` set `width` wide from `x` (the first one `text_indent`
/// further in, Prawn's `indent_paragraphs`), the first hanging from the
/// cursor plus the line padding, as many as fit in `height`. Returns the
/// fragments that did not fit and the baseline of the last line inked
/// (None when none was); the cursor does not move.
fn Flow::typeset_box(
self : Flow,
fragments : Array[Fragment],
metrics : LineMetrics,
base : Style,
align~ : Alignment,
x~ : Double,
width~ : Double,
height~ : Double,
text_indent~ : Double,
) -> (Array[Fragment], Double?) {
self.last_line = None
let base_face = self.font(base)
let top = self.y + metrics.padding_top
let mut remaining = self.catalog.apply_fallbacks(fragments)
let mut baseline = 0.0
let mut placed = 0
let mut last : Double? = None
while !remaining.is_empty() {
let indent = if placed == 0 { text_indent } else { 0.0 }
let (line, rest) = split_first_line(
remaining,
remaining,
self.catalog,
width - indent,
align,
)
let (a, d, h) = line_extent(line, base_face, base.size, false)
let next = if placed == 0 { a } else { baseline + h + metrics.leading }
if next + d > height + 0.0001 {
break
}
baseline = next
self.ink_line(line, x + indent, top + baseline)
self.last_line = Some((line, self.page, top + baseline))
self.last_line_first = placed == 0
last = Some(top + baseline)
placed += 1
remaining = rest
}
(remaining, last)
}
///|
/// `ink_paragraph_in_float_box`: `node`'s text set in the float box from
/// the cursor down; what does not fit continues below at the full width.
/// The box stays for the next paragraph while there is room left in it.
fn Converter::ink_paragraph_in_float_box(
self : Converter,
node : @core.Node,
float_box : FloatBox,
style : Style,
align : Alignment,
line_height : Double,
text_indent : Double,
) -> Unit {
let flow = self.flow
self.float_box = None
let face = flow.font(style)
let metrics = line_metrics(line_height, face, style.size)
// allocate the space of at least one empty line below the block
let line_height_length = line_height * style.size +
metrics.leading +
metrics.padding_top
let start_page = float_box.page
let start_y = flow.y
let block_bottom = float_box.bottom - float_box.gap_bottom
let height = @cmp.minimum(
flow.cursor(),
float_box.height - (start_y - float_box.top) + line_height_length,
)
let final_gap = face.descender_pt(style.size) + metrics.padding_bottom
let (overflow, end_y) = flow.typeset_box(
parse_formatted(node.content(), style),
metrics,
style,
align~,
x=float_box.left,
width=float_box.width,
height~,
text_indent~,
)
match end_y {
Some(y) => flow.y = y + final_gap
None => ()
}
let next = next_enclosed_block(node)
let insert_margin_bottom = () => {
match next {
Some(next) =>
match t_num("prose_margin_inner") {
Some(inner) if next.context == Paragraph => flow.margin(inner)
_ => flow.margin(prose_margin_bottom())
}
None => ()
}
}
if overflow.is_empty() {
match next {
Some(next_block) if supports_float_wrapping(next_block) => {
insert_margin_bottom()
if flow.page == start_page && flow.y < start_y + height {
self.float_box = Some(float_box)
}
}
_ =>
// the cursor after the last line (`end_cursor` less the final gap)
match end_y {
Some(_) if flow.y < block_bottom => {
flow.y = block_bottom
self.block_margin_bottom(node)
}
_ => insert_margin_bottom()
}
}
} else {
let indent = if end_y is None {
if start_y < float_box.bottom {
flow.y = float_box.bottom
}
text_indent
} else {
0.0
}
flow.typeset(overflow, metrics, style, align~, text_indent=indent)
insert_margin_bottom()
}
}
///|
/// A block image's `float` (`left` or `right`), when it has one.
fn image_float(node : @core.Node) -> String? {
match node.attr("float") {
Some("left" | "right") as f => f
_ => None
}
}
///|
/// The caption's `max_width` of a block image: `image_caption_max_width`,
/// `fit-content` when the image floats.
fn image_caption_max_width(node : @core.Node) -> String? {
let max_width = value_str(tv("image_caption_max_width"))
if image_float(node) is Some(_) &&
!(max_width is Some(m) && m.has_prefix("fit-content")) {
Some("fit-content")
} else {
max_width
}
}
///|
/// The alignment of an image caption (`image_caption_align`, else
/// `caption_align`; `inherit` takes the image's).
fn Converter::image_caption_align(
self : Converter,
block_align : Alignment,
) -> Alignment {
let value = match tv("image_caption_align") {
Null => tv("caption_align")
v => v
}
match value_str(value) {
Some("inherit") => block_align
Some(a) => alignment_of(a)
None => self.text_align
}
}
///|
/// The height a block image's caption (below it) takes, its inside margin
/// included, measured as wide as `ink_caption` sets it for a block
/// `block_width` wide (the explicit width, when there is one).
fn Converter::image_caption_height(
self : Converter,
node : @core.Node,
alignment : String,
block_width : Double?,
) -> Double {
if !node.has_title() {
return 0.0
}
let flow = self.flow
let style = Style::new(size=caption_font_size(), doc_italic=true)
let block_align = alignment_of(alignment)
let (left, right) = caption_indents(
flow.width(),
self.image_caption_align(block_align),
Some(block_align),
block_width,
image_caption_max_width(node),
)
flow.indent(left, right, () => {
flow.height_of(
parse_formatted(node.captioned_title(), style, normalize=false),
line_metrics(base_line_height(), flow.font(style), caption_font_size()),
style,
)
}) +
caption_margin_inside()
}
///|
/// Whether block image captions go above the image (`image_caption_end:
/// top`) rather than below it.
fn image_caption_on_top() -> Bool {
t_str("image_caption_end") == Some("top")
}
///|
/// A block image's caption, when it has one and it goes at this end
/// (`top`: above the image), as wide as `max_width` allows for an image
/// `rendered_w` wide.
fn Converter::ink_image_caption(
self : Converter,
node : @core.Node,
alignment : String,
rendered_w : Double,
top~ : Bool,
) -> Unit {
if node.has_title() && image_caption_on_top() == top {
self.ink_caption(
node.captioned_title(),
bottom=!top,
category="image",
block_align=alignment_of(alignment),
block_width=rendered_w,
max_width?=image_caption_max_width(node),
)
}
}
///|
/// A block image's caption below it, then its bottom margin, or the float
/// box beside it when it floats and a paragraph follows (the end of
/// asciidoctor-pdf's `convert_image`).
fn Converter::finish_image(
self : Converter,
node : @core.Node,
alignment : String,
rendered_w : Double,
rendered_h : Double,
caption_height : Double,
) -> Unit {
self.ink_image_caption(node, alignment, rendered_w, top=false)
if self.pinned {
return
}
match (image_float(node), next_enclosed_block(node)) {
(Some(float_to), Some(next)) if supports_float_wrapping(next) &&
rendered_w < self.flow.width() =>
self.init_float_box(rendered_w, rendered_h + caption_height, float_to)
_ => self.block_margin_bottom(node)
}
}