XML reading

This commit is contained in:
Laurenz 2022-09-25 18:20:39 +02:00
parent fffb55f79a
commit f6adc45638
8 changed files with 128 additions and 29 deletions

View File

@ -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<Utf8Error> for FileError {
fn from(_: Utf8Error) -> Self {
Self::InvalidUtf8
}
}
impl From<FromUtf8Error> for FileError {
fn from(_: FromUtf8Error) -> Self {
Self::InvalidUtf8
@ -202,3 +209,33 @@ impl From<FileError> 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}"),
}
}

View File

@ -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),
}
}

View File

@ -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);

View File

@ -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::*;

View File

@ -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<Value> {
let Spanned { v: path, span } =
args.expect::<Spanned<EcoString>>("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)
}

3
tests/res/bad.xml Normal file
View File

@ -0,0 +1,3 @@
<data>
<hello name="hi">1
</data>

7
tests/res/data.xml Normal file
View File

@ -0,0 +1,7 @@
<data>
<hello name="hi">1</hello>
<data>
<hello>World</hello>
<hello>World</hello>
</data>
</data>

View File

@ -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")