use std::borrow::Cow;
fn attr<'a>(attrs: &'a [(String, Option<String>)], key: &str) -> Option<&'a str> {
attrs
.iter()
.find(|(k, _)| k == key)
.and_then(|(_, v)| v.as_deref())
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Role {
#[default]
Body,
Heading(u8),
Code,
Link,
Mark(Option<MarkColor>),
ListMarker,
ListIndent,
QuoteGutter,
Rule,
Delimiter,
Image,
Math,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MarkColor {
Red,
Orange,
Yellow,
Green,
Blue,
Purple,
Brown,
}
impl MarkColor {
pub fn from_attrs(attrs: &[(String, Option<String>)]) -> Option<Self> {
attr(attrs, "data-color").and_then(Self::from_attr)
}
pub fn from_attr(value: &str) -> Option<Self> {
Some(match value {
"red" => Self::Red,
"orange" => Self::Orange,
"yellow" => Self::Yellow,
"green" => Self::Green,
"blue" => Self::Blue,
"purple" => Self::Purple,
"brown" => Self::Brown,
_ => return None,
})
}
pub const fn name(self) -> &'static str {
match self {
Self::Red => "red",
Self::Orange => "orange",
Self::Yellow => "yellow",
Self::Green => "green",
Self::Blue => "blue",
Self::Purple => "purple",
Self::Brown => "brown",
}
}
pub const fn index(self) -> usize {
match self {
Self::Red => 0,
Self::Orange => 1,
Self::Yellow => 2,
Self::Green => 3,
Self::Blue => 4,
Self::Purple => 5,
Self::Brown => 6,
}
}
pub const ALL: [Self; 7] = [
Self::Red,
Self::Orange,
Self::Yellow,
Self::Green,
Self::Blue,
Self::Purple,
Self::Brown,
];
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Align {
Center,
Right,
Justify,
}
impl Align {
pub fn from_attrs(attrs: &[(String, Option<String>)]) -> Option<Self> {
attr(attrs, "class")?
.split_whitespace()
.find_map(Self::from_token)
}
pub fn from_token(token: &str) -> Option<Self> {
Some(match token {
"center" => Self::Center,
"right" => Self::Right,
"justify" => Self::Justify,
_ => return None,
})
}
pub const fn name(self) -> &'static str {
match self {
Self::Center => "center",
Self::Right => "right",
Self::Justify => "justify",
}
}
pub const fn index(self) -> usize {
match self {
Self::Center => 0,
Self::Right => 1,
Self::Justify => 2,
}
}
pub const ALL: [Self; 3] = [Self::Center, Self::Right, Self::Justify];
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LineSpacing {
OneFifteen,
OneHalf,
Double,
}
impl LineSpacing {
pub fn from_attrs(attrs: &[(String, Option<String>)]) -> Option<Self> {
attr(attrs, "data-line-height").and_then(Self::from_attr)
}
pub fn from_attr(value: &str) -> Option<Self> {
Some(match value {
"1.15" => Self::OneFifteen,
"1.5" => Self::OneHalf,
"2" => Self::Double,
_ => return None,
})
}
pub const fn name(self) -> &'static str {
match self {
Self::OneFifteen => "1.15",
Self::OneHalf => "1.5",
Self::Double => "2",
}
}
pub const fn ratio(self) -> f32 {
match self {
Self::OneFifteen => 1.15,
Self::OneHalf => 1.5,
Self::Double => 2.0,
}
}
pub const fn index(self) -> usize {
match self {
Self::OneFifteen => 0,
Self::OneHalf => 1,
Self::Double => 2,
}
}
pub const ALL: [Self; 3] = [Self::OneFifteen, Self::OneHalf, Self::Double];
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SizeStep {
XxSmall,
XSmall,
Small,
Large,
XLarge,
XxLarge,
XxxLarge,
}
impl SizeStep {
pub fn from_attrs(attrs: &[(String, Option<String>)]) -> Option<Self> {
attr(attrs, "data-size").and_then(Self::from_attr)
}
pub fn from_attr(value: &str) -> Option<Self> {
Some(match value {
"xx-small" => Self::XxSmall,
"x-small" => Self::XSmall,
"small" => Self::Small,
"large" => Self::Large,
"x-large" => Self::XLarge,
"xx-large" => Self::XxLarge,
"xxx-large" => Self::XxxLarge,
_ => return None,
})
}
pub const fn name(self) -> &'static str {
match self {
Self::XxSmall => "xx-small",
Self::XSmall => "x-small",
Self::Small => "small",
Self::Large => "large",
Self::XLarge => "x-large",
Self::XxLarge => "xx-large",
Self::XxxLarge => "xxx-large",
}
}
pub const fn scale(self) -> f32 {
match self {
Self::XxSmall => 0.5625,
Self::XSmall => 0.625,
Self::Small => 0.8125,
Self::Large => 1.125,
Self::XLarge => 1.5,
Self::XxLarge => 2.0,
Self::XxxLarge => 3.0,
}
}
pub const fn index(self) -> usize {
match self {
Self::XxSmall => 0,
Self::XSmall => 1,
Self::Small => 2,
Self::Large => 3,
Self::XLarge => 4,
Self::XxLarge => 5,
Self::XxxLarge => 6,
}
}
pub const ALL: [Self; 7] = [
Self::XxSmall,
Self::XSmall,
Self::Small,
Self::Large,
Self::XLarge,
Self::XxLarge,
Self::XxxLarge,
];
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FontFamily {
Serif,
SansSerif,
Monospace,
Cursive,
}
impl FontFamily {
pub fn from_attrs(attrs: &[(String, Option<String>)]) -> Option<Self> {
attr(attrs, "data-font").and_then(Self::from_attr)
}
pub fn from_attr(value: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|f| value.eq_ignore_ascii_case(f.name()))
}
pub const fn name(self) -> &'static str {
match self {
Self::Serif => "serif",
Self::SansSerif => "sans-serif",
Self::Monospace => "monospace",
Self::Cursive => "cursive",
}
}
pub const fn index(self) -> usize {
match self {
Self::Serif => 0,
Self::SansSerif => 1,
Self::Monospace => 2,
Self::Cursive => 3,
}
}
pub const ALL: [Self; 4] = [Self::Serif, Self::SansSerif, Self::Monospace, Self::Cursive];
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Hundredths(u16);
impl Hundredths {
pub fn from_f32(value: f32) -> Option<Self> {
if !value.is_finite() {
return None;
}
let h = (value * 100.0).round();
(1.0..=f32::from(u16::MAX))
.contains(&h)
.then_some(Self(h as u16))
}
pub fn as_f32(self) -> f32 {
self.0 as f32 / 100.0
}
pub const fn hundredths(self) -> u16 {
self.0
}
fn parse(value: &str) -> Option<Self> {
let (int, frac) = match value.split_once('.') {
Some((_, "")) => return None,
Some(pair) => pair,
None => (value, ""),
};
if int.is_empty() && frac.is_empty() {
return None;
}
if !int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit()) {
return None;
}
let whole: u32 = if int.is_empty() { 0 } else { int.parse().ok()? };
if whole > u32::from(u16::MAX) / 100 {
return None;
}
let digit = |i: usize| frac.as_bytes().get(i).map_or(0, |b| u32::from(b - b'0'));
let mut h = whole * 100 + digit(0) * 10 + digit(1);
if digit(2) >= 5 {
h += 1;
}
(1..=u32::from(u16::MAX))
.contains(&h)
.then_some(Self(h as u16))
}
}
impl std::fmt::Display for Hundredths {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (whole, frac) = (self.0 / 100, self.0 % 100);
match frac {
0 => write!(f, "{whole}"),
_ if frac % 10 == 0 => write!(f, "{whole}.{}", frac / 10),
_ => write!(f, "{whole}.{frac:02}"),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FontSize {
Step(SizeStep),
Points(Hundredths),
}
impl FontSize {
pub fn from_attrs(attrs: &[(String, Option<String>)]) -> Option<Self> {
attr(attrs, "data-size").and_then(Self::from_attr)
}
pub fn from_attr(value: &str) -> Option<Self> {
let value = value.trim();
if let Some(step) = SizeStep::from_attr(value) {
return Some(Self::Step(step));
}
let number = value.strip_suffix("pt").or_else(|| {
let (n, unit) = value.split_at_checked(value.len().checked_sub(2)?)?;
unit.eq_ignore_ascii_case("pt").then_some(n)
})?;
Hundredths::parse(number).map(Self::Points)
}
pub fn points(points: f32) -> Option<Self> {
Hundredths::from_f32(points).map(Self::Points)
}
pub fn name(self) -> Cow<'static, str> {
match self {
Self::Step(step) => Cow::Borrowed(step.name()),
Self::Points(pt) => Cow::Owned(format!("{pt}pt")),
}
}
pub const fn step(self) -> Option<SizeStep> {
match self {
Self::Step(step) => Some(step),
Self::Points(_) => None,
}
}
pub fn points_value(self) -> Option<f32> {
match self {
Self::Step(_) => None,
Self::Points(pt) => Some(pt.as_f32()),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LineHeight {
Step(LineSpacing),
Ratio(Hundredths),
}
impl LineHeight {
pub fn from_attrs(attrs: &[(String, Option<String>)]) -> Option<Self> {
attr(attrs, "data-line-height").and_then(Self::from_attr)
}
pub fn from_attr(value: &str) -> Option<Self> {
let value = value.trim();
if let Some(step) = LineSpacing::from_attr(value) {
return Some(Self::Step(step));
}
Hundredths::parse(value).and_then(Self::of)
}
pub fn ratio(ratio: f32) -> Option<Self> {
Hundredths::from_f32(ratio).and_then(Self::of)
}
pub fn is_absence(value: &str) -> bool {
Hundredths::parse(value.trim()).is_some_and(|r| r.hundredths() == Self::SINGLE)
}
const SINGLE: u16 = 100;
fn of(ratio: Hundredths) -> Option<Self> {
if ratio.hundredths() == Self::SINGLE {
return None;
}
let step = LineSpacing::ALL
.into_iter()
.find(|s| (s.ratio() * 100.0).round() as u16 == ratio.hundredths());
Some(match step {
Some(step) => Self::Step(step),
None => Self::Ratio(ratio),
})
}
pub fn name(self) -> Cow<'static, str> {
match self {
Self::Step(step) => Cow::Borrowed(step.name()),
Self::Ratio(r) => Cow::Owned(r.to_string()),
}
}
pub const fn step(self) -> Option<LineSpacing> {
match self {
Self::Step(step) => Some(step),
Self::Ratio(_) => None,
}
}
pub fn as_f32(self) -> f32 {
match self {
Self::Step(step) => step.ratio(),
Self::Ratio(r) => r.as_f32(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TextColor {
Named(MarkColor),
Rgb { r: u8, g: u8, b: u8 },
}
impl TextColor {
pub fn from_attrs(attrs: &[(String, Option<String>)]) -> Option<Self> {
attr(attrs, "data-color").and_then(Self::from_attr)
}
pub fn from_attr(value: &str) -> Option<Self> {
let value = value.trim();
if let Some(named) = MarkColor::from_attr(value) {
return Some(Self::Named(named));
}
let hex = value.strip_prefix('#')?;
if !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let nib = |i: usize| u8::from_str_radix(&hex[i..i + 1], 16).ok();
let byte = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).ok();
match hex.len() {
3 => Some(Self::Rgb {
r: nib(0)? * 0x11,
g: nib(1)? * 0x11,
b: nib(2)? * 0x11,
}),
6 => Some(Self::Rgb {
r: byte(0)?,
g: byte(2)?,
b: byte(4)?,
}),
_ => None,
}
}
pub fn name(self) -> Cow<'static, str> {
match self {
Self::Named(c) => Cow::Borrowed(c.name()),
Self::Rgb { r, g, b } => Cow::Owned(format!("#{r:02x}{g:02x}{b:02x}")),
}
}
pub const fn named(self) -> Option<MarkColor> {
match self {
Self::Named(c) => Some(c),
Self::Rgb { .. } => None,
}
}
pub const fn rgb(self) -> Option<(u8, u8, u8)> {
match self {
Self::Named(_) => None,
Self::Rgb { r, g, b } => Some((r, g, b)),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum FontFace {
Generic(FontFamily),
Named(String),
}
impl FontFace {
pub fn from_attrs(attrs: &[(String, Option<String>)]) -> Option<Self> {
attr(attrs, "data-font").and_then(Self::from_attr)
}
pub fn from_attr(value: &str) -> Option<Self> {
let value = value.trim();
match FontFamily::from_attr(value) {
Some(generic) => Some(Self::Generic(generic)),
None => (!value.is_empty()).then(|| Self::Named(value.to_string())),
}
}
pub fn name(&self) -> Cow<'_, str> {
match self {
Self::Generic(generic) => Cow::Borrowed(generic.name()),
Self::Named(name) => Cow::Borrowed(name.as_str()),
}
}
pub const fn generic(&self) -> Option<FontFamily> {
match self {
Self::Generic(generic) => Some(*generic),
Self::Named(_) => None,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct FaceId(u32);
impl FaceId {
pub fn of(name: &str) -> Self {
let mut h: u32 = 0x811c_9dc5;
for b in name.as_bytes() {
h ^= u32::from(*b);
h = h.wrapping_mul(0x0100_0193);
}
Self(h)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FaceRef {
Generic(FontFamily),
Named(FaceId),
}
impl FaceRef {
pub const fn generic(self) -> Option<FontFamily> {
match self {
Self::Generic(generic) => Some(generic),
Self::Named(_) => None,
}
}
pub const fn id(self) -> Option<FaceId> {
match self {
Self::Generic(_) => None,
Self::Named(id) => Some(id),
}
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct FaceTable {
entries: Vec<(FaceId, String)>,
}
impl FaceTable {
pub fn name(&self, id: FaceId) -> Option<&str> {
let i = self.entries.binary_search_by_key(&id, |(k, _)| *k).ok()?;
Some(self.entries[i].1.as_str())
}
pub fn spell(&self, face: FaceRef) -> Option<Cow<'_, str>> {
match face {
FaceRef::Generic(generic) => Some(Cow::Borrowed(generic.name())),
FaceRef::Named(id) => self.name(id).map(Cow::Borrowed),
}
}
pub fn iter(&self) -> impl Iterator<Item = (FaceId, &str)> {
self.entries.iter().map(|(id, name)| (*id, name.as_str()))
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
fn insert(&mut self, id: FaceId, name: &str) {
if let Err(i) = self.entries.binary_search_by_key(&id, |(k, _)| *k) {
self.entries.insert(i, (id, name.to_string()));
}
}
pub(crate) fn intern(&mut self, face: &FontFace) -> FaceRef {
match face {
FontFace::Generic(generic) => FaceRef::Generic(*generic),
FontFace::Named(name) => {
let id = FaceId::of(name);
self.insert(id, name);
FaceRef::Named(id)
}
}
}
pub(crate) fn face_from_attrs(
&mut self,
attrs: &[(String, Option<String>)],
) -> Option<FaceRef> {
FontFace::from_attrs(attrs).map(|face| self.intern(&face))
}
pub(crate) fn merge(&mut self, other: &FaceTable) {
for (id, name) in &other.entries {
self.insert(*id, name);
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Token {
Punctuation,
Keyword,
Entity,
Support,
Constant,
String,
Comment,
Invalid,
}
impl Token {
pub const fn name(self) -> &'static str {
match self {
Self::Punctuation => "punctuation",
Self::Keyword => "keyword",
Self::Entity => "entity",
Self::Support => "support",
Self::Constant => "constant",
Self::String => "string",
Self::Comment => "comment",
Self::Invalid => "invalid",
}
}
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|t| t.name() == name)
}
pub const fn index(self) -> usize {
match self {
Self::Punctuation => 0,
Self::Keyword => 1,
Self::Entity => 2,
Self::Support => 3,
Self::Constant => 4,
Self::String => 5,
Self::Comment => 6,
Self::Invalid => 7,
}
}
pub const ALL: [Self; 8] = [
Self::Punctuation,
Self::Keyword,
Self::Entity,
Self::Support,
Self::Constant,
Self::String,
Self::Comment,
Self::Invalid,
];
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Baseline {
#[default]
Normal,
Super,
Sub,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Style {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub role: Role,
pub baseline: Baseline,
pub token: Option<Token>,
pub size: Option<FontSize>,
pub font: Option<FaceRef>,
pub color: Option<TextColor>,
}
impl Style {
pub const fn bold(mut self) -> Self {
self.bold = true;
self
}
pub const fn italic(mut self) -> Self {
self.italic = true;
self
}
pub const fn underline(mut self) -> Self {
self.underline = true;
self
}
pub const fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
pub const fn role(mut self, r: Role) -> Self {
self.role = r;
self
}
pub const fn baseline(mut self, b: Baseline) -> Self {
self.baseline = b;
self
}
pub const fn token(mut self, t: Option<Token>) -> Self {
self.token = t;
self
}
pub const fn size(mut self, s: Option<FontSize>) -> Self {
self.size = s;
self
}
pub const fn font(mut self, f: Option<FaceRef>) -> Self {
self.font = f;
self
}
pub const fn color(mut self, c: Option<TextColor>) -> Self {
self.color = c;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_colour_tables_agree_with_each_other() {
for (i, c) in MarkColor::ALL.into_iter().enumerate() {
assert_eq!(c.index(), i, "{} is not where ALL puts it", c.name());
assert_eq!(MarkColor::from_attr(c.name()), Some(c), "name round-trip");
}
assert_eq!(MarkColor::from_attr("chartreuse"), None);
assert_eq!(MarkColor::from_attr(""), None);
}
#[test]
fn the_token_tables_agree_with_each_other() {
for (i, t) in Token::ALL.into_iter().enumerate() {
assert_eq!(t.index(), i, "{} is not where ALL puts it", t.name());
assert_eq!(Token::from_name(t.name()), Some(t), "name round-trip");
}
assert_eq!(Token::from_name("meta"), None);
assert_eq!(Token::from_name(""), None);
}
#[test]
fn the_presentation_tables_agree_with_each_other() {
for (i, a) in Align::ALL.into_iter().enumerate() {
assert_eq!(a.index(), i, "{} is not where ALL puts it", a.name());
assert_eq!(Align::from_token(a.name()), Some(a), "name round-trip");
}
for (i, l) in LineSpacing::ALL.into_iter().enumerate() {
assert_eq!(l.index(), i, "{} is not where ALL puts it", l.name());
assert_eq!(LineSpacing::from_attr(l.name()), Some(l), "name round-trip");
}
for (i, z) in SizeStep::ALL.into_iter().enumerate() {
assert_eq!(z.index(), i, "{} is not where ALL puts it", z.name());
assert_eq!(SizeStep::from_attr(z.name()), Some(z), "name round-trip");
}
for (i, f) in FontFamily::ALL.into_iter().enumerate() {
assert_eq!(f.index(), i, "{} is not where ALL puts it", f.name());
assert_eq!(FontFamily::from_attr(f.name()), Some(f), "name round-trip");
}
assert_eq!(Align::from_token("left"), None);
assert_eq!(LineSpacing::from_attr("1"), None);
assert_eq!(SizeStep::from_attr("medium"), None);
assert_eq!(SizeStep::from_attr("14pt"), None);
assert_eq!(FontFamily::from_attr("Garamond"), None);
assert_eq!(FontFamily::from_attr("fantasy"), None);
assert_eq!(FontFamily::from_attr("Serif"), Some(FontFamily::Serif));
assert_eq!(
FontFamily::from_attr("SANS-SERIF"),
Some(FontFamily::SansSerif)
);
assert_eq!(
FontFamily::from_attr("MonoSpace").unwrap().name(),
"monospace"
);
assert_eq!(
FontFace::from_attr("Serif"),
Some(FontFace::Generic(FontFamily::Serif)),
"and the open type reads it as the generic, not as a family name"
);
assert_eq!(FontFace::from_attr("Serif").unwrap().name(), "serif");
}
#[test]
fn the_size_ramp_is_css_s_own() {
let ramp: Vec<f32> = SizeStep::ALL.into_iter().map(SizeStep::scale).collect();
assert_eq!(
ramp,
vec![0.5625, 0.625, 0.8125, 1.125, 1.5, 2.0, 3.0],
"the CSS absolute-size ratios, medium removed"
);
assert!(ramp.windows(2).all(|w| w[0] < w[1]));
assert!(SizeStep::Small.scale() < 1.0 && SizeStep::Large.scale() > 1.0);
let spacing: Vec<f32> = LineSpacing::ALL
.into_iter()
.map(LineSpacing::ratio)
.collect();
assert_eq!(spacing, vec![1.15, 1.5, 2.0]);
}
#[test]
fn an_alignment_is_one_token_of_a_class_and_the_rest_is_somebody_else_s() {
let class = |v: &str| vec![("class".to_string(), Some(v.to_string()))];
assert_eq!(Align::from_attrs(&class("center")), Some(Align::Center));
assert_eq!(
Align::from_attrs(&class("lead center")),
Some(Align::Center)
);
assert_eq!(
Align::from_attrs(&class("center lead")),
Some(Align::Center)
);
assert_eq!(Align::from_attrs(&class("lead wide")), None);
assert_eq!(Align::from_attrs(&class("")), None);
assert_eq!(Align::from_attrs(&[]), None);
assert_eq!(Align::from_attrs(&[("class".to_string(), None)]), None);
let attr = |k: &str, v: &str| vec![(k.to_string(), Some(v.to_string()))];
assert_eq!(
LineSpacing::from_attrs(&attr("data-line-height", "1.5")),
Some(LineSpacing::OneHalf)
);
assert_eq!(LineSpacing::from_attrs(&attr("class", "1.5")), None);
assert_eq!(
SizeStep::from_attrs(&attr("data-size", "large")),
Some(SizeStep::Large)
);
assert_eq!(
FontFamily::from_attrs(&attr("data-font", "monospace")),
Some(FontFamily::Monospace)
);
assert_eq!(
MarkColor::from_attrs(&attr("data-color", "blue")),
Some(MarkColor::Blue)
);
}
#[test]
fn a_colour_is_read_out_of_the_data_color_attribute_and_nothing_else() {
let attr = |k: &str, v: &str| vec![(k.to_string(), Some(v.to_string()))];
assert_eq!(
MarkColor::from_attrs(&attr("data-color", "green")),
Some(MarkColor::Green)
);
assert_eq!(MarkColor::from_attrs(&[]), None);
assert_eq!(MarkColor::from_attrs(&attr("id", "red")), None);
assert_eq!(
MarkColor::from_attrs(&[("data-color".to_string(), None)]),
None
);
}
#[test]
fn each_property_reads_a_name_or_a_value_and_spells_one_of_them_back() {
let size = |v: &str| FontSize::from_attr(v);
assert_eq!(size("large"), Some(FontSize::Step(SizeStep::Large)));
assert_eq!(size("14pt"), FontSize::points(14.0));
assert_eq!(size("14.0pt"), size("14pt"), "the same size, spelled twice");
assert_eq!(size(" 13.5pt "), FontSize::points(13.5));
assert_eq!(size("14PT"), size("14pt"), "CSS reads its units either way");
assert_eq!(size("14pt").unwrap().name(), "14pt");
assert_eq!(size("13.50pt").unwrap().name(), "13.5pt");
assert_eq!(size("13.25pt").unwrap().name(), "13.25pt");
assert_eq!(size("large").unwrap().name(), "large");
assert_eq!(size("14pt").unwrap().points_value(), Some(14.0));
assert_eq!(size("large").unwrap().points_value(), None);
assert_eq!(size("large").unwrap().step(), Some(SizeStep::Large));
let lh = |v: &str| LineHeight::from_attr(v);
assert_eq!(lh("1.5"), Some(LineHeight::Step(LineSpacing::OneHalf)));
assert_eq!(lh("1.50"), lh("1.5"), "one spacing, not two");
assert_eq!(lh("2.0"), Some(LineHeight::Step(LineSpacing::Double)));
assert_eq!(lh("1.3"), LineHeight::ratio(1.3));
assert_eq!(lh("1.3").unwrap().name(), "1.3");
assert_eq!(lh("1.25").unwrap().name(), "1.25");
assert_eq!(lh("1.5").unwrap().name(), "1.5");
assert!((lh("1.3").unwrap().as_f32() - 1.3).abs() < 1e-6);
assert!((lh("1.5").unwrap().as_f32() - 1.5).abs() < 1e-6);
for v in ["1", "1.0", "1.00", " 1 "] {
assert_eq!(lh(v), None, "{v:?} is single, which is absence");
}
assert_eq!(LineHeight::ratio(1.0), None);
let col = |v: &str| TextColor::from_attr(v);
assert_eq!(col("red"), Some(TextColor::Named(MarkColor::Red)));
assert_eq!(
col("#c03030"),
Some(TextColor::Rgb {
r: 0xc0,
g: 0x30,
b: 0x30
})
);
assert_eq!(col("#C03030"), col("#c03030"));
assert_eq!(col("#f00"), col("#ff0000"), "each digit doubled");
assert_eq!(col("#c03030").unwrap().name(), "#c03030");
assert_eq!(col("red").unwrap().name(), "red");
assert_eq!(col("#c03030").unwrap().rgb(), Some((0xc0, 0x30, 0x30)));
assert_eq!(col("red").unwrap().named(), Some(MarkColor::Red));
let face = |v: &str| FontFace::from_attr(v);
assert_eq!(face("serif"), Some(FontFace::Generic(FontFamily::Serif)));
assert_eq!(face("Garamond"), Some(FontFace::Named("Garamond".into())));
assert_eq!(face(" Garamond "), face("Garamond"));
assert_eq!(face("Garamond").unwrap().name(), "Garamond");
assert_eq!(face("serif").unwrap().name(), "serif");
assert_eq!(face("Garamond").unwrap().generic(), None);
}
#[test]
fn a_value_outside_the_grammar_is_carried_and_not_guessed_at() {
for v in ["huge", "14px", "1.3em", "14", "pt", "-14pt", "0pt", ""] {
assert_eq!(FontSize::from_attr(v), None, "{v:?} is not a size");
}
for v in ["1.3em", "normal", "-1.3", "0", "1.2.3", ""] {
assert_eq!(LineHeight::from_attr(v), None, "{v:?} is not a spacing");
}
for v in [
"rgb(192, 48, 48)",
"chartreuse",
"#c0303",
"#gggggg",
"c03030",
"#",
"",
] {
assert_eq!(TextColor::from_attr(v), None, "{v:?} is not a colour");
}
assert_eq!(FontFace::from_attr(" "), None);
assert_eq!(FontFace::from_attr(""), None);
let bare = |k: &str| vec![(k.to_string(), None)];
assert_eq!(FontSize::from_attrs(&bare("data-size")), None);
assert_eq!(LineHeight::from_attrs(&bare("data-line-height")), None);
assert_eq!(TextColor::from_attrs(&bare("data-color")), None);
assert_eq!(FontFace::from_attrs(&bare("data-font")), None);
}
#[test]
fn a_value_is_hundredths_and_spells_itself_as_short_as_it_can() {
let h = |v: f32| Hundredths::from_f32(v).unwrap().to_string();
assert_eq!(h(14.0), "14");
assert_eq!(h(13.5), "13.5");
assert_eq!(h(1.25), "1.25");
assert_eq!(h(1.3), "1.3");
assert_eq!(h(0.05), "0.05");
assert_eq!(Hundredths::from_f32(14.0).unwrap().as_f32(), 14.0);
assert_eq!(Hundredths::from_f32(14.0).unwrap().hundredths(), 1400);
assert_eq!(Hundredths::from_f32(700.0), None);
assert_eq!(Hundredths::from_f32(0.0), None);
assert_eq!(Hundredths::from_f32(-1.0), None);
assert_eq!(Hundredths::from_f32(f32::NAN), None);
assert_eq!(FontSize::from_attr("42949672.99pt"), None);
assert_eq!(FontSize::from_attr("999999999999pt"), None);
assert_eq!(FontSize::from_attr("655.36pt"), None);
assert_eq!(FontSize::from_attr("655.35pt").unwrap().name(), "655.35pt");
assert_eq!(FontSize::from_attr("13.456pt"), FontSize::points(13.46));
assert_eq!(FontSize::from_attr("13.454pt"), FontSize::points(13.45));
assert_eq!(FontSize::from_attr(".5pt"), FontSize::points(0.5));
assert_eq!(LineHeight::from_attr(".5"), LineHeight::ratio(0.5));
assert_eq!(FontSize::from_attr("14.pt"), None);
assert_eq!(LineHeight::from_attr("1."), None);
assert_eq!(FontSize::from_attr(".pt"), None);
}
#[test]
fn a_named_face_is_interned_once_and_its_id_is_the_name_s_own() {
let mut faces = FaceTable::default();
let garamond = FontFace::Named("Garamond".into());
let a = faces.intern(&garamond);
let b = faces.intern(&FontFace::Named("Garamond".into()));
assert_eq!(a, b, "one name, one id");
assert_eq!(faces.len(), 1, "and one entry");
assert_eq!(a, FaceRef::Named(FaceId::of("Garamond")));
assert_eq!(faces.name(FaceId::of("Garamond")), Some("Garamond"));
assert_eq!(a.generic(), None);
let serif = faces.intern(&FontFace::Generic(FontFamily::Serif));
assert_eq!(serif, FaceRef::Generic(FontFamily::Serif));
assert_eq!(serif.id(), None);
assert_eq!(faces.len(), 1);
let mut one = FaceTable::default();
one.intern(&FontFace::Named("Futura".into()));
one.intern(&garamond);
let mut two = FaceTable::default();
two.intern(&garamond);
two.intern(&FontFace::Named("Futura".into()));
assert_eq!(one, two);
let mut merged = FaceTable::default();
merged.merge(&one);
merged.merge(&faces);
assert_eq!(merged.len(), 2);
assert_eq!(merged.name(FaceId::of("Futura")), Some("Futura"));
assert_eq!(merged.name(FaceId::of("Bodoni")), None);
}
#[test]
fn a_face_table_spells_the_face_a_glyph_carries() {
let mut faces = FaceTable::default();
let garamond = faces.intern(&FontFace::Named("Garamond".into()));
assert_eq!(faces.spell(garamond).as_deref(), Some("Garamond"));
assert_eq!(
faces
.spell(FaceRef::Generic(FontFamily::SansSerif))
.as_deref(),
Some("sans-serif"),
"a generic names itself, and needs no entry to do it"
);
assert_eq!(
faces.spell(FaceRef::Named(FaceId::of("Bodoni"))),
None,
"an id from another map draws in the body face and names nothing"
);
}
#[test]
fn single_spacing_is_the_one_ratio_that_means_the_theme_s_own() {
for single in ["1", "1.0", "1.00", " 1 "] {
assert!(LineHeight::is_absence(single), "{single}");
assert_eq!(LineHeight::from_attr(single), None, "{single}");
}
for value in ["1.3", "1.5", "2", "0", "0.5", "huge", "1.", "", "1em"] {
assert!(!LineHeight::is_absence(value), "{value}");
}
}
}