use pdfboss_write::{Color, Standard14};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Element {
Body,
H1,
H2,
H3,
H4,
H5,
H6,
P,
Code,
Pre,
Blockquote,
Ul,
Ol,
Li,
Table,
Th,
Td,
A,
Del,
Hr,
}
impl Element {
pub const ALL: [Element; 20] = [
Element::Body,
Element::H1,
Element::H2,
Element::H3,
Element::H4,
Element::H5,
Element::H6,
Element::P,
Element::Code,
Element::Pre,
Element::Blockquote,
Element::Ul,
Element::Ol,
Element::Li,
Element::Table,
Element::Th,
Element::Td,
Element::A,
Element::Del,
Element::Hr,
];
pub fn name(self) -> &'static str {
match self {
Element::Body => "body",
Element::H1 => "h1",
Element::H2 => "h2",
Element::H3 => "h3",
Element::H4 => "h4",
Element::H5 => "h5",
Element::H6 => "h6",
Element::P => "p",
Element::Code => "code",
Element::Pre => "pre",
Element::Blockquote => "blockquote",
Element::Ul => "ul",
Element::Ol => "ol",
Element::Li => "li",
Element::Table => "table",
Element::Th => "th",
Element::Td => "td",
Element::A => "a",
Element::Del => "del",
Element::Hr => "hr",
}
}
pub fn from_name(name: &str) -> Option<Element> {
Element::ALL
.into_iter()
.find(|element| element.name() == name)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FontFamily {
Helvetica,
Times,
Courier,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Align {
Left,
Center,
Right,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Decoration {
None,
Underline,
LineThrough,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum FontSize {
Pt(f32),
Em(f32),
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Edges {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Declared {
pub family: Option<FontFamily>,
pub size: Option<FontSize>,
pub bold: Option<bool>,
pub italic: Option<bool>,
pub color: Option<Color>,
pub background: Option<Color>,
pub margin: [Option<f32>; 4],
pub padding: [Option<f32>; 4],
pub line_height: Option<f32>,
pub align: Option<Align>,
pub decoration: Option<Decoration>,
}
impl Declared {
pub fn merge(&mut self, other: &Declared) {
if other.family.is_some() {
self.family = other.family;
}
if other.size.is_some() {
self.size = other.size;
}
if other.bold.is_some() {
self.bold = other.bold;
}
if other.italic.is_some() {
self.italic = other.italic;
}
if other.color.is_some() {
self.color = other.color;
}
if other.background.is_some() {
self.background = other.background;
}
for side in 0..4 {
if other.margin[side].is_some() {
self.margin[side] = other.margin[side];
}
if other.padding[side].is_some() {
self.padding[side] = other.padding[side];
}
}
if other.line_height.is_some() {
self.line_height = other.line_height;
}
if other.align.is_some() {
self.align = other.align;
}
if other.decoration.is_some() {
self.decoration = other.decoration;
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextStyle {
pub family: FontFamily,
pub size: f32,
pub bold: bool,
pub italic: bool,
pub color: Color,
pub line_height: f32,
pub align: Align,
pub decoration: Decoration,
}
impl TextStyle {
pub fn base() -> TextStyle {
TextStyle {
family: FontFamily::Helvetica,
size: 11.0,
bold: false,
italic: false,
color: Color::BLACK,
line_height: 1.4,
align: Align::Left,
decoration: Decoration::None,
}
}
pub fn apply(&self, d: &Declared) -> TextStyle {
TextStyle {
family: d.family.unwrap_or(self.family),
size: match d.size {
Some(FontSize::Pt(pt)) => pt,
Some(FontSize::Em(em)) => em * self.size,
None => self.size,
},
bold: d.bold.unwrap_or(self.bold),
italic: d.italic.unwrap_or(self.italic),
color: d.color.unwrap_or(self.color),
line_height: d.line_height.unwrap_or(self.line_height),
align: d.align.unwrap_or(self.align),
decoration: d.decoration.unwrap_or(self.decoration),
}
}
pub fn font(&self) -> Standard14 {
match (self.family, self.bold, self.italic) {
(FontFamily::Helvetica, false, false) => Standard14::Helvetica,
(FontFamily::Helvetica, true, false) => Standard14::HelveticaBold,
(FontFamily::Helvetica, false, true) => Standard14::HelveticaOblique,
(FontFamily::Helvetica, true, true) => Standard14::HelveticaBoldOblique,
(FontFamily::Times, false, false) => Standard14::TimesRoman,
(FontFamily::Times, true, false) => Standard14::TimesBold,
(FontFamily::Times, false, true) => Standard14::TimesItalic,
(FontFamily::Times, true, true) => Standard14::TimesBoldItalic,
(FontFamily::Courier, false, false) => Standard14::Courier,
(FontFamily::Courier, true, false) => Standard14::CourierBold,
(FontFamily::Courier, false, true) => Standard14::CourierOblique,
(FontFamily::Courier, true, true) => Standard14::CourierBoldOblique,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn element_from_name_maps_every_selector() {
assert_eq!(Element::from_name("h1"), Some(Element::H1));
assert_eq!(Element::from_name("blockquote"), Some(Element::Blockquote));
assert_eq!(Element::from_name("div"), None);
for element in Element::ALL {
assert!(Element::from_name(element.name()).is_some());
}
}
#[test]
fn font_resolution_covers_all_twelve_faces() {
let style = TextStyle {
family: FontFamily::Times,
bold: true,
italic: true,
..TextStyle::base()
};
assert_eq!(style.font(), Standard14::TimesBoldItalic);
let style = TextStyle {
family: FontFamily::Courier,
..TextStyle::base()
};
assert_eq!(style.font(), Standard14::Courier);
}
#[test]
fn apply_overlays_only_declared_fields() {
let declared = Declared {
size: Some(FontSize::Em(2.0)),
bold: Some(true),
..Declared::default()
};
let applied = TextStyle::base().apply(&declared);
assert_eq!(applied.size, 22.0);
assert!(applied.bold);
assert_eq!(applied.family, FontFamily::Helvetica);
}
#[test]
fn merge_keeps_earlier_fields_the_later_rule_leaves_unset() {
let mut first = Declared {
bold: Some(true),
..Declared::default()
};
let second = Declared {
italic: Some(true),
..Declared::default()
};
first.merge(&second);
assert_eq!(first.bold, Some(true));
assert_eq!(first.italic, Some(true));
}
}