mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-16 08:19:59 +00:00
fix link fields
This commit is contained in:
1 parent
42a55e6759
commit
83b76bb838
14 files changed
+345
-79
No files matched your search
@@ -17,13 +17,15 @@ pub struct FileInfo {
|
||||
pub name: String,
|
||||
pub desc: Option<String>,
|
||||
pub author: Option<Author>,
|
||||
pub link: Option<Link>,
|
||||
pub links: Vec<Link>,
|
||||
pub time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Author {
|
||||
pub name: Option<String>,
|
||||
/// `id@domain`, which the file gives as the two attributes of an `email` element.
|
||||
pub email: Option<String>,
|
||||
/// The author has a single link, unlike the other elements.
|
||||
pub link: Option<Link>,
|
||||
}
|
||||
@@ -17,7 +17,7 @@ pub struct TrackInfo {
|
||||
pub cmt: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub src: Option<String>,
|
||||
pub link: Option<Link>,
|
||||
pub links: Vec<Link>,
|
||||
pub type_: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub opacity: Option<f64>,
|
||||
|
||||
@@ -13,7 +13,7 @@ pub struct Waypoint {
|
||||
pub name: Option<String>,
|
||||
pub cmt: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub link: Option<Link>,
|
||||
pub links: Vec<Link>,
|
||||
pub sym: Option<String>,
|
||||
pub type_: Option<String>,
|
||||
}
|
||||
@@ -28,7 +28,7 @@ impl Waypoint {
|
||||
&& self.name == other.name
|
||||
&& self.cmt == other.cmt
|
||||
&& self.desc == other.desc
|
||||
&& self.link == other.link
|
||||
&& self.links == other.links
|
||||
&& self.sym == other.sym
|
||||
&& self.type_ == other.type_
|
||||
}
|
||||
|
||||
@@ -259,6 +259,47 @@ fn apply_text(
|
||||
}
|
||||
}
|
||||
|
||||
/// The email of the author, given as the `id` and `domain` attributes of the element.
|
||||
fn set_email(stack: &mut [GPXElement], attributes: Attributes<'_>) {
|
||||
let Some(GPXElement::Author(author)) = stack.last_mut() else {
|
||||
return;
|
||||
};
|
||||
let (mut id, mut domain) = (None, None);
|
||||
for attr in attributes.flatten() {
|
||||
match attr.key.as_ref() {
|
||||
"id" => id = Some(attr.value.to_string()),
|
||||
"domain" => domain = Some(attr.value.to_string()),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
if let (Some(id), Some(domain)) = (id, domain) {
|
||||
author.email = Some(format!("{id}@{domain}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The link with the given `href`, if it is an attribute of the element.
|
||||
fn parse_link(attributes: Attributes<'_>) -> Link {
|
||||
let mut link = Link::default();
|
||||
for attr in attributes.flatten() {
|
||||
if attr.key.as_ref() == "href" {
|
||||
link.href = attr.value.to_string();
|
||||
}
|
||||
}
|
||||
link
|
||||
}
|
||||
|
||||
/// Gives a link to what holds it, which is at the top of the stack.
|
||||
fn attach_link(stack: &mut [GPXElement], gpx: &mut File, link: Link) {
|
||||
match stack.last_mut() {
|
||||
// the author has a single link, the other elements have as many as they want
|
||||
Some(GPXElement::Author(author)) => author.link = Some(link),
|
||||
Some(GPXElement::Track(trk)) => trk.info.links.push(link),
|
||||
Some(GPXElement::Waypoint(wpt)) => wpt.links.push(link),
|
||||
Some(GPXElement::Metadata) => gpx.info.links.push(link),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a GPX file. The surface, highway, SAC scale and MTB scale of the trackpoints are stored as codes of
|
||||
/// `categories`, which learns the values it does not know yet.
|
||||
pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File, Error> {
|
||||
@@ -283,15 +324,8 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
||||
"desc" => stack.push(GPXElement::Description),
|
||||
"src" => stack.push(GPXElement::Source),
|
||||
"author" => stack.push(GPXElement::Author(Author::default())),
|
||||
"link" => {
|
||||
let mut link = Link::default();
|
||||
for attr in e.attributes().flatten() {
|
||||
if attr.key.as_ref() == "href" {
|
||||
link.href = attr.value.to_string();
|
||||
}
|
||||
}
|
||||
stack.push(GPXElement::Link(link));
|
||||
}
|
||||
"link" => stack.push(GPXElement::Link(parse_link(e.attributes()))),
|
||||
"email" => set_email(&mut stack, e.attributes()),
|
||||
"text" => stack.push(GPXElement::Text),
|
||||
"trk" | "rte" => stack.push(GPXElement::Track(Track::default())),
|
||||
"trkseg" => {
|
||||
@@ -332,6 +366,8 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
||||
},
|
||||
// Self-closing points (`<trkpt lat=".." lon=".."/>`), which have no children
|
||||
Ok(Event::Empty(e)) => match e.name().as_ref() {
|
||||
"link" => attach_link(&mut stack, &mut gpx, parse_link(e.attributes())),
|
||||
"email" => set_email(&mut stack, e.attributes()),
|
||||
"trkpt" => {
|
||||
if let Some(GPXElement::Segment(trkseg)) = stack.last_mut() {
|
||||
trkpt_chunk.trkpt.push(Trackpoint {
|
||||
@@ -398,18 +434,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
||||
}
|
||||
"link" => {
|
||||
if let Some(GPXElement::Link(link)) = stack.pop() {
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Author(author)) => {
|
||||
author.link = Some(link);
|
||||
}
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
trk.info.link = Some(link);
|
||||
}
|
||||
Some(GPXElement::Waypoint(wpt)) => {
|
||||
wpt.link = Some(link);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
attach_link(&mut stack, &mut gpx, link);
|
||||
}
|
||||
}
|
||||
"trk" => {
|
||||
@@ -594,8 +619,8 @@ mod tests {
|
||||
.is_some_and(|d| d == "track description")
|
||||
);
|
||||
assert!(trk.info.src.as_ref().is_some_and(|s| s == "track source"));
|
||||
assert!(trk.info.link.is_some());
|
||||
let link = trk.info.link.as_ref().unwrap();
|
||||
assert_eq!(trk.info.links.len(), 1);
|
||||
let link = &trk.info.links[0];
|
||||
assert_eq!(link.href, "https://gpx.studio");
|
||||
assert!(link.text.as_ref().is_some_and(|t| t == "track link text"));
|
||||
assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling"));
|
||||
@@ -685,6 +710,86 @@ mod tests {
|
||||
assert!(points[2].time.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_every_link_and_the_email() {
|
||||
let gpx = parse_data("with_links");
|
||||
|
||||
// the file has as many links as it says, self-closing or not
|
||||
let hrefs = |links: &[Link]| links.iter().map(|l| l.href.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
hrefs(&gpx.info.links),
|
||||
["https://example.com/file-1", "https://example.com/file-2"]
|
||||
);
|
||||
assert_eq!(gpx.info.links[0].text.as_deref(), Some("first"));
|
||||
assert_eq!(gpx.info.links[1].text, None);
|
||||
|
||||
// the author has one link and an email, which is given by two attributes
|
||||
let author = gpx.info.author.as_ref().unwrap();
|
||||
assert_eq!(author.name.as_deref(), Some("someone"));
|
||||
assert_eq!(author.email.as_deref(), Some("someone@example.com"));
|
||||
assert_eq!(
|
||||
author.link.as_ref().unwrap().href,
|
||||
"https://example.com/author"
|
||||
);
|
||||
|
||||
let waypoints: Vec<_> = gpx.wpt.iter().collect();
|
||||
assert_eq!(
|
||||
hrefs(&waypoints[0].links),
|
||||
[
|
||||
"https://example.com/wpt-1",
|
||||
"https://example.com/wpt-2",
|
||||
"https://example.com/wpt-3"
|
||||
]
|
||||
);
|
||||
assert_eq!(waypoints[0].links[2].text.as_deref(), Some("three"));
|
||||
assert!(waypoints[1].links.is_empty());
|
||||
|
||||
// tracks, and routes which are read as tracks
|
||||
assert_eq!(gpx.trk.len(), 2);
|
||||
assert_eq!(
|
||||
hrefs(&gpx.trk[0].info.links),
|
||||
["https://example.com/trk-1", "https://example.com/trk-2"]
|
||||
);
|
||||
assert_eq!(
|
||||
hrefs(&gpx.trk[1].info.links),
|
||||
["https://example.com/rte-1", "https://example.com/rte-2"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_email_variants() {
|
||||
let author = |xml: &str| {
|
||||
let data = format!("<gpx><metadata><author>{xml}</author></metadata></gpx>");
|
||||
parse(data.as_bytes(), &mut Default::default())
|
||||
.unwrap()
|
||||
.info
|
||||
.author
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
author(r#"<email id="a" domain="b.c"></email>"#)
|
||||
.email
|
||||
.as_deref(),
|
||||
Some("a@b.c")
|
||||
);
|
||||
assert_eq!(
|
||||
author(r#"<name>n</name><email id="a" domain="b.c"/>"#)
|
||||
.email
|
||||
.as_deref(),
|
||||
Some("a@b.c")
|
||||
);
|
||||
// an email that is not complete is not one
|
||||
assert_eq!(author(r#"<email id="a"/>"#).email, None);
|
||||
assert_eq!(author("<name>n</name>").email, None);
|
||||
// outside of an author it means nothing
|
||||
let gpx = parse(
|
||||
br#"<gpx><metadata><name>n</name><email id="a" domain="b.c"/></metadata></gpx>"#,
|
||||
&mut Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(gpx.info.author.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_segments() {
|
||||
let gpx = parse_data("with_segments");
|
||||
@@ -763,8 +868,8 @@ mod tests {
|
||||
.as_ref()
|
||||
.is_some_and(|d| d == "waypoint description")
|
||||
);
|
||||
assert!(wpt.link.is_some());
|
||||
let link = wpt.link.as_ref().unwrap();
|
||||
assert_eq!(wpt.links.len(), 1);
|
||||
let link = &wpt.links[0];
|
||||
assert_eq!(link.href, "https://gpx.studio");
|
||||
assert!(
|
||||
link.text
|
||||
|
||||
@@ -165,8 +165,8 @@ fn time(w: &mut XmlWriter, millis: Option<i64>) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn link(w: &mut XmlWriter, link: &Option<Link>) -> io::Result<()> {
|
||||
if let Some(link) = link.as_ref().filter(|link| !link.href.is_empty()) {
|
||||
fn link(w: &mut XmlWriter, link: &Link) -> io::Result<()> {
|
||||
if !link.href.is_empty() {
|
||||
w.create_element("link")
|
||||
.with_attribute(("href", link.href.as_str()))
|
||||
.write_inner_content(|w| optional_text(w, "text", &link.text))?;
|
||||
@@ -174,13 +174,17 @@ fn link(w: &mut XmlWriter, link: &Option<Link>) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn links(w: &mut XmlWriter, links: &[Link]) -> io::Result<()> {
|
||||
links.iter().try_for_each(|l| link(w, l))
|
||||
}
|
||||
|
||||
fn write_metadata(w: &mut XmlWriter, file: &File, options: ExportOptions) -> io::Result<()> {
|
||||
let info = &file.info;
|
||||
let time = options.time.then_some(info.time).flatten();
|
||||
if info.name.is_empty()
|
||||
&& info.desc.is_none()
|
||||
&& info.author.is_none()
|
||||
&& info.link.is_none()
|
||||
&& info.links.is_empty()
|
||||
&& time.is_none()
|
||||
{
|
||||
return Ok(());
|
||||
@@ -197,10 +201,10 @@ fn write_metadata(w: &mut XmlWriter, file: &File, options: ExportOptions) -> io:
|
||||
.with_attributes([("id", id), ("domain", domain)])
|
||||
.write_empty()?;
|
||||
}
|
||||
link(w, &author.link)
|
||||
author.link.iter().try_for_each(|l| link(w, l))
|
||||
})?;
|
||||
}
|
||||
link(w, &info.link)?;
|
||||
links(w, &info.links)?;
|
||||
self::time(w, time)
|
||||
})?;
|
||||
Ok(())
|
||||
@@ -226,7 +230,7 @@ fn write_waypoint(w: &mut XmlWriter, wpt: &Waypoint, options: ExportOptions) ->
|
||||
optional_text(w, "name", &wpt.name)?;
|
||||
optional_text(w, "cmt", &wpt.cmt)?;
|
||||
optional_text(w, "desc", &wpt.desc)?;
|
||||
link(w, &wpt.link)?;
|
||||
links(w, &wpt.links)?;
|
||||
optional_text(w, "sym", &wpt.sym)?;
|
||||
optional_text(w, "type", &wpt.type_)
|
||||
})?;
|
||||
@@ -246,7 +250,7 @@ fn write_track(
|
||||
optional_text(w, "cmt", &info.cmt)?;
|
||||
optional_text(w, "desc", &info.desc)?;
|
||||
optional_text(w, "src", &info.src)?;
|
||||
link(w, &info.link)?;
|
||||
links(w, &info.links)?;
|
||||
optional_text(w, "type", &info.type_)?;
|
||||
if info.color.is_some() || info.opacity.is_some() || info.width.is_some() {
|
||||
w.create_element("extensions").write_inner_content(|w| {
|
||||
@@ -400,10 +404,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_links_and_the_email_are_written() {
|
||||
let data = std::fs::read("data/with_links.gpx").unwrap();
|
||||
let file = parse(&data, &mut Default::default()).unwrap();
|
||||
let again = parse(
|
||||
&write(&file, &Default::default(), ExportOptions::ALL),
|
||||
&mut Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(again.info.links, file.info.links);
|
||||
assert_eq!(again.info.links.len(), 2);
|
||||
assert_eq!(again.info.author, file.info.author);
|
||||
assert_eq!(
|
||||
again.info.author.as_ref().unwrap().email.as_deref(),
|
||||
Some("someone@example.com")
|
||||
);
|
||||
let waypoints = |file: &File| file.wpt.iter().map(|w| w.links.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(waypoints(&again), waypoints(&file));
|
||||
assert_eq!(again.wpt.iter().next().unwrap().links.len(), 3);
|
||||
for (a, b) in again.trk.iter().zip(&file.trk) {
|
||||
assert_eq!(a.info.links, b.info.links);
|
||||
assert_eq!(a.info.links.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_files_survive_a_round_trip() {
|
||||
for name in [
|
||||
"simple",
|
||||
"with_links",
|
||||
"with_time",
|
||||
"with_hr",
|
||||
"with_cad",
|
||||
|
||||
Reference in new issue
Block a user