1#![forbid(unsafe_code)]
4
5use rusty_xml_tree::{NodeId, NodeKind, XmlDoc};
6
7pub const XML_SAVE_FORMAT: i32 = 1 << 0;
9pub const XML_SAVE_NO_DECL: i32 = 1 << 1;
10pub const XML_SAVE_NO_EMPTY: i32 = 1 << 2;
11pub const XML_SAVE_NO_XHTML: i32 = 1 << 3;
12pub const XML_SAVE_XHTML: i32 = 1 << 4;
13pub const XML_SAVE_AS_XML: i32 = 1 << 5;
14pub const XML_SAVE_AS_HTML: i32 = 1 << 6;
15pub const XML_SAVE_WSNONSIG: i32 = 1 << 7;
16pub const XML_SAVE_EMPTY: i32 = 1 << 8;
17pub const XML_SAVE_NO_INDENT: i32 = 1 << 9;
18pub const XML_SAVE_INDENT: i32 = 1 << 10;
19
20fn hex_ref(c: u32) -> String {
21 format!("&#x{c:X};")
22}
23
24fn escape_text(s: &str, attr: bool, non_ascii: bool) -> String {
25 let mut out = String::new();
26 for c in s.chars() {
27 let u = c as u32;
28 if non_ascii && u >= 0x80 {
29 out.push_str(&hex_ref(u));
30 continue;
31 }
32 match c {
33 '&' => out.push_str("&"),
34 '<' => out.push_str("<"),
35 '>' => out.push_str(">"),
36 '"' if attr => out.push_str("""),
37 '\r' => out.push_str(" "),
38 '\t' if attr => out.push_str("	"),
39 '\n' if attr => out.push_str(" "),
40 c if u < 0x20 && c != '\t' && c != '\n' => out.push_str(&hex_ref(0xfffd)),
41 c => out.push(c),
42 }
43 }
44 out
45}
46
47fn qname(prefix: Option<&str>, local: &str) -> String {
48 match prefix {
49 Some(p) if !p.is_empty() => format!("{p}:{local}"),
50 _ => local.to_string(),
51 }
52}
53
54fn indent_unit() -> String {
55 std::env::var("XMLLINT_INDENT").unwrap_or_else(|_| " ".into())
56}
57
58fn write_indent(out: &mut String, level: i32) {
59 let unit = indent_unit();
60 for _ in 0..level {
61 out.push_str(&unit);
62 }
63}
64
65fn write_node(doc: &XmlDoc, id: NodeId, out: &mut String, opts: i32, level: i32, format: bool) {
66 match doc.kind(id) {
67 NodeKind::Element => {
68 out.push('<');
69 out.push_str(&qname(doc.prefix(id), doc.name(id)));
70 for (pre, href) in doc.ns_defs(id) {
71 out.push_str(" xmlns");
72 if let Some(p) = pre {
73 out.push(':');
74 out.push_str(p);
75 }
76 let non_ascii = doc.encoding.is_none();
77 out.push_str("=\"");
78 out.push_str(&escape_text(href, true, non_ascii));
79 out.push('"');
80 }
81 for a in doc.attrs(id) {
82 out.push(' ');
83 out.push_str(&qname(doc.prefix(a), doc.name(a)));
84 out.push_str("=\"");
85 out.push_str(&escape_text(
86 doc.content(a),
87 true,
88 doc.encoding.is_none(),
89 ));
90 out.push('"');
91 }
92 let has_kids = doc.first_child(id).is_some();
93 if !has_kids {
94 if (opts & XML_SAVE_NO_EMPTY) == 0 {
95 out.push_str("/>");
96 } else {
97 out.push_str("></");
98 out.push_str(&qname(doc.prefix(id), doc.name(id)));
99 out.push('>');
100 }
101 return;
102 }
103 let mixed = {
104 let mut c = doc.first_child(id);
105 let mut m = false;
106 while let Some(ch) = c {
107 if matches!(doc.kind(ch), NodeKind::Text | NodeKind::CData) {
108 m = true;
109 break;
110 }
111 c = doc.next_sibling(ch);
112 }
113 m
114 };
115 out.push('>');
116 let child_format = format && !mixed;
117 if child_format {
118 out.push('\n');
119 }
120 let mut c = doc.first_child(id);
121 while let Some(ch) = c {
122 if child_format {
123 write_indent(out, level + 1);
124 }
125 write_node(doc, ch, out, opts, level + 1, child_format);
126 if child_format {
127 out.push('\n');
128 }
129 c = doc.next_sibling(ch);
130 }
131 if child_format {
132 write_indent(out, level);
133 }
134 out.push_str("</");
135 out.push_str(&qname(doc.prefix(id), doc.name(id)));
136 out.push('>');
137 }
138 NodeKind::Text => {
139 out.push_str(&escape_text(
140 doc.content(id),
141 false,
142 doc.encoding.is_none(),
143 ));
144 }
145 NodeKind::CData => {
146 let content = doc.content(id);
147 if content.is_empty() {
148 out.push_str("<![CDATA[]]>");
149 } else {
150 let bytes = content.as_bytes();
152 let mut start = 0usize;
153 let mut i = 0usize;
154 while i + 2 < bytes.len() {
155 if bytes[i] == b']' && bytes[i + 1] == b']' && bytes[i + 2] == b'>' {
156 out.push_str("<![CDATA[");
157 out.push_str(&content[start..=i + 1]);
158 out.push_str("]]>");
159 start = i + 2;
160 i += 2;
161 }
162 i += 1;
163 }
164 if start < content.len() {
165 out.push_str("<![CDATA[");
166 out.push_str(&content[start..]);
167 out.push_str("]]>");
168 }
169 }
170 }
171 NodeKind::Comment => {
172 let _ = (format, level);
173 out.push_str("<!--");
174 out.push_str(doc.content(id));
175 out.push_str("-->");
176 }
177 NodeKind::Pi => {
178 let _ = (format, level);
179 out.push_str("<?");
180 out.push_str(doc.name(id));
181 if !doc.content(id).is_empty() {
182 out.push(' ');
183 out.push_str(doc.content(id));
184 }
185 out.push_str("?>");
186 }
187 _ => {}
188 }
189}
190
191#[doc(alias = "xmlSaveDoc")]
193pub fn xml_save_doc(doc: &XmlDoc, options: i32) -> Vec<u8> {
194 let mut out = String::new();
195 if (options & XML_SAVE_NO_DECL) == 0 {
196 out.push_str("<?xml version=\"");
197 out.push_str(if doc.version.is_empty() {
198 "1.0"
199 } else {
200 &doc.version
201 });
202 out.push('"');
203 if let Some(enc) = &doc.encoding {
204 out.push_str(" encoding=\"");
205 out.push_str(enc);
206 out.push('"');
207 }
208 match doc.standalone {
209 Some(true) => out.push_str(" standalone=\"yes\""),
210 Some(false) => out.push_str(" standalone=\"no\""),
211 None => {}
212 }
213 out.push_str("?>\n");
214 }
215 let format = (options & XML_SAVE_FORMAT) != 0;
216 let mut child = doc.first_child(rusty_xml_tree::NodeId::DOCUMENT);
217 while let Some(id) = child {
218 write_node(doc, id, &mut out, options, 0, format);
219 out.push('\n');
220 child = doc.next_sibling(id);
221 }
222 out.into_bytes()
223}
224
225#[doc(alias = "xmlDocDumpFormatMemory")]
227pub fn xml_doc_dump_format_memory(doc: &XmlDoc, format: bool) -> Vec<u8> {
228 xml_save_doc(doc, if format { XML_SAVE_FORMAT } else { 0 })
229}
230
231#[doc(alias = "xmlDocDumpMemory")]
233pub fn xml_doc_dump_memory(doc: &XmlDoc) -> Vec<u8> {
234 xml_save_doc(doc, 0)
235}
236
237#[doc(alias = "xmlNodeDump")]
239pub fn xml_node_dump(doc: &XmlDoc, node: NodeId, options: i32) -> Vec<u8> {
240 let mut out = String::new();
241 write_node(doc, node, &mut out, options, 0, (options & XML_SAVE_FORMAT) != 0);
242 out.into_bytes()
243}
244
245#[derive(Clone, Copy, PartialEq, Eq)]
246enum FrameKind {
247 Document,
248 Element,
249}
250
251struct Frame {
252 kind: FrameKind,
253 name: String,
254 prefix: Option<String>,
255 open: bool,
256 has_content: bool,
257}
258
259pub struct XmlTextWriter {
261 buf: String,
262 stack: Vec<Frame>,
263 indent: bool,
264 indent_unit: String,
265 started: bool,
266}
267
268impl Default for XmlTextWriter {
269 fn default() -> Self {
270 Self::xml_new_text_writer_memory()
271 }
272}
273
274impl XmlTextWriter {
275 #[doc(alias = "xmlNewTextWriterMemory")]
277 pub fn xml_new_text_writer_memory() -> Self {
278 Self {
279 buf: String::new(),
280 stack: Vec::new(),
281 indent: false,
282 indent_unit: indent_unit(),
283 started: false,
284 }
285 }
286
287 pub fn set_indent(&mut self, indent: bool) {
288 self.indent = indent;
289 }
290
291 fn close_start_tag(&mut self) {
292 if let Some(f) = self.stack.last_mut() {
293 if f.kind == FrameKind::Element && !f.open {
294 self.buf.push('>');
295 f.open = true;
296 f.has_content = true;
297 }
298 }
299 }
300
301 #[doc(alias = "xmlTextWriterStartDocument")]
303 pub fn start_document(
304 &mut self,
305 version: Option<&str>,
306 encoding: Option<&str>,
307 standalone: Option<&str>,
308 ) -> Result<(), String> {
309 self.buf.push_str("<?xml version=\"");
310 self.buf.push_str(version.unwrap_or("1.0"));
311 self.buf.push('"');
312 if let Some(e) = encoding {
313 self.buf.push_str(" encoding=\"");
314 self.buf.push_str(e);
315 self.buf.push('"');
316 }
317 if let Some(s) = standalone {
318 self.buf.push_str(" standalone=\"");
319 self.buf.push_str(s);
320 self.buf.push('"');
321 }
322 self.buf.push_str("?>\n");
323 self.stack.push(Frame {
324 kind: FrameKind::Document,
325 name: "#document".into(),
326 prefix: None,
327 open: true,
328 has_content: true,
329 });
330 self.started = true;
331 Ok(())
332 }
333
334 #[doc(alias = "xmlTextWriterStartElement")]
336 pub fn start_element(&mut self, name: &str) -> Result<(), String> {
337 self.start_element_ns(None, name, None)
338 }
339
340 #[doc(alias = "xmlTextWriterStartElementNS")]
342 pub fn start_element_ns(
343 &mut self,
344 prefix: Option<&str>,
345 name: &str,
346 ns_uri: Option<&str>,
347 ) -> Result<(), String> {
348 self.close_start_tag();
349 if self.indent && !self.buf.is_empty() && !self.buf.ends_with('\n') {
350 self.buf.push('\n');
351 }
352 if self.indent {
353 let depth = self.stack.iter().filter(|f| f.kind == FrameKind::Element).count();
354 for _ in 0..depth {
355 self.buf.push_str(&self.indent_unit);
356 }
357 }
358 self.buf.push('<');
359 self.buf.push_str(&qname(prefix, name));
360 if let Some(uri) = ns_uri {
361 if let Some(p) = prefix {
362 self.buf.push_str(" xmlns:");
363 self.buf.push_str(p);
364 } else {
365 self.buf.push_str(" xmlns");
366 }
367 self.buf.push_str("=\"");
368 self.buf.push_str(&escape_text(uri, true, false));
369 self.buf.push('"');
370 }
371 self.stack.push(Frame {
372 kind: FrameKind::Element,
373 name: name.to_string(),
374 prefix: prefix.map(str::to_string),
375 open: false,
376 has_content: false,
377 });
378 Ok(())
379 }
380
381 #[doc(alias = "xmlTextWriterWriteAttribute")]
383 pub fn write_attribute(&mut self, name: &str, value: &str) -> Result<(), String> {
384 let top = self.stack.last().ok_or_else(|| "no open element".to_string())?;
385 if top.kind != FrameKind::Element || top.open {
386 return Err("attribute after element content".into());
387 }
388 self.buf.push(' ');
389 self.buf.push_str(name);
390 self.buf.push_str("=\"");
391 self.buf.push_str(&escape_text(value, true, false));
392 self.buf.push('"');
393 Ok(())
394 }
395
396 #[doc(alias = "xmlTextWriterWriteAttributeNS")]
398 pub fn write_attribute_ns(
399 &mut self,
400 prefix: Option<&str>,
401 name: &str,
402 _ns_uri: Option<&str>,
403 value: &str,
404 ) -> Result<(), String> {
405 self.write_attribute(&qname(prefix, name), value)
406 }
407
408 #[doc(alias = "xmlTextWriterWriteString")]
410 pub fn write_string(&mut self, content: &str) -> Result<(), String> {
411 self.close_start_tag();
412 self.buf.push_str(&escape_text(content, false, false));
413 Ok(())
414 }
415
416 #[doc(alias = "xmlTextWriterWriteComment")]
418 pub fn write_comment(&mut self, content: &str) -> Result<(), String> {
419 self.close_start_tag();
420 self.buf.push_str("<!--");
421 self.buf.push_str(content);
422 self.buf.push_str("-->");
423 Ok(())
424 }
425
426 #[doc(alias = "xmlTextWriterWritePI")]
428 pub fn write_pi(&mut self, target: &str, data: Option<&str>) -> Result<(), String> {
429 self.close_start_tag();
430 self.buf.push_str("<?");
431 self.buf.push_str(target);
432 if let Some(d) = data {
433 self.buf.push(' ');
434 self.buf.push_str(d);
435 }
436 self.buf.push_str("?>");
437 Ok(())
438 }
439
440 #[doc(alias = "xmlTextWriterWriteCDATA")]
442 pub fn write_cdata(&mut self, content: &str) -> Result<(), String> {
443 self.close_start_tag();
444 self.buf.push_str("<![CDATA[");
445 self.buf.push_str(content);
446 self.buf.push_str("]]>");
447 Ok(())
448 }
449
450 #[doc(alias = "xmlTextWriterWriteRaw")]
452 pub fn write_raw(&mut self, content: &str) -> Result<(), String> {
453 self.close_start_tag();
454 self.buf.push_str(content);
455 Ok(())
456 }
457
458 #[doc(alias = "xmlTextWriterEndElement")]
460 pub fn end_element(&mut self) -> Result<(), String> {
461 let f = self.stack.pop().ok_or_else(|| "no open element".to_string())?;
462 if f.kind != FrameKind::Element {
463 return Err("end_element on document".into());
464 }
465 if !f.open {
466 self.buf.push_str("/>");
467 } else {
468 if self.indent && f.has_content {
469 }
471 self.buf.push_str("</");
472 self.buf.push_str(&qname(f.prefix.as_deref(), &f.name));
473 self.buf.push('>');
474 }
475 Ok(())
476 }
477
478 #[doc(alias = "xmlTextWriterEndDocument")]
480 pub fn end_document(&mut self) -> Result<(), String> {
481 while self
482 .stack
483 .last()
484 .map(|f| f.kind == FrameKind::Element)
485 .unwrap_or(false)
486 {
487 self.end_element()?;
488 }
489 if !self.buf.ends_with('\n') {
490 self.buf.push('\n');
491 }
492 self.stack.clear();
493 Ok(())
494 }
495
496 pub fn into_bytes(self) -> Vec<u8> {
497 self.buf.into_bytes()
498 }
499
500 pub fn as_str(&self) -> &str {
501 &self.buf
502 }
503}
504
505#[doc(alias = "xmlNewTextWriterMemory")]
507pub fn xml_new_text_writer_memory() -> XmlTextWriter {
508 XmlTextWriter::xml_new_text_writer_memory()
509}