use crate::context::KatexContext;
use crate::define_function::{FunctionDefSpec, FunctionPropSpec};
use crate::dom_tree::{HtmlDomNode, Img};
use crate::mathml_tree::{MathDomNode, MathNode, MathNodeType};
use crate::options::Options;
use crate::parser::parse_node::{
AnyParseNode, NodeType, ParseNode, ParseNodeColor, ParseNodeIncludegraphics, ParseNodeText,
ParseNodeTextOrd,
};
use crate::spacing_data::MeasurementOwned;
use crate::types::{
ArgType, CssProperty, CssStyle, Mode, ParseError, ParseErrorKind, TokenText, TrustContext,
};
use crate::units::{make_em, valid_unit};
fn is_plain_number(s: &str) -> bool {
let trimmed = s.trim();
let mut chars = trimmed.chars().peekable();
if let Some(&c) = chars.peek()
&& (c == '+' || c == '-')
{
chars.next();
}
while let Some(&c) = chars.peek() {
if c == ' ' {
chars.next();
} else {
break;
}
}
let remaining: String = chars.collect();
if remaining.is_empty() {
return false;
}
let bytes = remaining.as_bytes();
let mut i = 0;
let mut has_digit = false;
while i < bytes.len() && bytes[i].is_ascii_digit() {
has_digit = true;
i += 1;
}
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
if !has_digit && i >= bytes.len() {
return false; }
while i < bytes.len() && bytes[i].is_ascii_digit() {
has_digit = true;
i += 1;
}
}
has_digit && i == bytes.len()
}
fn parse_size_with_unit(s: &str) -> Option<(String, String, String)> {
let mut chars = s.chars().peekable();
let mut sign = String::new();
let mut number = String::new();
let mut unit = String::new();
if let Some(&c) = chars.peek()
&& (c == '+' || c == '-')
{
sign.push(c);
chars.next();
}
while let Some(&c) = chars.peek() {
if c == ' ' {
chars.next();
} else {
break;
}
}
let mut has_digit = false;
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
has_digit = true;
number.push(c);
chars.next();
} else {
break;
}
}
if chars.peek() == Some(&'.') {
number.push('.');
chars.next();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
has_digit = true;
number.push(c);
chars.next();
} else {
break;
}
}
} else if !has_digit {
if chars.peek() == Some(&'.') {
number.push('.');
chars.next();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
has_digit = true;
number.push(c);
chars.next();
} else {
break;
}
}
}
}
if !has_digit {
return None;
}
while let Some(&c) = chars.peek() {
if c == ' ' {
chars.next();
} else {
break;
}
}
for _ in 0..2 {
if let Some(&c) = chars.peek() {
if c.is_ascii_lowercase() {
unit.push(c);
chars.next();
} else {
return None;
}
} else {
return None;
}
}
while let Some(&c) = chars.peek() {
if c == ' ' {
chars.next();
} else {
break;
}
}
if chars.peek().is_some() {
return None;
}
Some((sign, number, unit))
}
fn size_data(str_val: &str) -> Result<MeasurementOwned, ParseError> {
if is_plain_number(str_val) {
let number = str_val.trim().parse::<f64>().map_err(|_| {
ParseError::new(ParseErrorKind::InvalidIncludeGraphicsNumber {
value: str_val.to_owned(),
})
})?;
return Ok(MeasurementOwned {
number,
unit: "bp".to_owned(),
});
}
if let Some((sign, number_str, unit_str)) = parse_size_with_unit(str_val) {
let number_with_sign = format!("{sign}{number_str}");
let number = number_with_sign.parse::<f64>().map_err(|_| {
ParseError::new(ParseErrorKind::InvalidIncludeGraphicsNumber {
value: str_val.to_owned(),
})
})?;
let measurement = MeasurementOwned {
number,
unit: unit_str,
};
if !valid_unit(&measurement) {
return Err(ParseError::new(
ParseErrorKind::InvalidIncludeGraphicsUnit {
unit: measurement.unit,
},
));
}
Ok(measurement)
} else {
Err(ParseError::new(
ParseErrorKind::InvalidIncludeGraphicsSize {
size: str_val.to_owned(),
},
))
}
}
fn format_unsupported_cmd(text: &str, mode: Mode, error_color: &str) -> ParseNode {
let mut textord_array: Vec<AnyParseNode> = Vec::with_capacity(text.chars().count());
for ch in text.chars() {
textord_array.push(AnyParseNode::TextOrd(ParseNodeTextOrd {
mode: Mode::Text,
loc: None,
text: TokenText::from(ch.to_string()),
}));
}
let text_node = AnyParseNode::Text(ParseNodeText {
mode,
loc: None,
body: textord_array,
font: None,
});
ParseNode::Color(ParseNodeColor {
mode,
loc: None,
color: error_color.to_owned(),
body: vec![text_node],
})
}
pub fn define_includegraphics(ctx: &mut KatexContext) {
ctx.define_function(FunctionDefSpec {
node_type: Some(NodeType::Includegraphics),
names: &["\\includegraphics"],
props: FunctionPropSpec {
num_args: 1,
num_optional_args: 1,
arg_types: Some(vec![ArgType::Raw, ArgType::Url]),
allowed_in_text: false,
..Default::default()
},
handler: Some(
|context, args: Vec<ParseNode>, opt_args: Vec<Option<ParseNode>>| {
let mut width = MeasurementOwned {
number: 0.0,
unit: "em".to_owned(),
};
let mut height = MeasurementOwned {
number: 0.9, unit: "em".to_owned(),
};
let mut total_height = MeasurementOwned {
number: 0.0,
unit: "em".to_owned(),
};
let mut alt = String::new();
if let Some(Some(opt_arg)) = opt_args.first()
&& let ParseNode::Raw(raw_node) = opt_arg
{
let attributes: Vec<&str> = raw_node.string.split(',').collect();
for attribute in attributes {
let key_val: Vec<&str> = attribute.split('=').collect();
if key_val.len() == 2 {
let key = key_val[0].trim();
let value = key_val[1].trim();
match key {
"alt" => {
value.clone_into(&mut alt);
}
"width" => {
width = size_data(value)?;
}
"height" => {
height = size_data(value)?;
}
"totalheight" => {
total_height = size_data(value)?;
}
_ => {
return Err(ParseError::new(
ParseErrorKind::InvalidIncludeGraphicsKey {
key: key.to_owned(),
},
));
}
}
}
}
}
let src = if let ParseNode::Url(url_node) = &args[0] {
url_node.url.clone()
} else {
return Err(ParseError::new(ParseErrorKind::IncludeGraphicsExpectedUrl));
};
if alt.is_empty() {
alt.clone_from(&src);
if let Some(last_slash) = alt.rfind(['/', '\\']) {
alt = alt[last_slash + 1..].to_string();
}
if let Some(last_dot) = alt.rfind('.') {
alt = alt[..last_dot].to_string();
}
}
let mut trust_ctx = TrustContext {
command: "\\includegraphics".to_owned(),
url: Some(src.clone()),
protocol: None,
class: None,
id: None,
style: None,
attributes: None,
};
if !context.parser.settings.is_trusted(&mut trust_ctx) {
return Ok(format_unsupported_cmd(
"\\includegraphics",
context.parser.mode,
&context.parser.settings.error_color,
));
}
Ok(ParseNode::Includegraphics(ParseNodeIncludegraphics {
mode: context.parser.mode,
loc: context.loc(),
alt,
width,
height,
total_height,
src,
}))
},
),
html_builder: Some(html_builder),
mathml_builder: Some(mathml_builder),
});
}
fn html_builder(
node: &ParseNode,
options: &Options,
ctx: &KatexContext,
) -> Result<HtmlDomNode, ParseError> {
if let ParseNode::Includegraphics(includegraphics_node) = node {
let height = ctx.calculate_size(&includegraphics_node.height, options)?;
let depth = if includegraphics_node.total_height.number > 0.0 {
ctx.calculate_size(&includegraphics_node.total_height, options)? - height
} else {
0.0
};
let width = if includegraphics_node.width.number > 0.0 {
ctx.calculate_size(&includegraphics_node.width, options)?
} else {
0.0
};
let mut style = CssStyle::with_capacity(3);
style.insert(CssProperty::Height, make_em(height + depth));
if width > 0.0 {
style.insert(CssProperty::Width, make_em(width));
}
if depth > 0.0 {
style.insert(CssProperty::VerticalAlign, make_em(-depth));
}
let img = Img::new(
includegraphics_node.src.clone(),
includegraphics_node.alt.clone(),
height,
depth,
0.0, style,
);
Ok(HtmlDomNode::Img(img))
} else {
Err(ParseError::new(ParseErrorKind::ExpectedNode {
node: NodeType::Includegraphics,
}))
}
}
fn mathml_builder(
node: &ParseNode,
options: &Options,
ctx: &KatexContext,
) -> Result<MathDomNode, ParseError> {
if let ParseNode::Includegraphics(includegraphics_node) = node {
let mut math_node = MathNode::builder().node_type(MathNodeType::Mglyph).build();
math_node.set_attribute("alt", includegraphics_node.alt.clone());
let height = ctx.calculate_size(&includegraphics_node.height, options)?;
let mut depth = 0.0;
if includegraphics_node.total_height.number > 0.0 {
depth = ctx.calculate_size(&includegraphics_node.total_height, options)? - height;
math_node.set_attribute("valign", make_em(-depth));
}
math_node.set_attribute("height", make_em(height + depth));
if includegraphics_node.width.number > 0.0 {
let width = ctx.calculate_size(&includegraphics_node.width, options)?;
math_node.set_attribute("width", make_em(width));
}
math_node.set_attribute("src", includegraphics_node.src.clone());
Ok(MathDomNode::Math(math_node))
} else {
Err(ParseError::new(ParseErrorKind::ExpectedNode {
node: NodeType::Includegraphics,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_size_data_plain_number() {
let result = size_data("10").unwrap();
assert_eq!(result.number, 10.0);
assert_eq!(result.unit, "bp");
}
#[test]
fn test_size_data_decimal_number() {
let result = size_data("10.5").unwrap();
assert_eq!(result.number, 10.5);
assert_eq!(result.unit, "bp");
}
#[test]
fn test_size_data_negative_number() {
let result = size_data("-5.2").unwrap();
assert_eq!(result.number, -5.2);
assert_eq!(result.unit, "bp");
}
#[test]
fn test_size_data_with_unit() {
let result = size_data("2.5em").unwrap();
assert_eq!(result.number, 2.5);
assert_eq!(result.unit, "em");
}
#[test]
fn test_size_data_with_spaces() {
let result = size_data(" 3.0 pt ").unwrap();
assert_eq!(result.number, 3.0);
assert_eq!(result.unit, "pt");
}
#[test]
fn test_size_data_with_sign_and_spaces() {
let result = size_data("+ 1.5 cm").unwrap();
assert_eq!(result.number, 1.5);
assert_eq!(result.unit, "cm");
}
#[test]
fn test_size_data_invalid_unit() {
let result = size_data("10xx");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid unit"));
}
#[test]
fn test_size_data_invalid_format() {
let result = size_data("abc");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid size"));
}
}