use std::fmt;
use std::sync::Arc;
use crate::features::refs::{citation_to_typst, Citation, CitationMode, CiteGroup, ReferenceType};
use chrono::{NaiveDate, NaiveTime};
use indexmap::IndexMap;
use regex::Regex;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LengthUnit {
Pt,
Mm,
Cm,
In,
Em,
}
impl LengthUnit {
pub fn to_pt(&self, value: f64) -> Option<f64> {
match self {
LengthUnit::Pt => Some(value),
LengthUnit::Mm => Some(value * 2.835),
LengthUnit::Cm => Some(value * 28.35),
LengthUnit::In => Some(value * 72.0),
LengthUnit::Em => None, }
}
pub fn suffix(&self) -> &'static str {
match self {
LengthUnit::Pt => "pt",
LengthUnit::Mm => "mm",
LengthUnit::Cm => "cm",
LengthUnit::In => "in",
LengthUnit::Em => "em",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Length {
Exact(f64, LengthUnit),
Ratio(f64),
Sum(Box<Length>, Box<Length>),
}
impl Length {
pub fn exact(value: f64, unit: LengthUnit) -> Self {
Length::Exact(value, unit)
}
pub fn ratio(value: f64) -> Self {
Length::Ratio(value)
}
pub fn negate(&self) -> Self {
match self {
Length::Exact(v, u) => Length::Exact(-v, *u),
Length::Ratio(r) => Length::Ratio(-r),
Length::Sum(a, b) => Length::Sum(Box::new(a.negate()), Box::new(b.negate())),
}
}
pub fn scale(&self, factor: f64) -> Self {
match self {
Length::Exact(v, u) => Length::Exact(v * factor, *u),
Length::Ratio(r) => Length::Ratio(r * factor),
Length::Sum(a, b) => Length::Sum(Box::new(a.scale(factor)), Box::new(b.scale(factor))),
}
}
pub fn to_typst(&self) -> String {
match self {
Length::Exact(v, u) => format!("{}{}", v, u.suffix()),
Length::Ratio(r) => format!("{}%", r * 100.0),
Length::Sum(a, b) => format!("{} + {}", a.to_typst(), b.to_typst()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Color {
Rgb { r: f64, g: f64, b: f64, a: f64 },
Cmyk { c: f64, m: f64, y: f64, k: f64 },
Luma(f64),
}
impl Color {
pub fn rgb(r: u8, g: u8, b: u8) -> Self {
Color::Rgb {
r: r as f64 / 255.0,
g: g as f64 / 255.0,
b: b as f64 / 255.0,
a: 1.0,
}
}
pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Color::Rgb {
r: r as f64 / 255.0,
g: g as f64 / 255.0,
b: b as f64 / 255.0,
a: a as f64 / 255.0,
}
}
pub fn from_hex(hex: &str) -> Option<Self> {
let hex = hex.trim_start_matches('#');
let len = hex.len();
let parse = |s: &str| u8::from_str_radix(s, 16).ok();
match len {
3 => {
let r = parse(&hex[0..1].repeat(2))?;
let g = parse(&hex[1..2].repeat(2))?;
let b = parse(&hex[2..3].repeat(2))?;
Some(Color::rgb(r, g, b))
}
6 => {
let r = parse(&hex[0..2])?;
let g = parse(&hex[2..4])?;
let b = parse(&hex[4..6])?;
Some(Color::rgb(r, g, b))
}
8 => {
let r = parse(&hex[0..2])?;
let g = parse(&hex[2..4])?;
let b = parse(&hex[4..6])?;
let a = parse(&hex[6..8])?;
Some(Color::rgba(r, g, b, a))
}
_ => None,
}
}
pub fn darken(&self, factor: f64) -> Self {
let factor = 1.0 - factor.clamp(0.0, 1.0);
match self {
Color::Rgb { r, g, b, a } => Color::Rgb {
r: r * factor,
g: g * factor,
b: b * factor,
a: *a,
},
Color::Cmyk { c, m, y, k } => Color::Cmyk {
c: c * factor,
m: m * factor,
y: y * factor,
k: k * factor,
},
Color::Luma(l) => Color::Luma(l * factor),
}
}
pub fn lighten(&self, factor: f64) -> Self {
let factor = factor.clamp(0.0, 1.0);
match self {
Color::Rgb { r, g, b, a } => Color::Rgb {
r: r + (1.0 - r) * factor,
g: g + (1.0 - g) * factor,
b: b + (1.0 - b) * factor,
a: *a,
},
Color::Cmyk { c, m, y, k } => Color::Cmyk {
c: c + (1.0 - c) * factor,
m: m + (1.0 - m) * factor,
y: y + (1.0 - y) * factor,
k: k + (1.0 - k) * factor,
},
Color::Luma(l) => Color::Luma(l + (1.0 - l) * factor),
}
}
pub fn to_typst(&self) -> String {
match self {
Color::Rgb { r, g, b, a } => {
if (*a - 1.0).abs() < 0.001 {
format!(
"rgb({}, {}, {})",
(r * 255.0).round() as u8,
(g * 255.0).round() as u8,
(b * 255.0).round() as u8
)
} else {
format!(
"rgb({}, {}, {}, {}%)",
(r * 255.0).round() as u8,
(g * 255.0).round() as u8,
(b * 255.0).round() as u8,
(a * 100.0).round() as u8
)
}
}
Color::Cmyk { c, m, y, k } => {
format!(
"cmyk({}%, {}%, {}%, {}%)",
(c * 100.0).round() as u8,
(m * 100.0).round() as u8,
(y * 100.0).round() as u8,
(k * 100.0).round() as u8
)
}
Color::Luma(l) => format!("luma({}%)", (l * 100.0).round() as u8),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HorizAlign {
Start,
End,
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VertAlign {
Top,
Horizon,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Alignment {
pub horiz: Option<HorizAlign>,
pub vert: Option<VertAlign>,
}
impl Alignment {
pub fn new(horiz: Option<HorizAlign>, vert: Option<VertAlign>) -> Self {
Self { horiz, vert }
}
pub fn to_typst(&self) -> String {
match (self.horiz, self.vert) {
(Some(h), Some(v)) => format!("{} + {}", horiz_to_str(h), vert_to_str(v)),
(Some(h), None) => horiz_to_str(h).to_string(),
(None, Some(v)) => vert_to_str(v).to_string(),
(None, None) => String::new(),
}
}
}
fn horiz_to_str(h: HorizAlign) -> &'static str {
match h {
HorizAlign::Start => "start",
HorizAlign::End => "end",
HorizAlign::Left => "left",
HorizAlign::Center => "center",
HorizAlign::Right => "right",
}
}
fn vert_to_str(v: VertAlign) -> &'static str {
match v {
VertAlign::Top => "top",
VertAlign::Horizon => "horizon",
VertAlign::Bottom => "bottom",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Ltr,
Rtl,
Ttb,
Btt,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DateTime {
pub date: Option<NaiveDate>,
pub time: Option<NaiveTime>,
}
impl DateTime {
pub fn new(date: Option<NaiveDate>, time: Option<NaiveTime>) -> Self {
Self { date, time }
}
pub fn to_typst(&self) -> String {
match (&self.date, &self.time) {
(Some(d), Some(t)) => format!(
"datetime(year: {}, month: {}, day: {}, hour: {}, minute: {}, second: {})",
d.format("%Y"),
d.format("%m"),
d.format("%d"),
t.format("%H"),
t.format("%M"),
t.format("%S")
),
(Some(d), None) => format!(
"datetime(year: {}, month: {}, day: {})",
d.format("%Y"),
d.format("%m"),
d.format("%d")
),
(None, Some(t)) => format!(
"datetime(hour: {}, minute: {}, second: {})",
t.format("%H"),
t.format("%M"),
t.format("%S")
),
(None, None) => "datetime()".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Symbol {
pub default: String,
pub accent: bool,
pub variants: Vec<(Vec<String>, String)>,
}
impl Symbol {
pub fn new(default: impl Into<String>) -> Self {
Self {
default: default.into(),
accent: false,
variants: Vec::new(),
}
}
pub fn with_accent(mut self, accent: bool) -> Self {
self.accent = accent;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Selector {
Element(String, Vec<(String, Value)>),
String(String),
Regex(WrappedRegex),
Label(String),
Or(Box<Selector>, Box<Selector>),
And(Box<Selector>, Box<Selector>),
Before(Box<Selector>, Box<Selector>),
After(Box<Selector>, Box<Selector>),
}
impl Selector {
pub fn matches(&self, node: &ContentNode) -> bool {
match self {
Selector::Element(name, filters) => {
let type_matches = match (name.as_str(), node) {
("heading", ContentNode::Heading { .. }) => true,
("strong", ContentNode::Strong(_)) => true,
("emph", ContentNode::Emph(_)) => true,
("raw", ContentNode::Raw { .. }) => true,
("math", ContentNode::Math { .. }) => true,
("list", ContentNode::ListItem(_)) => true,
("enum", ContentNode::EnumItem { .. }) => true,
("text", ContentNode::Text(_)) => true,
("cite", ContentNode::Citation { .. }) => true,
("ref", ContentNode::Reference { .. }) => true,
("label", ContentNode::LabelDef(_)) => true,
("bibliography", ContentNode::Bibliography { .. }) => true,
(elem_name, ContentNode::Element { name: n, .. }) if elem_name == n => true,
(func_name, ContentNode::FuncCall { name, .. }) if func_name == name => true,
_ => false,
};
if !type_matches {
return false;
}
if filters.is_empty() {
return true;
}
let fields = self.get_node_fields(node);
filters.iter().all(|(field_name, expected_value)| {
fields
.get(field_name)
.map(|v| v == expected_value)
.unwrap_or(false)
})
}
Selector::Label(label) => {
matches!(node, ContentNode::Label(l) if l == label)
}
Selector::String(s) => {
matches!(node, ContentNode::Text(t) if t.contains(s))
}
Selector::Regex(pattern) => {
if let ContentNode::Text(text) = node {
pattern.is_match(text)
} else {
false
}
}
Selector::Or(a, b) => a.matches(node) || b.matches(node),
Selector::And(a, b) => a.matches(node) && b.matches(node),
Selector::Before(_, _) | Selector::After(_, _) => {
false
}
}
}
fn get_node_fields(&self, node: &ContentNode) -> IndexMap<String, Value> {
match node {
ContentNode::Element { fields, .. } => fields.clone(),
ContentNode::Heading { level, content } => {
let mut fields = IndexMap::new();
fields.insert("level".to_string(), Value::Int(*level as i64));
fields.insert("body".to_string(), Value::Content(content.clone()));
fields
}
ContentNode::Raw { text, lang, block } => {
let mut fields = IndexMap::new();
fields.insert("text".to_string(), Value::Str(text.clone()));
if let Some(l) = lang {
fields.insert("lang".to_string(), Value::Str(l.clone()));
}
fields.insert("block".to_string(), Value::Bool(*block));
fields
}
ContentNode::Math { segments, block } => {
let mut fields = IndexMap::new();
fields.insert(
"body".to_string(),
Value::Str(render_math_segments_to_typst_source(segments)),
);
fields.insert("block".to_string(), Value::Bool(*block));
fields
}
ContentNode::EnumItem { number, .. } => {
let mut fields = IndexMap::new();
if let Some(n) = number {
fields.insert("number".to_string(), Value::Int(*n));
}
fields
}
ContentNode::Citation {
keys,
mode,
supplement,
} => {
let mut fields = IndexMap::new();
fields.insert(
"keys".to_string(),
Value::Array(keys.iter().cloned().map(Value::Str).collect()),
);
fields.insert(
"mode".to_string(),
Value::Str(
match mode {
CitationMode::Normal => "normal",
CitationMode::AuthorInText => "prose",
CitationMode::SuppressAuthor => "year",
CitationMode::NoParen => "author",
}
.to_string(),
),
);
if let Some(supplement) = supplement {
fields.insert("supplement".to_string(), Value::Str(supplement.clone()));
}
fields
}
ContentNode::Reference { target, ref_type } => {
let mut fields = IndexMap::new();
fields.insert("target".to_string(), Value::Str(target.clone()));
fields.insert(
"kind".to_string(),
Value::Str(
match ref_type {
ReferenceType::Basic => "basic",
ReferenceType::Named => "named",
ReferenceType::Page => "page",
ReferenceType::Equation => "equation",
}
.to_string(),
),
);
fields
}
ContentNode::LabelDef(label) => {
let mut fields = IndexMap::new();
fields.insert("label".to_string(), Value::Str(label.clone()));
fields
}
ContentNode::Bibliography { file, style } => {
let mut fields = IndexMap::new();
fields.insert("file".to_string(), Value::Str(file.clone()));
if let Some(style) = style {
fields.insert("style".to_string(), Value::Str(style.clone()));
}
fields
}
_ => IndexMap::new(),
}
}
pub fn element(name: impl Into<String>) -> Self {
Selector::Element(name.into(), Vec::new())
}
pub fn element_with_filters(name: impl Into<String>, filters: Vec<(String, Value)>) -> Self {
Selector::Element(name.into(), filters)
}
pub fn label(label: impl Into<String>) -> Self {
Selector::Label(label.into())
}
pub fn with_filter(self, field: impl Into<String>, value: Value) -> Self {
match self {
Selector::Element(name, mut filters) => {
filters.push((field.into(), value));
Selector::Element(name, filters)
}
other => other, }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Counter {
Custom(String),
Label(String),
Selector(String),
Page,
}
#[derive(Debug, Clone, PartialEq)]
pub struct State {
pub key: String,
pub init: Box<Value>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Arguments {
pub positional: Vec<Value>,
pub named: IndexMap<String, Value>,
}
impl Arguments {
pub fn new() -> Self {
Self {
positional: Vec::new(),
named: IndexMap::new(),
}
}
pub fn with_positional(mut self, args: Vec<Value>) -> Self {
self.positional = args;
self
}
pub fn with_named(mut self, named: IndexMap<String, Value>) -> Self {
self.named = named;
self
}
}
impl Default for Arguments {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct WrappedRegex(pub Regex);
impl PartialEq for WrappedRegex {
fn eq(&self, other: &Self) -> bool {
self.0.as_str() == other.0.as_str()
}
}
impl std::ops::Deref for WrappedRegex {
type Target = Regex;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Default)]
pub enum Value {
#[default]
None,
Auto,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
Length(Length),
Ratio(f64),
Angle(f64),
Fraction(f64),
Color(Color),
Alignment(Alignment),
Direction(Direction),
Symbol(Symbol),
Array(Vec<Value>),
Dict(IndexMap<String, Value>),
Func(Arc<Closure>),
Content(Vec<ContentNode>),
Regex(WrappedRegex),
DateTime(DateTime),
Label(String),
Selector(Selector),
Counter(Counter),
State(State),
Arguments(Arguments),
Module(String, IndexMap<String, Value>),
Version(Vec<u32>),
Bytes(Vec<u8>),
Type(ValType),
Styles,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValType {
None,
Auto,
Bool,
Int,
Float,
Str,
Length,
Ratio,
Angle,
Fraction,
Color,
Alignment,
Direction,
Symbol,
Array,
Dict,
Function,
Content,
Regex,
DateTime,
Label,
Selector,
Counter,
State,
Arguments,
Module,
Version,
Bytes,
Type,
Styles,
}
impl ValType {
pub fn name(&self) -> &'static str {
match self {
ValType::None => "none",
ValType::Auto => "auto",
ValType::Bool => "bool",
ValType::Int => "int",
ValType::Float => "float",
ValType::Str => "str",
ValType::Length => "length",
ValType::Ratio => "ratio",
ValType::Angle => "angle",
ValType::Fraction => "fraction",
ValType::Color => "color",
ValType::Alignment => "alignment",
ValType::Direction => "direction",
ValType::Symbol => "symbol",
ValType::Array => "array",
ValType::Dict => "dictionary",
ValType::Function => "function",
ValType::Content => "content",
ValType::Regex => "regex",
ValType::DateTime => "datetime",
ValType::Label => "label",
ValType::Selector => "selector",
ValType::Counter => "counter",
ValType::State => "state",
ValType::Arguments => "arguments",
ValType::Module => "module",
ValType::Version => "version",
ValType::Bytes => "bytes",
ValType::Type => "type",
ValType::Styles => "styles",
}
}
}
impl fmt::Display for ValType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl fmt::Debug for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::None => write!(f, "none"),
Value::Auto => write!(f, "auto"),
Value::Bool(b) => write!(f, "{}", b),
Value::Int(i) => write!(f, "{}", i),
Value::Float(v) => write!(f, "{}", v),
Value::Str(s) => write!(f, "\"{}\"", s),
Value::Length(l) => write!(f, "{}", l.to_typst()),
Value::Ratio(r) => write!(f, "{}%", r * 100.0),
Value::Angle(a) => write!(f, "{}deg", a),
Value::Fraction(fr) => write!(f, "{}fr", fr),
Value::Color(c) => write!(f, "{}", c.to_typst()),
Value::Alignment(a) => write!(f, "{}", a.to_typst()),
Value::Direction(d) => write!(f, "{:?}", d),
Value::Symbol(s) => write!(f, "{}", s.default),
Value::Array(arr) => {
write!(f, "(")?;
for (i, v) in arr.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{:?}", v)?;
}
write!(f, ")")
}
Value::Dict(dict) => {
write!(f, "(")?;
for (i, (k, v)) in dict.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}: {:?}", k, v)?;
}
write!(f, ")")
}
Value::Func(c) => write!(f, "<function({})>", c.params.join(", ")),
Value::Content(nodes) => {
write!(f, "[")?;
for node in nodes {
write!(f, "{:?}", node)?;
}
write!(f, "]")
}
Value::Regex(r) => write!(f, "regex(\"{}\")", r.as_str()),
Value::DateTime(dt) => write!(f, "{}", dt.to_typst()),
Value::Label(l) => write!(f, "<{}>", l),
Value::Selector(s) => write!(f, "{:?}", s),
Value::Counter(c) => write!(f, "{:?}", c),
Value::Arguments(a) => write!(f, "arguments({:?}, {:?})", a.positional, a.named),
Value::Module(name, _) => write!(f, "<module {}>", name),
Value::Version(v) => {
write!(f, "version(")?;
for (i, n) in v.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", n)?;
}
write!(f, ")")
}
Value::Bytes(b) => write!(f, "bytes({})", b.len()),
Value::Type(t) => write!(f, "{}", t.name()),
Value::Styles => write!(f, "<styles>"),
Value::State(s) => write!(f, "state({:?})", s.key),
}
}
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Value::None, Value::None) => true,
(Value::Auto, Value::Auto) => true,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Int(a), Value::Int(b)) => a == b,
(Value::Float(a), Value::Float(b)) => a == b,
(Value::Int(a), Value::Float(b)) => (*a as f64) == *b,
(Value::Float(a), Value::Int(b)) => *a == (*b as f64),
(Value::Str(a), Value::Str(b)) => a == b,
(Value::Length(a), Value::Length(b)) => a == b,
(Value::Ratio(a), Value::Ratio(b)) => a == b,
(Value::Angle(a), Value::Angle(b)) => a == b,
(Value::Fraction(a), Value::Fraction(b)) => a == b,
(Value::Color(a), Value::Color(b)) => a == b,
(Value::Alignment(a), Value::Alignment(b)) => a == b,
(Value::Direction(a), Value::Direction(b)) => a == b,
(Value::Symbol(a), Value::Symbol(b)) => a == b,
(Value::Array(a), Value::Array(b)) => a == b,
(Value::Dict(a), Value::Dict(b)) => a == b,
(Value::Func(a), Value::Func(b)) => Arc::ptr_eq(a, b),
(Value::Content(a), Value::Content(b)) => a == b,
(Value::Regex(a), Value::Regex(b)) => a == b,
(Value::DateTime(a), Value::DateTime(b)) => a == b,
(Value::Label(a), Value::Label(b)) => a == b,
(Value::Selector(a), Value::Selector(b)) => a == b,
(Value::Counter(a), Value::Counter(b)) => a == b,
(Value::Arguments(a), Value::Arguments(b)) => a == b,
(Value::Module(n1, m1), Value::Module(n2, m2)) => n1 == n2 && m1 == m2,
(Value::Version(a), Value::Version(b)) => a == b,
(Value::Bytes(a), Value::Bytes(b)) => a == b,
(Value::Type(a), Value::Type(b)) => a == b,
(Value::Styles, Value::Styles) => true,
_ => false,
}
}
}
impl Value {
pub fn val_type(&self) -> ValType {
match self {
Value::None => ValType::None,
Value::Auto => ValType::Auto,
Value::Bool(_) => ValType::Bool,
Value::Int(_) => ValType::Int,
Value::Float(_) => ValType::Float,
Value::Str(_) => ValType::Str,
Value::Length(_) => ValType::Length,
Value::Ratio(_) => ValType::Ratio,
Value::Angle(_) => ValType::Angle,
Value::Fraction(_) => ValType::Fraction,
Value::Color(_) => ValType::Color,
Value::Alignment(_) => ValType::Alignment,
Value::Direction(_) => ValType::Direction,
Value::Symbol(_) => ValType::Symbol,
Value::Array(_) => ValType::Array,
Value::Dict(_) => ValType::Dict,
Value::Func(_) => ValType::Function,
Value::Content(_) => ValType::Content,
Value::Regex(_) => ValType::Regex,
Value::DateTime(_) => ValType::DateTime,
Value::Label(_) => ValType::Label,
Value::Selector(_) => ValType::Selector,
Value::Counter(_) => ValType::Counter,
Value::State(_) => ValType::State,
Value::Arguments(_) => ValType::Arguments,
Value::Module(_, _) => ValType::Module,
Value::Version(_) => ValType::Version,
Value::Bytes(_) => ValType::Bytes,
Value::Type(_) => ValType::Type,
Value::Styles => ValType::Styles,
}
}
pub fn type_name(&self) -> &'static str {
self.val_type().name()
}
pub fn is_truthy(&self) -> bool {
match self {
Value::None => false,
Value::Auto => true,
Value::Bool(b) => *b,
Value::Int(i) => *i != 0,
Value::Float(f) => *f != 0.0,
Value::Str(s) => !s.is_empty(),
Value::Length(_) => true,
Value::Ratio(r) => *r != 0.0,
Value::Angle(a) => *a != 0.0,
Value::Fraction(f) => *f != 0.0,
Value::Color(_) => true,
Value::Alignment(_) => true,
Value::Direction(_) => true,
Value::Symbol(_) => true,
Value::Array(arr) => !arr.is_empty(),
Value::Dict(dict) => !dict.is_empty(),
Value::Func(_) => true,
Value::Content(nodes) => !nodes.is_empty(),
Value::Regex(_) => true,
Value::DateTime(_) => true,
Value::Label(_) => true,
Value::Selector(_) => true,
Value::Counter(_) => true,
Value::State(_) => true,
Value::Arguments(_) => true,
Value::Module(_, _) => true,
Value::Version(_) => true,
Value::Bytes(b) => !b.is_empty(),
Value::Type(_) => true,
Value::Styles => true,
}
}
pub fn as_bool(&self) -> Result<bool, EvalError> {
match self {
Value::Bool(b) => Ok(*b),
_ => Err(EvalError::type_mismatch("bool", self.type_name())),
}
}
pub fn as_int(&self) -> Result<i64, EvalError> {
match self {
Value::Int(i) => Ok(*i),
Value::Float(f) => Ok(*f as i64),
_ => Err(EvalError::type_mismatch("int", self.type_name())),
}
}
pub fn as_float(&self) -> Result<f64, EvalError> {
match self {
Value::Int(i) => Ok(*i as f64),
Value::Float(f) => Ok(*f),
Value::Ratio(r) => Ok(*r),
Value::Angle(a) => Ok(*a),
Value::Fraction(f) => Ok(*f),
_ => Err(EvalError::type_mismatch("float", self.type_name())),
}
}
pub fn as_str(&self) -> Result<&str, EvalError> {
match self {
Value::Str(s) => Ok(s),
_ => Err(EvalError::type_mismatch("str", self.type_name())),
}
}
pub fn as_array(&self) -> Result<&Vec<Value>, EvalError> {
match self {
Value::Array(arr) => Ok(arr),
_ => Err(EvalError::type_mismatch("array", self.type_name())),
}
}
pub fn as_array_mut(&mut self) -> Result<&mut Vec<Value>, EvalError> {
match self {
Value::Array(arr) => Ok(arr),
_ => Err(EvalError::type_mismatch("array", self.type_name())),
}
}
pub fn as_dict(&self) -> Result<&IndexMap<String, Value>, EvalError> {
match self {
Value::Dict(dict) => Ok(dict),
_ => Err(EvalError::type_mismatch("dictionary", self.type_name())),
}
}
pub fn as_func(&self) -> Result<&Arc<Closure>, EvalError> {
match self {
Value::Func(f) => Ok(f),
_ => Err(EvalError::type_mismatch("function", self.type_name())),
}
}
pub fn display(&self) -> String {
match self {
Value::None => String::new(),
Value::Auto => "auto".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => format_float(*f),
Value::Str(s) => s.clone(),
Value::Length(l) => l.to_typst(),
Value::Ratio(r) => format!("{}%", r * 100.0),
Value::Angle(a) => format!("{}deg", a),
Value::Fraction(f) => format!("{}fr", f),
Value::Color(c) => c.to_typst(),
Value::Alignment(a) => a.to_typst(),
Value::Direction(d) => format!("{:?}", d).to_lowercase(),
Value::Symbol(s) => s.default.clone(),
Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(|v| v.display()).collect();
format!("({})", items.join(", "))
}
Value::Dict(dict) => {
let items: Vec<String> = dict
.iter()
.map(|(k, v)| format!("{}: {}", k, v.display()))
.collect();
format!("({})", items.join(", "))
}
Value::Func(c) => format!("<function({})>", c.params.join(", ")),
Value::Content(nodes) => nodes.iter().map(|n| n.to_typst()).collect(),
Value::Regex(r) => format!("regex(\"{}\")", r.as_str()),
Value::DateTime(dt) => dt.to_typst(),
Value::Label(l) => format!("<{}>", l),
Value::Selector(_) => "<selector>".to_string(),
Value::Counter(_) => "<counter>".to_string(),
Value::State(s) => format!("state({:?})", s.key),
Value::Arguments(_) => "<arguments>".to_string(),
Value::Module(name, _) => format!("<module {}>", name),
Value::Version(v) => v
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("."),
Value::Bytes(b) => format!("bytes({})", b.len()),
Value::Type(t) => t.name().to_string(),
Value::Styles => "<styles>".to_string(),
}
}
pub fn display_in_math(&self) -> String {
value_to_typst_math_expr(self)
}
pub fn into_content(self) -> Vec<ContentNode> {
match self {
Value::Content(nodes) => nodes,
Value::None => vec![],
other => vec![ContentNode::Text(other.display())],
}
}
}
fn format_float(f: f64) -> String {
if f.fract() == 0.0 {
format!("{:.1}", f)
} else {
format!("{}", f)
}
}
#[derive(Debug, Clone)]
pub struct Closure {
pub name: Option<String>,
pub params: Vec<String>,
pub defaults: Vec<Option<String>>,
pub sink: Option<String>,
pub body_source: String,
pub captures: IndexMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ContentNode {
Text(String),
Space,
Linebreak,
Parbreak,
Strong(Vec<ContentNode>),
Emph(Vec<ContentNode>),
Raw {
text: String,
lang: Option<String>,
block: bool,
},
Math {
segments: Vec<MathSegment>,
block: bool,
},
Heading {
level: u8,
content: Vec<ContentNode>,
},
ListItem(Vec<ContentNode>),
EnumItem {
number: Option<i64>,
content: Vec<ContentNode>,
},
Element {
name: String,
fields: IndexMap<String, Value>,
},
Label(String),
Citation {
keys: Vec<String>,
mode: CitationMode,
supplement: Option<String>,
},
Reference {
target: String,
ref_type: ReferenceType,
},
LabelDef(String),
Bibliography { file: String, style: Option<String> },
FuncCall { name: String, args: Vec<Arg> },
RawSource(String),
State { key: String, default: Box<Value> },
CounterDisplay { key: String, numbering: String },
}
#[derive(Debug, Clone, PartialEq)]
pub enum Arg {
Pos(Value),
Named(String, Value),
Spread(Value),
}
#[derive(Debug, Clone, PartialEq)]
pub enum MathSegment {
Source(String),
Evaluated(Value),
}
#[derive(Debug, Clone)]
pub struct ShowRule {
pub selector: Selector,
pub transform: Arc<Closure>,
pub priority: usize,
}
impl ShowRule {
pub fn new(selector: Selector, transform: Arc<Closure>, priority: usize) -> Self {
Self {
selector,
transform,
priority,
}
}
}
pub(crate) fn normalize_ref_target_text(text: &str) -> String {
text.trim()
.trim_start_matches('<')
.trim_end_matches('>')
.trim_matches('"')
.trim_matches('\'')
.trim()
.to_string()
}
pub(crate) fn normalize_supplement_text(text: &str) -> Option<String> {
let normalized = text
.trim()
.trim_start_matches('[')
.trim_end_matches(']')
.trim_matches('"')
.trim_matches('\'')
.trim()
.to_string();
if normalized.is_empty() {
None
} else {
Some(normalized)
}
}
pub(crate) fn citation_content_value(
keys: Vec<String>,
mode: CitationMode,
supplement: Option<String>,
) -> EvalResult<Value> {
let keys: Vec<String> = keys
.into_iter()
.map(|key| normalize_ref_target_text(&key))
.filter(|key| !key.is_empty())
.collect();
if keys.is_empty() {
return Err(EvalError::argument(
"cite expects at least one label argument".to_string(),
));
}
Ok(Value::Content(vec![ContentNode::Citation {
keys,
mode,
supplement: supplement.and_then(|value| normalize_supplement_text(&value)),
}]))
}
pub(crate) fn reference_content_value(target: String, ref_type: ReferenceType) -> Value {
Value::Content(vec![ContentNode::Reference {
target: normalize_ref_target_text(&target),
ref_type,
}])
}
pub(crate) fn label_content_value(label: String) -> Value {
Value::Content(vec![ContentNode::LabelDef(normalize_ref_target_text(
&label,
))])
}
pub(crate) fn bibliography_content_value(file: String, style: Option<String>) -> EvalResult<Value> {
let file = file.trim().to_string();
if file.is_empty() {
return Err(EvalError::argument(
"bibliography expects a file argument".to_string(),
));
}
let style = style.and_then(|value| {
let trimmed = value
.trim()
.trim_matches('"')
.trim_matches('\'')
.trim()
.to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
});
Ok(Value::Content(vec![ContentNode::Bibliography {
file,
style,
}]))
}
pub(crate) fn render_math_segments_to_typst_source(segments: &[MathSegment]) -> String {
segments
.iter()
.map(|segment| match segment {
MathSegment::Source(text) => text.clone(),
MathSegment::Evaluated(value) => value_to_typst_math_expr(value),
})
.collect()
}
fn render_func_call_source(name: &str, args: &[Arg], prefix_hash: bool, in_math: bool) -> String {
let args_str: Vec<String> = args
.iter()
.map(|arg| match arg {
Arg::Pos(value) => {
if in_math {
value_to_typst_math_arg(value)
} else {
value_to_typst_arg(value)
}
}
Arg::Named(key, value) => {
let rendered = if in_math {
value_to_typst_math_arg(value)
} else {
value_to_typst_arg(value)
};
format!("{}: {}", key, rendered)
}
Arg::Spread(value) => {
let rendered = if in_math {
value_to_typst_math_arg(value)
} else {
value_to_typst_arg(value)
};
format!("..{}", rendered)
}
})
.collect();
let hash = if prefix_hash { "#" } else { "" };
format!("{}{}({})", hash, name, args_str.join(", "))
}
pub(crate) fn func_call_to_typst_source(name: &str, args: &[Arg]) -> String {
render_func_call_source(name, args, true, false)
}
fn func_call_to_typst_code_source(name: &str, args: &[Arg]) -> String {
render_func_call_source(name, args, false, false)
}
pub(crate) fn func_call_to_typst_math_source(name: &str, args: &[Arg]) -> String {
render_func_call_source(name, args, true, true)
}
fn func_call_to_typst_math_code_source(name: &str, args: &[Arg]) -> String {
render_func_call_source(name, args, false, true)
}
pub(crate) fn value_to_typst_math_expr(value: &Value) -> String {
match value {
Value::Content(nodes) => nodes.iter().map(|node| node.to_typst_in_math()).collect(),
_ => value_to_typst_arg(value),
}
}
fn value_to_typst_math_arg(value: &Value) -> String {
match value {
Value::Content(nodes) => {
if nodes.len() == 1 {
match &nodes[0] {
ContentNode::FuncCall { name, args } => {
return func_call_to_typst_math_code_source(name, args);
}
ContentNode::Math { segments, .. } => {
return render_math_segments_to_typst_source(segments);
}
_ => {}
}
}
nodes.iter().map(|node| node.to_typst_in_math()).collect()
}
_ => value_to_typst_arg(value),
}
}
impl ContentNode {
pub fn to_typst(&self) -> String {
match self {
ContentNode::Text(s) => s.clone(),
ContentNode::Space => " ".to_string(),
ContentNode::Linebreak => "\\\n".to_string(),
ContentNode::Parbreak => "\n\n".to_string(),
ContentNode::Strong(children) => {
let inner: String = children.iter().map(|c| c.to_typst()).collect();
format!("*{}*", inner)
}
ContentNode::Emph(children) => {
let inner: String = children.iter().map(|c| c.to_typst()).collect();
format!("_{}_", inner)
}
ContentNode::Raw { text, lang, block } => {
if *block {
let lang_str = lang.as_deref().unwrap_or("");
format!("```{}\n{}\n```", lang_str, text)
} else {
format!("`{}`", text)
}
}
ContentNode::Math { segments, block } => {
let content = render_math_segments_to_typst_source(segments);
if *block {
format!("$ {} $", content)
} else {
format!("${}$", content)
}
}
ContentNode::Heading { level, content } => {
let prefix = "=".repeat(*level as usize);
let inner: String = content.iter().map(|c| c.to_typst()).collect();
format!("{} {}\n", prefix, inner)
}
ContentNode::ListItem(children) => {
let inner: String = children.iter().map(|c| c.to_typst()).collect();
format!("- {}\n", inner)
}
ContentNode::EnumItem { number, content } => {
let inner: String = content.iter().map(|c| c.to_typst()).collect();
if let Some(n) = number {
format!("{}. {}\n", n, inner)
} else {
format!("+ {}\n", inner)
}
}
ContentNode::Element { name, fields } => {
let args: Vec<String> = fields
.iter()
.map(|(k, v)| format!("{}: {}", k, value_to_typst_arg(v)))
.collect();
format!("#{}({})", name, args.join(", "))
}
ContentNode::Label(l) => format!("<{}>", l),
ContentNode::Citation {
keys,
mode,
supplement,
} => {
let mut group = CiteGroup::new();
group.suffix = supplement
.clone()
.and_then(|value| normalize_supplement_text(&value));
for key in keys {
group.push(Citation::with_mode(key.clone(), *mode));
}
citation_to_typst(&group)
}
ContentNode::Reference { target, ref_type } => match ref_type {
ReferenceType::Equation => {
let target = if target.starts_with("eq-") {
target.clone()
} else {
format!("eq-{}", target)
};
format!("@{}", target)
}
ReferenceType::Page => format!("#locate(loc => {{@{}.page()}})", target),
_ => format!("#ref(<{}>)", target),
},
ContentNode::LabelDef(l) => format!("#label(<{}>)", l),
ContentNode::Bibliography { file, style } => {
if let Some(style) = style {
format!("#bibliography(\"{}\", style: {})", file, style)
} else {
format!("#bibliography(\"{}\")", file)
}
}
ContentNode::FuncCall { name, args } => func_call_to_typst_source(name, args),
ContentNode::RawSource(s) => s.clone(),
ContentNode::State { key, default } => {
format!(
"#state({:?}, {}).display()",
key,
value_to_typst_arg(default)
)
}
ContentNode::CounterDisplay { key, numbering } => {
if numbering.is_empty() {
format!("#counter({:?}).step()", key)
} else {
format!("#counter({:?}).display({:?})", key, numbering)
}
}
}
}
pub fn to_typst_in_math(&self) -> String {
match self {
ContentNode::Math { segments, .. } => render_math_segments_to_typst_source(segments),
ContentNode::FuncCall { name, args } => func_call_to_typst_math_source(name, args),
_ => self.to_typst(),
}
}
}
pub fn value_to_typst_arg(v: &Value) -> String {
match v {
Value::None => "none".to_string(),
Value::Auto => "auto".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => format_float(*f),
Value::Str(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
Value::Length(l) => l.to_typst(),
Value::Ratio(r) => format!("{}%", r * 100.0),
Value::Angle(a) => format!("{}deg", a),
Value::Fraction(f) => format!("{}fr", f),
Value::Color(c) => c.to_typst(),
Value::Alignment(a) => a.to_typst(),
Value::Direction(d) => format!("{:?}", d).to_lowercase(),
Value::Symbol(s) => s.default.clone(),
Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(value_to_typst_arg).collect();
format!("({})", items.join(", "))
}
Value::Dict(dict) => {
let items: Vec<String> = dict
.iter()
.map(|(k, v)| format!("{}: {}", k, value_to_typst_arg(v)))
.collect();
format!("({})", items.join(", "))
}
Value::Func(_) => "<function>".to_string(),
Value::Content(nodes) => {
if nodes.len() == 1 {
if let ContentNode::FuncCall { name, args } = &nodes[0] {
return func_call_to_typst_code_source(name, args);
}
}
let inner: String = nodes.iter().map(|n| n.to_typst()).collect();
format!("[{}]", inner)
}
Value::Regex(r) => format!("regex(\"{}\")", r.as_str()),
Value::DateTime(dt) => dt.to_typst(),
Value::Label(l) => format!("<{}>", l),
Value::Selector(_) => "<selector>".to_string(),
Value::Counter(_) => "<counter>".to_string(),
Value::State(s) => format!("state({:?})", s.key),
Value::Arguments(_) => "<arguments>".to_string(),
Value::Module(name, _) => format!("<module {}>", name),
Value::Version(v) => format!(
"version({})",
v.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ")
),
Value::Bytes(b) => format!("bytes({})", b.len()),
Value::Type(t) => t.name().to_string(),
Value::Styles => "<styles>".to_string(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SourceSpan {
pub start: usize,
pub end: usize,
}
impl SourceSpan {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn from_typst_span(span: typst_syntax::Span) -> Option<Self> {
let range = span.range()?;
Some(Self {
start: range.start,
end: range.end,
})
}
pub fn is_empty(&self) -> bool {
self.start == 0 && self.end == 0
}
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn extract<'a>(&self, source: &'a str) -> Option<&'a str> {
if self.end <= source.len() {
Some(&source[self.start..self.end])
} else {
None
}
}
}
impl fmt::Display for SourceSpan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}..{}", self.start, self.end)
}
}
#[derive(Debug, Clone)]
pub enum EvalErrorKind {
TypeMismatch {
expected: &'static str,
got: &'static str,
},
UndefinedVariable(String),
DivisionByZero,
InvalidOperation(String),
TooManyIterations,
RecursionLimitExceeded { max_depth: usize },
ArgumentError(String),
IndexOutOfBounds { index: i64, len: usize },
KeyNotFound(String),
SyntaxError(String),
FileNotFound(String),
ImportError(String),
RegexError(String),
Other(String),
}
impl fmt::Display for EvalErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EvalErrorKind::TypeMismatch { expected, got } => {
write!(f, "type mismatch: expected {}, got {}", expected, got)
}
EvalErrorKind::UndefinedVariable(name) => {
write!(f, "undefined variable: {}", name)
}
EvalErrorKind::DivisionByZero => write!(f, "division by zero"),
EvalErrorKind::InvalidOperation(msg) => write!(f, "invalid operation: {}", msg),
EvalErrorKind::TooManyIterations => {
write!(f, "loop seems infinite (>10000 iterations)")
}
EvalErrorKind::RecursionLimitExceeded { max_depth } => {
write!(
f,
"recursion depth exceeded maximum ({}). Possible infinite recursion.",
max_depth
)
}
EvalErrorKind::ArgumentError(msg) => write!(f, "argument error: {}", msg),
EvalErrorKind::IndexOutOfBounds { index, len } => {
write!(f, "index {} out of bounds for length {}", index, len)
}
EvalErrorKind::KeyNotFound(key) => write!(f, "key not found: {}", key),
EvalErrorKind::SyntaxError(msg) => write!(f, "syntax error: {}", msg),
EvalErrorKind::FileNotFound(path) => write!(f, "file not found: {}", path),
EvalErrorKind::ImportError(msg) => write!(f, "import error: {}", msg),
EvalErrorKind::RegexError(msg) => write!(f, "regex error: {}", msg),
EvalErrorKind::Other(msg) => write!(f, "{}", msg),
}
}
}
#[derive(Debug, Clone)]
pub struct EvalError {
pub kind: EvalErrorKind,
pub span: Option<SourceSpan>,
pub file: Option<String>,
}
impl EvalError {
pub fn new(kind: EvalErrorKind) -> Self {
Self {
kind,
span: None,
file: None,
}
}
pub fn with_span(mut self, span: SourceSpan) -> Self {
self.span = Some(span);
self
}
pub fn with_file(mut self, file: impl Into<String>) -> Self {
self.file = Some(file.into());
self
}
pub fn type_mismatch(expected: &'static str, got: &'static str) -> Self {
Self::new(EvalErrorKind::TypeMismatch { expected, got })
}
pub fn undefined(name: impl Into<String>) -> Self {
Self::new(EvalErrorKind::UndefinedVariable(name.into()))
}
pub fn div_zero() -> Self {
Self::new(EvalErrorKind::DivisionByZero)
}
pub fn invalid_op(msg: impl Into<String>) -> Self {
Self::new(EvalErrorKind::InvalidOperation(msg.into()))
}
pub fn too_many_iterations() -> Self {
Self::new(EvalErrorKind::TooManyIterations)
}
pub fn argument(msg: impl Into<String>) -> Self {
Self::new(EvalErrorKind::ArgumentError(msg.into()))
}
pub fn index_oob(index: i64, len: usize) -> Self {
Self::new(EvalErrorKind::IndexOutOfBounds { index, len })
}
pub fn key_not_found(key: impl Into<String>) -> Self {
Self::new(EvalErrorKind::KeyNotFound(key.into()))
}
pub fn syntax(msg: impl Into<String>) -> Self {
Self::new(EvalErrorKind::SyntaxError(msg.into()))
}
pub fn file_not_found(path: impl Into<String>) -> Self {
Self::new(EvalErrorKind::FileNotFound(path.into()))
}
pub fn import(msg: impl Into<String>) -> Self {
Self::new(EvalErrorKind::ImportError(msg.into()))
}
pub fn regex(msg: impl Into<String>) -> Self {
Self::new(EvalErrorKind::RegexError(msg.into()))
}
pub fn other(msg: impl Into<String>) -> Self {
Self::new(EvalErrorKind::Other(msg.into()))
}
pub fn kind(&self) -> &EvalErrorKind {
&self.kind
}
pub fn format_with_source(&self, source: &str) -> String {
let mut msg = self.kind.to_string();
if let Some(span) = &self.span {
let prefix = &source[..span.start.min(source.len())];
let line = prefix.lines().count().max(1);
let last_newline = prefix.rfind('\n').map(|i| i + 1).unwrap_or(0);
let col = span.start - last_newline + 1;
msg = format!("{}:{}: {}", line, col, msg);
if let Some(extract) = span.extract(source) {
let snippet = if extract.len() > 40 {
format!("{}...", &extract[..40])
} else {
extract.to_string()
};
msg = format!("{}\n --> `{}`", msg, snippet);
}
}
if let Some(file) = &self.file {
msg = format!("{}: {}", file, msg);
}
msg
}
}
impl fmt::Display for EvalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
if let Some(span) = &self.span {
write!(f, " at {}", span)?;
}
Ok(())
}
}
impl std::error::Error for EvalError {}
impl From<EvalErrorKind> for EvalError {
fn from(kind: EvalErrorKind) -> Self {
Self::new(kind)
}
}
pub type EvalResult<T> = Result<T, EvalError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_func_call_to_typst_uses_code_syntax_for_nested_calls() {
let nested = ContentNode::FuncCall {
name: "h".to_string(),
args: vec![Arg::Pos(Value::Content(vec![ContentNode::FuncCall {
name: "image".to_string(),
args: vec![Arg::Pos(Value::Str("x.png".to_string()))],
}]))],
};
assert_eq!(nested.to_typst(), r#"#h(image("x.png"))"#);
}
#[test]
fn test_math_nodes_serialize_preserved_func_calls_without_bracket_wrapping() {
let math = ContentNode::Math {
segments: vec![
MathSegment::Source("a ".to_string()),
MathSegment::Evaluated(Value::Content(vec![ContentNode::FuncCall {
name: "h".to_string(),
args: vec![Arg::Pos(Value::Length(Length::exact(1.0, LengthUnit::Cm)))],
}])),
MathSegment::Source(" b".to_string()),
],
block: false,
};
assert_eq!(math.to_typst(), "$a #h(1cm) b$");
assert_eq!(math.to_typst_in_math(), "a #h(1cm) b");
}
#[test]
fn test_func_call_to_typst_math_source_serializes_common_spacing_args() {
let fixed = func_call_to_typst_math_source(
"h",
&[Arg::Pos(Value::Length(Length::exact(1.0, LengthUnit::Cm)))],
);
let flex = func_call_to_typst_math_source("h", &[Arg::Pos(Value::Fraction(1.0))]);
let plain = func_call_to_typst_math_source(
"h",
&[Arg::Pos(Value::Content(vec![ContentNode::Text(
"foo".to_string(),
)]))],
);
assert_eq!(fixed, "#h(1cm)");
assert_eq!(flex, "#h(1fr)");
assert_eq!(plain, "#h(foo)");
}
}