///|
// The theme of the conversion (asciidoctor-pdf's `@theme`): loaded by the
// theme package from the document's `pdf-theme` (default: the bundled
// default theme), prepared as `prepare_theme` does, and read through the
// accessors below; each styling value the converter uses is a function
// named after its theme key.
///|
/// The theme of the conversion in progress.
let current_theme : Ref[@theme.Theme] = { val: @theme.Theme::new(), }
///|
/// `self`, else `other` (Ruby's `a || b` on optional values).
fn[T] Option::or_some(self : T?, other : T?) -> T? {
match self {
Some(_) => self
None => other
}
}
///|
/// The value of a theme key (nil when absent).
fn tv(key : String) -> @theme.Value {
current_theme.val[key]
}
///|
/// A numeric theme value.
fn value_num(v : @theme.Value) -> Double? {
match v {
Int(i) => Some(i.to_double())
Float(f) => Some(f)
_ => None
}
}
///|
fn t_num(key : String) -> Double? {
value_num(tv(key))
}
///|
fn t_num_or(key : String, default : Double) -> Double {
t_num(key).unwrap_or(default)
}
///|
/// A string theme value (a number as Ruby prints it).
fn value_str(v : @theme.Value) -> String? {
match v {
Str(s) | Hex(s) => Some(s)
Int(_) | Float(_) | Bool(_) | Transparent => Some(v.to_ruby_s())
_ => None
}
}
///|
fn t_str(key : String) -> String? {
value_str(tv(key))
}
///|
/// Whether the key is set to something other than nil or false.
fn t_set(key : String) -> Bool {
tv(key).truthy()
}
///|
/// A theme color: a hex color (`HexColorValue`, or a plain six-digit
/// string as the base theme has), a CMYK color (approximated in RGB, which
/// is all the renderer has), None for nil and `transparent`.
fn value_color(v : @theme.Value) -> Color? {
match v {
Hex(s) | Str(s) => {
let hex = if s.has_prefix("#") { s[1:].to_owned() } else { s }
let hex = if hex.length() == 3 {
let sb = StringBuilder()
for c in hex {
sb.write_char(c)
sb.write_char(c)
}
sb.to_string()
} else {
hex
}
if hex.length() != 6 {
return None
}
parse_int_radix(hex, 16).map(rgb_hex)
}
Cmyk(items) => {
let c = items.map(x => value_num(x).unwrap_or(0.0) / 100.0)
let k = 1.0 - c[3]
let channel = (x : Double) => ((1.0 - x) * k * 255.0).round().to_int()
Some(Color::rgb(channel(c[0]), channel(c[1]), channel(c[2])))
}
Int(_) => value_color(Str(v.to_ruby_s()))
_ => None
}
}
///|
fn t_color(key : String) -> Color? {
value_color(tv(key))
}
///|
fn t_color_or(key : String, default : Color) -> Color {
t_color(key).unwrap_or(default)
}
///|
/// A font style (`bold`, `italic`, `bold_italic`, `normal`,
/// `normal_italic`) as (bold, italic).
fn font_style_of(style : String) -> (Bool, Bool) {
match style {
"bold" => (true, false)
"italic" | "normal_italic" => (false, true)
"bold_italic" => (true, true)
_ => (false, false)
}
}
///|
/// A font size: points, or relative to `current` (`1.2em`, `80%`) or to
/// the root font size (`1.2rem`), as asciidoctor-pdf's `font_size` reads
/// it.
fn resolve_font_size(v : @theme.Value, current : Double) -> Double? {
match v {
Int(_) | Float(_) => value_num(v)
Str(s) =>
if s.has_suffix("rem") {
Some(base_font_size() * @theme.ruby_string_to_f(s))
} else if s.has_suffix("em") {
Some(current * @theme.ruby_string_to_f(s))
} else if s.has_suffix("%") {
Some(current * (@theme.ruby_string_to_f(s) / 100.0))
} else {
Some(@theme.ruby_string_to_f(s))
}
_ => None
}
}
///|
/// A theme value as a length in points (`str_to_pt` for a string).
fn value_pt(v : @theme.Value) -> Double? {
match v {
Int(_) | Float(_) => value_num(v)
Str(s) => Some(@theme.str_to_pt(s))
_ => None
}
}
///|
/// asciidoctor-pdf's `expand_rect_values` (padding, margin): one value for
/// all sides, [vertical, horizontal], [top, horizontal, bottom] or [top,
/// right, bottom, left]; nil entries count as 0.
fn expand_rect(v : @theme.Value) -> (Double, Double, Double, Double) {
match v {
Array(items) => {
let at = (i : Int) => items.get(i).bind(value_pt).unwrap_or(0.0)
match items.length() {
0 => (0.0, 0.0, 0.0, 0.0)
1 => (at(0), at(0), at(0), at(0))
2 => (at(0), at(1), at(0), at(1))
3 => (at(0), at(1), at(2), at(1))
_ => (at(0), at(1), at(2), at(3))
}
}
other => {
let x = value_pt(other).unwrap_or(0.0)
(x, x, x, x)
}
}
}
///|
/// The theme's padding for a category (`_padding`).
fn t_padding(key : String) -> (Double, Double, Double, Double) {
expand_rect(tv(key))
}
///|
/// The page geometry of the conversion: size and margins (top, right,
/// bottom, left), asciidoctor-pdf's `build_pdf_options`.
priv struct PageSetup {
width : Double
height : Double
margin_top : Double
margin_right : Double
margin_bottom : Double
margin_left : Double
}
///|
let page_setup : Ref[PageSetup] = {
val: {
width: 595.28,
height: 841.89,
margin_top: 36.0,
margin_right: 48.24,
margin_bottom: 48.24,
margin_left: 48.24,
},
}
///|
fn page_width() -> Double {
page_setup.val.width
}
///|
fn page_height() -> Double {
page_setup.val.height
}
///|
/// PDF::Core's named page sizes (`PageGeometry::SIZES`), portrait.
let named_page_sizes : Array[(String, Double, Double)] = [
("4A0", 4767.87, 6740.79),
("2A0", 3370.39, 4767.87),
("A0", 2383.94, 3370.39),
("A1", 1683.78, 2383.94),
("A2", 1190.55, 1683.78),
("A3", 841.89, 1190.55),
("A4", 595.28, 841.89),
("A5", 419.53, 595.28),
("A6", 297.64, 419.53),
("A7", 209.76, 297.64),
("A8", 147.40, 209.76),
("A9", 104.88, 147.40),
("A10", 73.70, 104.88),
("B0", 2834.65, 4008.19),
("B1", 2004.09, 2834.65),
("B2", 1417.32, 2004.09),
("B3", 1000.63, 1417.32),
("B4", 708.66, 1000.63),
("B5", 498.90, 708.66),
("B6", 354.33, 498.90),
("B7", 249.45, 354.33),
("B8", 175.75, 249.45),
("B9", 124.72, 175.75),
("B10", 87.87, 124.72),
("C0", 2599.37, 3676.54),
("C1", 1836.85, 2599.37),
("C2", 1298.27, 1836.85),
("C3", 918.43, 1298.27),
("C4", 649.13, 918.43),
("C5", 459.21, 649.13),
("C6", 323.15, 459.21),
("C7", 229.61, 323.15),
("C8", 161.57, 229.61),
("C9", 113.39, 161.57),
("C10", 79.37, 113.39),
("RA0", 2437.80, 3458.27),
("RA1", 1729.13, 2437.80),
("RA2", 1218.90, 1729.13),
("RA3", 864.57, 1218.90),
("RA4", 609.45, 864.57),
("SRA0", 2551.18, 3628.35),
("SRA1", 1814.17, 2551.18),
("SRA2", 1275.59, 1814.17),
("SRA3", 907.09, 1275.59),
("SRA4", 637.80, 907.09),
("EXECUTIVE", 521.86, 756.00),
("FOLIO", 612.00, 936.00),
("LEGAL", 612.00, 1008.00),
("LETTER", 612.00, 792.00),
("TABLOID", 792.00, 1224.00),
]
///|
/// `MeasurementPartsRx`: `^(\d+(?:\.\d+)?)(in|mm|cm|p[txc])?$`, in points
/// truncated to four decimals (as PDF::Core keeps them); None otherwise.
fn measurement_pt(s : String) -> Double? {
let (number, units) = if s.length() >= 2 &&
["in", "mm", "cm", "pt", "px", "pc"].contains(s[s.length() - 2:].to_owned()) {
(s[:s.length() - 2].to_owned(), s[s.length() - 2:].to_owned())
} else {
(s, "")
}
let parts = number.split(".").map(p => p.to_owned()).collect()
let digits = (p : String) => {
p != "" && p.iter().all(c => c >= '0' && c <= '9')
}
let ok = match parts {
[a] => digits(a)
[a, b] => digits(a) && digits(b)
_ => false
}
guard ok else { return None }
let pt = @theme.to_pt(@theme.ruby_string_to_f(number), units).unwrap_or(0.0)
Some((pt * 10000.0).floor() / 10000.0)
}
///|
/// asciidoctor-pdf's `resolve_page_margin`: 1 to 4 margins from an array,
/// a number, or a string (`[a, b]` or one measurement).
fn resolve_page_margin(v : @theme.Value) -> Array[Double]? {
match v {
Array([]) | Null | Str("") => None
Array(items) =>
Some(
items[:@cmp.minimum(items.length(), 4)]
.iter()
.map(x => {
match x {
Int(_) | Float(_) => value_num(x).unwrap_or(0.0)
other => @theme.str_to_pt(other.to_ruby_s())
}
})
.collect(),
)
Int(_) | Float(_) => Some([value_num(v).unwrap_or(0.0)])
Str(s) =>
if s.has_prefix("[") && s.has_suffix("]") {
let inner = s[1:s.length() - 1].trim_end(chars=" \t\n").to_owned()
if inner == "" {
return None
}
let parts = inner.split(",").map(p => p.to_owned()).collect()
let parts = parts[:@cmp.minimum(parts.length(), 4)]
Some(
parts.map(p => @theme.str_to_pt(p.trim_end(chars=" \t\n").to_owned())),
)
} else {
Some([@theme.str_to_pt(s)])
}
_ => None
}
}
///|
/// The page size, layout and margins of `doc` (`build_pdf_options`): the
/// `pdf-page-size`, `pdf-page-layout` and `pdf-page-margin` attributes,
/// else the theme's `page_size`, `page_layout` and `page_margin`.
fn resolve_page_setup(doc : @core.Node) -> PageSetup {
let margin = match doc.attr("pdf-page-margin") {
Some(m) => resolve_page_margin(Str(m))
None => resolve_page_margin(tv("page_margin"))
}
let (top, right, bottom, left) = expand_rect(
Array(margin.unwrap_or([36.0]).map(m => (Float(m) : @theme.Value))),
)
// the size: a name, or [width, height]
let size : @theme.Value = match doc.attr("pdf-page-size") {
Some(s) if s != "" => {
let s = s.trim().to_owned()
if s.has_prefix("[") && s.has_suffix("]") && s.contains(",") {
let inner = s[1:s.length() - 1].to_owned()
match inner.find(",") {
Some(i) =>
Array([
Str(inner[:i].trim().to_owned()),
Str(inner[i + 1:].trim().to_owned()),
])
None => Str(s)
}
} else if s.contains("x") &&
s.find("x") is Some(i) &&
measurement_pt(s[:i].trim().to_owned()) is Some(_) {
Array([Str(s[:i].trim().to_owned()), Str(s[i + 1:].trim().to_owned())])
} else {
Str(s)
}
}
_ => tv("page_size")
}
let mut width = 595.28
let mut height = 841.89
match size {
Str(name) =>
for entry in named_page_sizes {
if entry.0 == name.to_upper() {
width = entry.1
height = entry.2
}
}
Array(dims) if dims.length() > 0 => {
let dims = if dims.length() == 1 { [dims[0], dims[0]] } else { dims }
let resolved = dims[:2]
.iter()
.map(d => {
match d {
Int(_) | Float(_) =>
value_num(d).bind(x => if x > 0.0 { Some(x) } else { None })
Str(s) =>
measurement_pt(s).bind(x => if x > 0.0 { Some(x) } else { None })
_ => None
}
})
.collect()
if resolved.iter().all(x => x is Some(_)) {
width = resolved[0].unwrap()
height = resolved[1].unwrap()
}
}
_ => ()
}
let layout = match doc.attr("pdf-page-layout") {
Some(l) if l != "" => Some(l)
_ => t_str("page_layout")
}
if layout == Some("landscape") {
let w = width
width = height
height = w
}
{
width,
height,
margin_top: top,
margin_right: right,
margin_bottom: bottom,
margin_left: left,
}
}
///|
/// asciidoctor-pdf's `theme_font category`: the font settings of a theme
/// category on top of the current ones (`base`, and the base line height
/// `line_height`): its family, size (points, or relative to the current
/// size), style (replacing the current one), colour, line height and text
/// transform, each when the theme sets it.
fn theme_font(
base : Style,
line_height : Double,
category : String,
) -> (Style, Double) {
let mut s = base
match t_str(category + "_font_family") {
Some(family) => s = { ..s, family, }
None => ()
}
match resolve_font_size(tv(category + "_font_size"), s.size) {
Some(size) => s = { ..s, size, }
None => ()
}
match t_str(category + "_font_style") {
Some(style) => {
let (doc_bold, doc_italic) = font_style_of(style)
s = { ..s, doc_bold, doc_italic, }
}
None => ()
}
match t_color(category + "_font_color") {
Some(color) => s = { ..s, color, }
None => ()
}
match t_str(category + "_text_transform") {
Some("none") => s = { ..s, text_transform: None, }
Some(transform) => s = { ..s, text_transform: Some(transform), }
None => ()
}
(s, t_num(category + "_line_height").unwrap_or(line_height))
}
///|
/// `theme_font :heading, level: n`: the `heading_h_*` keys, else the
/// `heading_*` ones; the family defaults to the base font's, the size to
/// the root size, and the style to normal.
fn theme_font_heading(
base : Style,
line_height : Double,
level : Int,
category? : String = "heading",
) -> (Style, Double) {
let key = (name : String) => {
match tv("\{category}_h\{level}_\{name}") {
Null => tv("\{category}_\{name}")
v => v
}
}
let family = value_str(key("font_family")).unwrap_or(base_font_family())
let size = match key("font_size") {
Null => base_font_size()
v => resolve_font_size(v, base.size).unwrap_or(base_font_size())
}
let (doc_bold, doc_italic) = font_style_of(
value_str(key("font_style")).unwrap_or("normal"),
)
let color = value_color(key("font_color")).unwrap_or(base.color)
let transform = match value_str(key("text_transform")) {
Some("none") => None
Some(t) => Some(t)
None => base.text_transform
}
(
{
..base,
family,
size,
doc_bold,
doc_italic,
color,
text_transform: transform,
},
value_num(key("line_height")).unwrap_or(line_height),
)
}
///|
/// A block's text alignment from its roles (`text-left` ...) or, with
/// `query_theme`, the theme's `role__text_align` of its roles (last
/// first); None when neither says (`resolve_text_align_from_role`).
fn text_align_from_roles(
roles : Array[String],
query_theme? : Bool = false,
) -> Alignment? {
for i = roles.length() - 1; i >= 0; i = i - 1 {
match roles[i] {
"text-left" => return Some(Left)
"text-right" => return Some(Right)
"text-center" => return Some(Center)
"text-justify" => return Some(Justify)
_ => ()
}
}
if query_theme {
for i = roles.length() - 1; i >= 0; i = i - 1 {
match t_str("role_\{roles[i]}_text_align") {
Some(align) => return Some(alignment_of(align))
None => ()
}
}
}
None
}
///|
/// A theme alignment (`left`, `center`, `right`, `justify`).
fn alignment_of(value : String) -> Alignment {
match value {
"center" => Center
"right" => Right
"justify" => Justify
_ => Left
}
}
///|
/// A theme key's alignment, else `default`.
fn t_align(key : String, default : Alignment) -> Alignment {
match t_str(key) {
Some(value) => alignment_of(value)
None => default
}
}
// ---------------------------------------------------------------------------
// the keys the converter reads, with the defaults `prepare_theme` gives
// them
///|
fn base_font_family() -> String {
t_str("base_font_family").unwrap_or("Helvetica")
}
///|
/// The size of captions (`theme_font :caption`).
fn caption_font_size() -> Double {
theme_font(base_style(), base_line_height(), "caption").0.size
}
///|
/// `caption_margin_inside`
fn caption_margin_inside() -> Double {
t_num_or("caption_margin_inside", 0.0)
}
///|
/// The root font size (`@root_font_size`).
fn base_font_size() -> Double {
t_num_or("base_font_size", 12.0)
}
///|
fn base_font_color() -> Color {
t_color_or("base_font_color", rgb_hex(0))
}
///|
/// `base_font_style` as (bold, italic).
fn base_font_style() -> (Bool, Bool) {
font_style_of(t_str("base_font_style").unwrap_or("normal"))
}
///|
/// The font blocks are set in while a category's `theme_font` is in force
/// (the admonition, example, sidebar or quote content), with its base line
/// height; None outside them.
let font_scope : Ref[(Style, Double)?] = { val: None, }
///|
/// The current font (asciidoctor-pdf's current font and `@font_color`):
/// the base font, or the enclosing category's.
fn base_style() -> Style {
match font_scope.val {
Some((style, _)) => style
None => Style::new()
}
}
///|
/// The current base line height (`@base_line_height`).
fn base_line_height() -> Double {
match font_scope.val {
Some((_, line_height)) => line_height
None => t_num_or("base_line_height", 1.0)
}
}
///|
/// Run `body` in the font `style` with the base line height `line_height`
/// (`theme_font` around a block's content).
fn with_font(style : Style, line_height : Double, body : () -> Unit) -> Unit {
let saved = font_scope.val
font_scope.val = Some((style, line_height))
body()
font_scope.val = saved
}
///|
/// `base_border_color` (`transparent` is none).
fn base_border_color() -> Color? {
t_color("base_border_color")
}
///|
/// `page_background_color` (`@page_bg_color`).
fn page_background_color() -> Color {
t_color_or("page_background_color", rgb_hex(0xFFFFFF))
}
///|
fn block_margin_bottom() -> Double {
t_num_or("block_margin_bottom", 0.0)
}
///|
fn prose_margin_bottom() -> Double {
t_num_or("prose_margin_bottom", 0.0)
}
///|
fn heading_margin_top() -> Double {
t_num_or("heading_margin_top", 0.0)
}
///|
fn heading_margin_bottom() -> Double {
t_num_or("heading_margin_bottom", 0.0)
}
///|
fn list_indent() -> Double {
t_num_or("list_indent", 0.0)
}
///|
fn list_item_spacing() -> Double {
t_num_or("list_item_spacing", 0.0)
}
///|
fn description_list_term_spacing() -> Double {
t_num_or("description_list_term_spacing", 0.0)
}
///|
fn description_list_description_indent() -> Double {
t_num_or("description_list_description_indent", 0.0)
}
///|
fn admonition_column_rule_width() -> Double {
t_num_or("admonition_column_rule_width", 0.0)
}
///|
/// `admonition_padding`: [top, right, bottom, left]
fn admonition_padding() -> (Double, Double, Double, Double) {
t_padding("admonition_padding")
}
///|
/// `toc_indent`
fn toc_indent() -> Double {
t_num_or("toc_indent", 0.0)
}
///|
/// `index_columns` (2 when the theme sets none)
fn index_columns() -> Int {
t_num_or("index_columns", 2.0).to_int()
}
///|
/// `index_column_gap` (the root font size when the theme sets none)
fn index_column_gap() -> Double {
t_num_or("index_column_gap", base_font_size())
}