Add self-closed tag support for to xml (#11577)

# Description
This PR closes #11524
Add `to xml --self-closed` flag to output empty tags as self close.
For example:

![image](https://github.com/nushell/nushell/assets/17511668/bdf040f7-8ac1-4e8b-80bb-0043d7cec7f9)


# User-Facing Changes
New `to xml` flag `--self-closed`.

# Tests + Formatting
Added new example for `to xml` command and new test for self-closed
tags.
This commit is contained in:
Artemiy 2024-01-19 14:35:29 +03:00 committed by GitHub
parent 56067da39c
commit ff290a5c3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 57 additions and 14 deletions

View File

@ -34,6 +34,11 @@ impl Command for ToXml {
"Only escape mandatory characters in text and attributes", "Only escape mandatory characters in text and attributes",
Some('p'), Some('p'),
) )
.switch(
"self-closed",
"Output empty tags as self closing",
Some('s'),
)
.category(Category::Formats) .category(Category::Formats)
} }
@ -77,6 +82,13 @@ Additionally any field which is: empty record, empty list or null, can be omitte
result: Some(Value::test_string( result: Some(Value::test_string(
r#"<note a="'qwe'\">"'</note>"# r#"<note a="'qwe'\">"'</note>"#
)) ))
},
Example {
description: "Save space using self-closed tags",
example: r#"{tag: root content: [[tag]; [a] [b] [c]]} | to xml --self-closed"#,
result: Some(Value::test_string(
r#"<root><a/><b/><c/></root>"#
))
} }
] ]
} }
@ -95,8 +107,9 @@ Additionally any field which is: empty record, empty list or null, can be omitte
let head = call.head; let head = call.head;
let indent: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "indent")?; let indent: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "indent")?;
let partial_escape = call.has_flag(engine_state, stack, "partial-escape")?; let partial_escape = call.has_flag(engine_state, stack, "partial-escape")?;
let self_closed = call.has_flag(engine_state, stack, "self-closed")?;
let job = Job::new(indent, partial_escape); let job = Job::new(indent, partial_escape, self_closed);
let input = input.try_expand_range()?; let input = input.try_expand_range()?;
job.run(input, head) job.run(input, head)
} }
@ -105,10 +118,11 @@ Additionally any field which is: empty record, empty list or null, can be omitte
struct Job { struct Job {
writer: quick_xml::Writer<Cursor<Vec<u8>>>, writer: quick_xml::Writer<Cursor<Vec<u8>>>,
partial_escape: bool, partial_escape: bool,
self_closed: bool,
} }
impl Job { impl Job {
fn new(indent: Option<Spanned<i64>>, partial_escape: bool) -> Self { fn new(indent: Option<Spanned<i64>>, partial_escape: bool, self_closed: bool) -> Self {
let writer = indent.as_ref().map_or_else( let writer = indent.as_ref().map_or_else(
|| quick_xml::Writer::new(Cursor::new(Vec::new())), || quick_xml::Writer::new(Cursor::new(Vec::new())),
|p| quick_xml::Writer::new_with_indent(Cursor::new(Vec::new()), b' ', p.item as usize), |p| quick_xml::Writer::new_with_indent(Cursor::new(Vec::new()), b' ', p.item as usize),
@ -117,6 +131,7 @@ impl Job {
Self { Self {
writer, writer,
partial_escape, partial_escape,
self_closed,
} }
} }
@ -424,12 +439,18 @@ impl Job {
}); });
} }
let self_closed = self.self_closed && children.is_empty();
let attributes = Self::parse_attributes(attrs)?; let attributes = Self::parse_attributes(attrs)?;
let mut open_tag_event = BytesStart::new(tag.clone()); let mut open_tag = BytesStart::new(tag.clone());
self.add_attributes(&mut open_tag_event, &attributes); self.add_attributes(&mut open_tag, &attributes);
let open_tag_event = if self_closed {
Event::Empty(open_tag)
} else {
Event::Start(open_tag)
};
self.writer self.writer
.write_event(Event::Start(open_tag_event)) .write_event(open_tag_event)
.map_err(|_| ShellError::CantConvert { .map_err(|_| ShellError::CantConvert {
to_type: "XML".to_string(), to_type: "XML".to_string(),
from_type: Type::Record(vec![]).to_string(), from_type: Type::Record(vec![]).to_string(),
@ -441,15 +462,18 @@ impl Job {
.into_iter() .into_iter()
.try_for_each(|child| self.write_xml_entry(child, false))?; .try_for_each(|child| self.write_xml_entry(child, false))?;
let close_tag_event = BytesEnd::new(tag); if !self_closed {
let close_tag_event = Event::End(BytesEnd::new(tag));
self.writer self.writer
.write_event(Event::End(close_tag_event)) .write_event(close_tag_event)
.map_err(|_| ShellError::CantConvert { .map_err(|_| ShellError::CantConvert {
to_type: "XML".to_string(), to_type: "XML".to_string(),
from_type: Type::Record(vec![]).to_string(), from_type: Type::Record(vec![]).to_string(),
span: entry_span, span: entry_span,
help: Some("Failure writing tag to xml".into()), help: Some("Failure writing tag to xml".into()),
}) })?;
}
Ok(())
} }
fn parse_attributes(attrs: Record) -> Result<IndexMap<String, String>, ShellError> { fn parse_attributes(attrs: Record) -> Result<IndexMap<String, String>, ShellError> {

View File

@ -91,3 +91,22 @@ fn to_xml_pi_comment_not_escaped() {
)); ));
assert_eq!(actual.out, r#"<a><?qwe "'<>&?><!--"'<>&--></a>"#); assert_eq!(actual.out, r#"<a><?qwe "'<>&?><!--"'<>&--></a>"#);
} }
#[test]
fn to_xml_self_closed() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
{
tag: root
content: [
[tag attributes content];
[a null null]
[b {e: r} null]
[c {t: y} []]
]
} | to xml --self-closed
"#
));
assert_eq!(actual.out, r#"<root><a/><b e="r"/><c t="y"/></root>"#);
}