diff --git a/src/diag.rs b/src/diag.rs index 81bb7e517..0d016a21b 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -3,6 +3,7 @@ use std::fmt::{self, Display, Formatter}; use std::io; use std::path::{Path, PathBuf}; +use std::str::Utf8Error; use std::string::FromUtf8Error; use comemo::Tracked; @@ -191,6 +192,12 @@ impl Display for FileError { } } +impl From for FileError { + fn from(_: Utf8Error) -> Self { + Self::InvalidUtf8 + } +} + impl From for FileError { fn from(_: FromUtf8Error) -> Self { Self::InvalidUtf8 @@ -202,3 +209,33 @@ impl From for String { error.to_string() } } + +/// Format a user-facing error message for an XML-like file format. +pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> String { + match error { + roxmltree::Error::UnexpectedCloseTag { expected, actual, pos } => { + format!( + "failed to parse {format}: found closing tag '{actual}' \ + instead of '{expected}' in line {}", + pos.row + ) + } + roxmltree::Error::UnknownEntityReference(entity, pos) => { + format!( + "failed to parse {format}: unknown entity '{entity}' in line {}", + pos.row + ) + } + roxmltree::Error::DuplicatedAttribute(attr, pos) => { + format!( + "failed to parse {format}: duplicate attribute '{attr}' in line {}", + pos.row + ) + } + roxmltree::Error::NoRootNode => { + format!("failed to parse {format}: missing root node") + } + roxmltree::Error::SizeLimit => "file is too large".into(), + _ => format!("failed to parse {format}"), + } +} diff --git a/src/image.rs b/src/image.rs index 717b1a938..ae4d165fe 100644 --- a/src/image.rs +++ b/src/image.rs @@ -2,7 +2,7 @@ use std::io; -use crate::diag::StrResult; +use crate::diag::{format_xml_like_error, StrResult}; use crate::util::Buffer; /// A raster or vector image. @@ -161,31 +161,6 @@ fn format_usvg_error(error: usvg::Error) -> String { usvg::Error::InvalidSize => { "failed to parse svg: width, height, or viewbox is invalid".into() } - usvg::Error::ParsingFailed(error) => match error { - roxmltree::Error::UnexpectedCloseTag { expected, actual, pos } => { - format!( - "failed to parse svg: found closing tag '{actual}' \ - instead of '{expected}' in line {}", - pos.row - ) - } - roxmltree::Error::UnknownEntityReference(entity, pos) => { - format!( - "failed to parse svg: unknown entity '{entity}' in line {}", - pos.row - ) - } - roxmltree::Error::DuplicatedAttribute(attr, pos) => { - format!( - "failed to parse svg: duplicate attribute '{attr}' in line {}", - pos.row - ) - } - roxmltree::Error::NoRootNode => { - "failed to parse svg: missing root node".into() - } - roxmltree::Error::SizeLimit => "file is too large".into(), - _ => "failed to parse svg".into(), - }, + usvg::Error::ParsingFailed(error) => format_xml_like_error("svg", error), } } diff --git a/src/library/mod.rs b/src/library/mod.rs index d806f298e..91e4671c5 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -98,6 +98,7 @@ pub fn new() -> Scope { std.def_fn("lorem", utility::lorem); std.def_fn("csv", utility::csv); std.def_fn("json", utility::json); + std.def_fn("xml", utility::xml); // Predefined colors. std.define("black", Color::BLACK); diff --git a/src/library/prelude.rs b/src/library/prelude.rs index 44d1af7ff..03bef51e1 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -9,10 +9,12 @@ pub use std::sync::Arc; pub use comemo::Tracked; pub use typst_macros::node; -pub use crate::diag::{with_alternative, At, SourceError, SourceResult, StrResult}; +pub use crate::diag::{ + with_alternative, At, FileError, FileResult, SourceError, SourceResult, StrResult, +}; pub use crate::eval::{ Arg, Args, Array, Cast, Dict, Dynamic, Func, Node, RawAlign, RawLength, RawStroke, - Scope, Smart, Value, Vm, + Scope, Smart, Str, Value, Vm, }; pub use crate::frame::*; pub use crate::geom::*; diff --git a/src/library/utility/data.rs b/src/library/utility/data.rs index e3efe6e73..0cff42c1f 100644 --- a/src/library/utility/data.rs +++ b/src/library/utility/data.rs @@ -1,5 +1,6 @@ use std::fmt::Write; +use crate::diag::format_xml_like_error; use crate::library::prelude::*; /// Read structured data from a CSV file. @@ -84,3 +85,47 @@ fn format_json_error(error: serde_json::Error) -> String { error.line() ) } + +/// Read structured data from an XML file. +pub fn xml(vm: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: path, span } = + args.expect::>("path to xml file")?; + + let path = vm.locate(&path).at(span)?; + let data = vm.world.file(&path).at(span)?; + let text = std::str::from_utf8(&data).map_err(FileError::from).at(span)?; + + let document = roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?; + + Ok(convert_xml(document.root())) +} + +/// Convert an XML node to a Typst value. +fn convert_xml(node: roxmltree::Node) -> Value { + if node.is_text() { + return Value::Str(node.text().unwrap_or_default().into()); + } + + let children: Array = node.children().map(convert_xml).collect(); + if node.is_root() { + return Value::Array(children); + } + + let tag: Str = node.tag_name().name().into(); + let attrs: Dict = node + .attributes() + .iter() + .map(|attr| (attr.name().into(), attr.value().into())) + .collect(); + + Value::Dict(dict! { + "tag" => tag, + "attrs" => attrs, + "children" => children, + }) +} + +/// Format the user-facing XML error message. +fn format_xml_error(error: roxmltree::Error) -> String { + format_xml_like_error("xml file", error) +} diff --git a/tests/res/bad.xml b/tests/res/bad.xml new file mode 100644 index 000000000..7fa6a6ede --- /dev/null +++ b/tests/res/bad.xml @@ -0,0 +1,3 @@ + + 1 + diff --git a/tests/res/data.xml b/tests/res/data.xml new file mode 100644 index 000000000..9ae409eee --- /dev/null +++ b/tests/res/data.xml @@ -0,0 +1,7 @@ + + 1 + + World + World + + diff --git a/tests/typ/utility/data.typ b/tests/typ/utility/data.typ index 4fdb84c27..e90c1b0d5 100644 --- a/tests/typ/utility/data.typ +++ b/tests/typ/utility/data.typ @@ -27,3 +27,32 @@ --- // Error: 7-22 failed to parse json file: syntax error in line 3 #json("/res/bad.json") + +--- +// Test reading XML data. +#let data = xml("/res/data.xml") +#test(data, (( + tag: "data", + attrs: (:), + children: ( + "\n ", + (tag: "hello", attrs: (name: "hi"), children: ("1",)), + "\n ", + ( + tag: "data", + attrs: (:), + children: ( + "\n ", + (tag: "hello", attrs: (:), children: ("World",)), + "\n ", + (tag: "hello", attrs: (:), children: ("World",)), + "\n ", + ), + ), + "\n", + ), +),)) + +--- +// Error: 6-20 failed to parse xml file: found closing tag 'data' instead of 'hello' in line 3 +#xml("/res/bad.xml")