use std::sync::Arc;
use crossterm::style::Color;
use cssparser::{Parser as CssParser, ParserInput, Token};
use indexmap::IndexMap;
use crate::html::box_edges::{Border, BorderStyle, EdgeAuto, EdgeSizes, MarginSizes};
use super::color::parse_color_str;
pub const PX_PER_CELL: f32 = 8.0;
fn compose_key<'k>(buf: &'k mut [u8; 32], parts: &[&str]) -> &'k str {
let mut len = 0usize;
for part in parts {
buf[len..len + part.len()].copy_from_slice(part.as_bytes());
len += part.len();
}
std::str::from_utf8(&buf[..len]).expect("concatenation of &str parts is valid UTF-8")
}
pub(crate) fn parse_edge_sizes(
decls: &IndexMap<Arc<str>, Arc<str>>,
shorthand: &str,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> EdgeSizes {
let shorthand_raw = decls.get(shorthand);
let mut edges = parse_shorthand_lengths(
shorthand_raw.map(|s| s.as_ref()),
containing_width,
font_ctx,
viewport,
);
let mut key = [0u8; 32];
let top_raw = decls
.get(compose_key(&mut key, &[shorthand, "-top"]))
.map(|s| s.as_ref());
if let Some(v) = parse_length_token(top_raw, containing_width, font_ctx, viewport) {
edges.top = v;
}
let right_raw = decls
.get(compose_key(&mut key, &[shorthand, "-right"]))
.map(|s| s.as_ref());
if let Some(v) = parse_length_token(right_raw, containing_width, font_ctx, viewport) {
edges.right = v;
}
let bottom_raw = decls
.get(compose_key(&mut key, &[shorthand, "-bottom"]))
.map(|s| s.as_ref());
if let Some(v) = parse_length_token(bottom_raw, containing_width, font_ctx, viewport) {
edges.bottom = v;
}
let left_raw = decls
.get(compose_key(&mut key, &[shorthand, "-left"]))
.map(|s| s.as_ref());
if let Some(v) = parse_length_token(left_raw, containing_width, font_ctx, viewport) {
edges.left = v;
}
edges
}
fn parse_shorthand_lengths(
value: Option<&str>,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> EdgeSizes {
let Some(value) = value else {
return EdgeSizes::default();
};
let tokens: Vec<&str> = value.split_whitespace().collect();
let parsed: Vec<usize> = tokens
.iter()
.filter_map(|t| parse_length_token(Some(t), containing_width, font_ctx, viewport))
.collect();
match parsed.len() {
0 => EdgeSizes::default(),
1 => EdgeSizes::new(parsed[0], parsed[0], parsed[0], parsed[0]),
2 => EdgeSizes::new(parsed[0], parsed[1], parsed[0], parsed[1]),
3 => EdgeSizes::new(parsed[0], parsed[1], parsed[2], parsed[1]),
_ => EdgeSizes::new(parsed[0], parsed[1], parsed[2], parsed[3]),
}
}
pub(crate) fn parse_edge_auto(decls: &IndexMap<Arc<str>, Arc<str>>, shorthand: &str) -> EdgeAuto {
let mut out = EdgeAuto::default();
if let Some(value) = decls.get(shorthand) {
let autos: Vec<bool> = value
.split_whitespace()
.map(|t| t.eq_ignore_ascii_case("auto"))
.collect();
match autos.as_slice() {
[a] => {
if *a {
out = EdgeAuto::new(true, true, true, true);
}
}
[a, b] => {
if *a {
out.top = true;
out.bottom = true;
}
if *b {
out.right = true;
out.left = true;
}
}
[a, b, c] => {
if *a {
out.top = true;
}
if *b {
out.right = true;
out.left = true;
}
if *c {
out.bottom = true;
}
}
[a, b, c, d, ..] => {
out.top = *a;
out.right = *b;
out.bottom = *c;
out.left = *d;
}
[] => {}
}
}
let mut key = [0u8; 32];
for (side_name, flag) in [
("top", &mut out.top),
("right", &mut out.right),
("bottom", &mut out.bottom),
("left", &mut out.left),
] {
if let Some(raw) = decls.get(compose_key(&mut key, &[shorthand, "-", side_name])) {
*flag = raw.eq_ignore_ascii_case("auto");
}
}
out
}
fn parse_signed_length_token(
value: Option<&str>,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> Option<isize> {
let raw = value?;
match Length::parse_signed(raw)? {
Length::Auto => None,
len => Some(len.resolve_signed(LengthContext {
containing: containing_width,
font: font_ctx,
viewport,
})),
}
}
pub(crate) fn parse_margin_sizes(
decls: &IndexMap<Arc<str>, Arc<str>>,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> MarginSizes {
let shorthand_raw = decls.get("margin");
let mut edges = parse_shorthand_margins(
shorthand_raw.map(|s| s.as_ref()),
containing_width,
font_ctx,
viewport,
);
let mut key = [0u8; 32];
if let Some(raw) = decls.get(compose_key(&mut key, &["margin", "-top"]))
&& let Some(v) =
parse_signed_length_token(Some(raw.as_ref()), containing_width, font_ctx, viewport)
{
edges.top = v.max(0) as usize;
}
if let Some(raw) = decls.get(compose_key(&mut key, &["margin", "-right"]))
&& let Some(v) =
parse_signed_length_token(Some(raw.as_ref()), containing_width, font_ctx, viewport)
{
edges.right = v;
}
if let Some(raw) = decls.get(compose_key(&mut key, &["margin", "-bottom"]))
&& let Some(v) =
parse_signed_length_token(Some(raw.as_ref()), containing_width, font_ctx, viewport)
{
edges.bottom = v.max(0) as usize;
}
if let Some(raw) = decls.get(compose_key(&mut key, &["margin", "-left"]))
&& let Some(v) =
parse_signed_length_token(Some(raw.as_ref()), containing_width, font_ctx, viewport)
{
edges.left = v;
}
edges
}
fn parse_shorthand_margins(
value: Option<&str>,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> MarginSizes {
let Some(value) = value else {
return MarginSizes::default();
};
let parsed: Vec<isize> = value
.split_whitespace()
.filter_map(|t| parse_signed_length_token(Some(t), containing_width, font_ctx, viewport))
.collect();
let v = |x: isize| x.max(0) as usize;
match parsed.len() {
0 => MarginSizes::default(),
1 => MarginSizes::new(v(parsed[0]), parsed[0], v(parsed[0]), parsed[0]),
2 => MarginSizes::new(v(parsed[0]), parsed[1], v(parsed[0]), parsed[1]),
3 => MarginSizes::new(v(parsed[0]), parsed[1], v(parsed[2]), parsed[1]),
_ => MarginSizes::new(v(parsed[0]), parsed[1], v(parsed[2]), parsed[3]),
}
}
fn resolve_length(
length: Length,
containing: usize,
font: FontContext,
viewport: Viewport,
) -> usize {
length.resolve(LengthContext {
containing,
font,
viewport,
})
}
pub(super) fn parse_length_token(
value: Option<&str>,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> Option<usize> {
let raw = value?;
match Length::parse(raw)? {
Length::Auto => None,
len => Some(resolve_length(len, containing_width, font_ctx, viewport)),
}
}
pub(crate) fn parse_border_width(
value: Option<&str>,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> usize {
parse_length_token(value, containing_width, font_ctx, viewport).unwrap_or(0)
}
pub(crate) fn parse_border_style(value: Option<&str>) -> BorderStyle {
let Some(value) = value else {
return BorderStyle::None;
};
BorderStyle::parse(value)
}
pub(crate) fn parse_border_side(
side_raw: Option<&Arc<str>>,
shorthand_raw: Option<&Arc<str>>,
fallback: Border,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> Border {
if let Some(value) = side_raw
&& let Some(parsed) = parse_border_shorthand(value, containing_width, font_ctx, viewport)
{
return parsed;
}
if let Some(value) = shorthand_raw
&& let Some(parsed) = parse_border_shorthand(value, containing_width, font_ctx, viewport)
{
return parsed;
}
fallback
}
pub(crate) fn merge_border_longhand(
side: Border,
decls: &IndexMap<Arc<str>, Arc<str>>,
side_name: &str,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> Border {
let mut side = side;
let mut key = [0u8; 32];
if let Some(raw) = decls.get(compose_key(&mut key, &["border-", side_name, "-width"]))
&& let Some(w) =
parse_length_token(Some(raw.as_ref()), containing_width, font_ctx, viewport)
{
side.width = w;
}
if let Some(raw) = decls.get(compose_key(&mut key, &["border-", side_name, "-style"])) {
side.style = parse_border_style(Some(raw.as_ref()));
}
if let Some(raw) = decls.get(compose_key(&mut key, &["border-", side_name, "-color"]))
&& let Some(c) = parse_color_str(raw)
{
side.color = Some(c);
}
side
}
pub(crate) fn parse_border_shorthand(
value: &str,
containing_width: usize,
font_ctx: FontContext,
viewport: Viewport,
) -> Option<Border> {
let mut width: Option<usize> = None;
let mut style: Option<BorderStyle> = None;
let mut color: Option<Color> = None;
for token in split_border_tokens(value) {
if let Some(w) =
parse_length_token(Some(token.as_str()), containing_width, font_ctx, viewport)
{
width = Some(w);
continue;
}
let lower = token.to_ascii_lowercase();
if matches!(
lower.as_str(),
"solid"
| "dashed"
| "dotted"
| "double"
| "none"
| "hidden"
| "groove"
| "ridge"
| "inset"
| "outset"
) {
style = Some(BorderStyle::parse(&token));
continue;
}
if let Some(c) = parse_color_str(&token) {
color = Some(c);
continue;
}
}
if width.is_none() && style.is_none() && color.is_none() {
return None;
}
Some(Border {
width: width.unwrap_or(0),
style: style.unwrap_or(BorderStyle::None),
color,
})
}
fn split_border_tokens(value: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut cur = String::new();
let mut depth = 0usize;
for c in value.chars() {
match c {
'(' => {
depth = depth.saturating_add(1);
cur.push(c);
}
')' => {
depth = depth.saturating_sub(1);
cur.push(c);
}
c if c.is_whitespace() && depth == 0 => {
if !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
}
c => cur.push(c),
}
}
if !cur.is_empty() {
out.push(cur);
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FontContext {
pub font_size_cells: usize,
pub root_font_size_cells: usize,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Viewport {
pub cols: usize,
pub rows: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LengthContext {
pub containing: usize,
pub font: FontContext,
pub viewport: Viewport,
}
impl FontContext {
pub fn root() -> Self {
Self {
font_size_cells: 1,
root_font_size_cells: 1,
}
}
pub fn child(self, child_cells: usize) -> Self {
Self {
font_size_cells: child_cells,
root_font_size_cells: self.root_font_size_cells,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Length {
Auto,
Px(f32),
Pct(f32),
Em(f32),
Rem(f32),
Ch(f32),
Vw(f32),
Vh(f32),
Math(MathFn),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MathFn {
Calc(Term, Term),
Min(Term, Term),
Max(Term, Term),
Clamp(Term, Term, Term),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Term {
pub n: f32,
pub unit: MathUnit,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MathUnit {
Px,
Pct,
Em,
Rem,
Ch,
Vw,
Vh,
}
impl Term {
fn cells(self, context: LengthContext) -> f32 {
match self.unit {
MathUnit::Px => self.n / PX_PER_CELL,
MathUnit::Pct => self.n * context.containing as f32 / 100.0,
MathUnit::Em => self.n * context.font.font_size_cells as f32,
MathUnit::Rem => self.n * context.font.root_font_size_cells as f32,
MathUnit::Ch => self.n,
MathUnit::Vw => self.n * context.viewport.cols as f32 / 100.0,
MathUnit::Vh => self.n * context.viewport.rows as f32 / 100.0,
}
}
}
impl MathFn {
fn eval(self, context: LengthContext) -> f32 {
match self {
MathFn::Calc(a, b) => a.cells(context) + b.cells(context),
MathFn::Min(a, b) => a.cells(context).min(b.cells(context)),
MathFn::Max(a, b) => a.cells(context).max(b.cells(context)),
MathFn::Clamp(min, val, max) => val
.cells(context)
.clamp(min.cells(context), max.cells(context)),
}
}
}
#[derive(Debug, Clone, Copy)]
enum PreLength {
Px(f32),
Pct(f32),
Em(f32),
Rem(f32),
Ch(f32),
Vw(f32),
Vh(f32),
}
impl Length {
pub fn parse(s: &str) -> Option<Length> {
Self::parse_length_shared(s, false)
}
pub fn parse_signed(s: &str) -> Option<Length> {
Self::parse_length_shared(s, true)
}
pub fn resolve_signed(&self, context: LengthContext) -> isize {
match self {
Length::Auto => context.containing as isize,
&Length::Px(px) => (px / PX_PER_CELL).round() as isize,
&Length::Pct(p) => (context.containing as f32 * p / 100.0).round() as isize,
&Length::Em(n) => (n * context.font.font_size_cells as f32).round() as isize,
&Length::Rem(n) => (n * context.font.root_font_size_cells as f32).round() as isize,
&Length::Ch(n) => n.round() as isize,
&Length::Vw(n) => (n * context.viewport.cols as f32 / 100.0).round() as isize,
&Length::Vh(n) => (n * context.viewport.rows as f32 / 100.0).round() as isize,
Length::Math(func) => func.eval(context).round() as isize,
}
}
fn parse_length_shared(s: &str, allow_signed: bool) -> Option<Length> {
let s = s.trim();
if s.eq_ignore_ascii_case("auto") {
return Some(Length::Auto);
}
if let Some(math) = parse_math_function(s) {
return Some(Length::Math(math));
}
let mut input_buf = ParserInput::new(s);
let mut input = CssParser::new(&mut input_buf);
let pre = match input.next().ok()? {
Token::Number {
value,
int_value,
has_sign,
..
} => {
if *has_sign && !allow_signed {
return None;
}
PreLength::Px(int_value.map(|i| i as f32).unwrap_or(*value))
}
Token::Dimension {
value,
int_value,
has_sign,
unit,
..
} => {
if *has_sign && !allow_signed {
return None;
}
let n = int_value.map(|i| i as f32).unwrap_or(*value);
match unit.to_ascii_lowercase().as_str() {
"px" => PreLength::Px(n),
"em" => PreLength::Em(n),
"rem" => PreLength::Rem(n),
"ch" => PreLength::Ch(n),
"vw" => PreLength::Vw(n),
"vh" => PreLength::Vh(n),
_ => return None,
}
}
Token::Percentage {
unit_value,
has_sign,
..
} => {
if *has_sign && !allow_signed {
return None;
}
let percent = unit_value * 100.0;
let nearest_integer = percent.round();
let percent = if (percent - nearest_integer).abs() < 0.0001 {
nearest_integer
} else {
percent
};
PreLength::Pct(percent)
}
_ => return None,
};
if !input.is_exhausted() {
return None;
}
Some(match pre {
PreLength::Px(n) => Length::Px(n),
PreLength::Pct(p) => Length::Pct(p),
PreLength::Em(n) => Length::Em(n),
PreLength::Rem(n) => Length::Rem(n),
PreLength::Ch(n) => Length::Ch(n),
PreLength::Vw(n) => Length::Vw(n),
PreLength::Vh(n) => Length::Vh(n),
})
}
pub fn resolve(&self, context: LengthContext) -> usize {
match self {
Length::Auto => context.containing,
&Length::Px(px) => (px / PX_PER_CELL).round().max(0.0) as usize,
&Length::Pct(p) => {
let raw = (context.containing as f32 * p / 100.0).round();
(raw as usize).max(1)
}
&Length::Em(n) => (n * context.font.font_size_cells as f32).round().max(0.0) as usize,
&Length::Rem(n) => (n * context.font.root_font_size_cells as f32)
.round()
.max(0.0) as usize,
&Length::Ch(n) => n.round().max(0.0) as usize,
&Length::Vw(n) => {
let raw = (n * context.viewport.cols as f32 / 100.0).round();
(raw as usize).max(1)
}
&Length::Vh(n) => {
let raw = (n * context.viewport.rows as f32 / 100.0).round();
(raw as usize).max(1)
}
Length::Math(func) => {
let cells = func.eval(context).round();
(cells.max(0.0) as usize).max(1)
}
}
}
}
fn parse_math_function(s: &str) -> Option<MathFn> {
let lower = s.to_ascii_lowercase();
for name in ["calc", "min", "max", "clamp"] {
let prefix = format!("{name}(");
if let Some(rest) = lower.strip_prefix(&prefix)
&& rest.ends_with(')')
{
let body = &s[name.len() + 1..s.len() - 1];
return parse_math_body(name, body);
}
}
None
}
fn parse_math_body(name: &str, body: &str) -> Option<MathFn> {
match name {
"calc" => {
let (a, b) = split_calc_addends(body)?;
Some(MathFn::Calc(a, b))
}
"min" | "max" => {
let parts = split_commas(body);
if parts.len() != 2 {
return None;
}
let a = parse_term(parts[0])?;
let b = parse_term(parts[1])?;
Some(if name == "min" {
MathFn::Min(a, b)
} else {
MathFn::Max(a, b)
})
}
_ => {
let parts = split_commas(body);
if parts.len() != 3 {
return None;
}
Some(MathFn::Clamp(
parse_term(parts[0])?,
parse_term(parts[1])?,
parse_term(parts[2])?,
))
}
}
}
fn split_calc_addends(body: &str) -> Option<(Term, Term)> {
let mut depth = 0usize;
for (i, ch) in body.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
'+' | '-' if depth == 0 && i > 0 => {
let a = parse_term(&body[..i])?;
let b = parse_term(&body[i + 1..])?;
let b_signed = Term {
n: if ch == '-' { -b.n } else { b.n },
unit: b.unit,
};
return Some((a, b_signed));
}
_ => {}
}
}
let a = parse_term(body)?;
Some((
a,
Term {
n: 0.0,
unit: MathUnit::Ch,
},
))
}
fn split_commas(body: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
for (i, ch) in body.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
parts.push(body[start..i].trim());
start = i + 1;
}
_ => {}
}
}
parts.push(body[start..].trim());
parts.into_iter().filter(|p| !p.is_empty()).collect()
}
fn parse_term(s: &str) -> Option<Term> {
let s = s.trim();
if let Some((lhs, rhs)) = s.split_once('*') {
let k: f32 = lhs.trim().parse().ok()?;
let mut t = parse_value_term(rhs)?;
t.n *= k;
return Some(t);
}
if let Some((lhs, rhs)) = s.split_once('/') {
let d: f32 = rhs.trim().parse().ok()?;
let mut t = parse_value_term(lhs)?;
if d == 0.0 {
return None;
}
t.n /= d;
return Some(t);
}
parse_value_term(s)
}
fn parse_value_term(s: &str) -> Option<Term> {
let s = s.trim();
let (num_end, unit) = s
.char_indices()
.find(|(_, c)| !(c.is_ascii_digit() || *c == '.' || *c == '-' || *c == '+'))
.map(|(i, _)| (i, &s[i..]))
.unwrap_or((s.len(), ""));
let n: f32 = s[..num_end].trim().parse().ok()?;
let unit = match unit.trim().to_ascii_lowercase().as_str() {
"" | "px" => MathUnit::Px,
"%" => MathUnit::Pct,
"em" => MathUnit::Em,
"rem" => MathUnit::Rem,
"ch" => MathUnit::Ch,
"vw" => MathUnit::Vw,
"vh" => MathUnit::Vh,
_ => return None,
};
Some(Term { n, unit })
}