1use std::fmt;
31
32use crate::engine::{encode_loop, is_invalid_for_xml, is_unicode_noncharacter};
33
34pub fn for_xml(input: &str) -> String {
46 crate::html::for_html(input)
47}
48
49pub fn write_xml<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
53 crate::html::write_html(out, input)
54}
55
56pub fn for_xml_content(input: &str) -> String {
69 crate::html::for_html_content(input)
70}
71
72pub fn write_xml_content<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
76 crate::html::write_html_content(out, input)
77}
78
79pub fn for_xml_attribute(input: &str) -> String {
92 crate::html::for_html_attribute(input)
93}
94
95pub fn write_xml_attribute<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
99 crate::html::write_html_attribute(out, input)
100}
101
102pub fn for_xml_comment(input: &str) -> String {
127 let mut out = String::with_capacity(input.len());
128 write_xml_comment(&mut out, input).expect("writing to string cannot fail");
129 out
130}
131
132pub fn write_xml_comment<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
136 let mut last_was_hyphen = false;
137 let mut chars = input.chars().peekable();
138
139 while let Some(c) = chars.next() {
140 if c == '-' {
141 if last_was_hyphen {
142 out.write_char('~')?;
144 last_was_hyphen = false;
145 } else if chars.peek().is_none() {
146 out.write_char('~')?;
148 } else {
149 out.write_char('-')?;
150 last_was_hyphen = true;
151 }
152 } else if is_invalid_for_xml(c) {
153 out.write_char(' ')?;
154 last_was_hyphen = false;
155 } else {
156 out.write_char(c)?;
157 last_was_hyphen = false;
158 }
159 }
160
161 Ok(())
162}
163
164pub fn for_cdata(input: &str) -> String {
186 let mut out = String::with_capacity(input.len());
187 write_cdata(&mut out, input).expect("writing to string cannot fail");
188 out
189}
190
191pub fn write_cdata<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
195 let mut bracket_count: u32 = 0;
196
197 for c in input.chars() {
198 if c == ']' {
199 bracket_count += 1;
200 } else if c == '>' && bracket_count >= 2 {
201 for _ in 0..(bracket_count - 2) {
203 out.write_char(']')?;
204 }
205 out.write_str("]]]]><![CDATA[>")?;
206 bracket_count = 0;
207 } else {
208 for _ in 0..bracket_count {
210 out.write_char(']')?;
211 }
212 bracket_count = 0;
213
214 if is_invalid_for_xml(c) {
215 out.write_char(' ')?;
216 } else {
217 out.write_char(c)?;
218 }
219 }
220 }
221
222 for _ in 0..bracket_count {
224 out.write_char(']')?;
225 }
226
227 Ok(())
228}
229
230pub fn for_xml11(input: &str) -> String {
251 let mut out = String::with_capacity(input.len());
252 write_xml11(&mut out, input).expect("writing to string cannot fail");
253 out
254}
255
256pub fn write_xml11<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
260 encode_loop(out, input, needs_xml11_encoding, write_xml11_encoded)
261}
262
263pub fn for_xml11_content(input: &str) -> String {
277 let mut out = String::with_capacity(input.len());
278 write_xml11_content(&mut out, input).expect("writing to string cannot fail");
279 out
280}
281
282pub fn write_xml11_content<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
286 encode_loop(
287 out,
288 input,
289 needs_xml11_content_encoding,
290 write_xml11_content_encoded,
291 )
292}
293
294pub fn for_xml11_attribute(input: &str) -> String {
308 let mut out = String::with_capacity(input.len());
309 write_xml11_attribute(&mut out, input).expect("writing to string cannot fail");
310 out
311}
312
313pub fn write_xml11_attribute<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
317 encode_loop(
318 out,
319 input,
320 needs_xml11_attribute_encoding,
321 write_xml11_attribute_encoded,
322 )
323}
324
325fn is_xml11_restricted_or_invalid(c: char) -> bool {
332 let cp = c as u32;
333 cp == 0
334 || (0x01..=0x08).contains(&cp)
335 || cp == 0x0B
336 || cp == 0x0C
337 || (0x0E..=0x1F).contains(&cp)
338 || (0x7F..=0x84).contains(&cp)
339 || (0x86..=0x9F).contains(&cp)
340 || is_unicode_noncharacter(cp)
341}
342
343fn needs_xml11_encoding(c: char) -> bool {
344 matches!(c, '&' | '<' | '>' | '"' | '\'') || is_xml11_restricted_or_invalid(c)
345}
346
347fn write_xml11_encoded<W: fmt::Write>(out: &mut W, c: char, _next: Option<char>) -> fmt::Result {
348 match c {
349 '&' => out.write_str("&"),
350 '<' => out.write_str("<"),
351 '>' => out.write_str(">"),
352 '"' => out.write_str("""),
353 '\'' => out.write_str("'"),
354 '\0' => out.write_char(' '),
355 c if is_unicode_noncharacter(c as u32) => out.write_char(' '),
356 c => write!(out, "&#x{:x};", c as u32),
358 }
359}
360
361fn needs_xml11_content_encoding(c: char) -> bool {
362 matches!(c, '&' | '<' | '>') || is_xml11_restricted_or_invalid(c)
363}
364
365fn write_xml11_content_encoded<W: fmt::Write>(
366 out: &mut W,
367 c: char,
368 _next: Option<char>,
369) -> fmt::Result {
370 match c {
371 '&' => out.write_str("&"),
372 '<' => out.write_str("<"),
373 '>' => out.write_str(">"),
374 '\0' => out.write_char(' '),
375 c if is_unicode_noncharacter(c as u32) => out.write_char(' '),
376 c => write!(out, "&#x{:x};", c as u32),
377 }
378}
379
380fn needs_xml11_attribute_encoding(c: char) -> bool {
381 matches!(c, '&' | '<' | '"' | '\'') || is_xml11_restricted_or_invalid(c)
382}
383
384fn write_xml11_attribute_encoded<W: fmt::Write>(
385 out: &mut W,
386 c: char,
387 _next: Option<char>,
388) -> fmt::Result {
389 match c {
390 '&' => out.write_str("&"),
391 '<' => out.write_str("<"),
392 '"' => out.write_str("""),
393 '\'' => out.write_str("'"),
394 '\0' => out.write_char(' '),
395 c if is_unicode_noncharacter(c as u32) => out.write_char(' '),
396 c => write!(out, "&#x{:x};", c as u32),
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
407 fn xml_aliases_match_html() {
408 let input = r#"<b attr="val">&</b>"#;
409 assert_eq!(for_xml(input), crate::html::for_html(input));
410 assert_eq!(for_xml_content(input), crate::html::for_html_content(input));
411 assert_eq!(
412 for_xml_attribute(input),
413 crate::html::for_html_attribute(input)
414 );
415 }
416
417 #[test]
420 fn comment_passthrough() {
421 assert_eq!(for_xml_comment("safe text"), "safe text");
422 assert_eq!(for_xml_comment(""), "");
423 }
424
425 #[test]
426 fn comment_double_hyphen() {
427 assert_eq!(for_xml_comment("a--b"), "a-~b");
428 assert_eq!(for_xml_comment("--"), "-~");
429 assert_eq!(for_xml_comment("---"), "-~~");
430 assert_eq!(for_xml_comment("----"), "-~-~");
431 assert_eq!(for_xml_comment("a--b--c"), "a-~b-~c");
432 }
433
434 #[test]
435 fn comment_trailing_hyphen() {
436 assert_eq!(for_xml_comment("trailing-"), "trailing~");
437 assert_eq!(for_xml_comment("-"), "~");
438 }
439
440 #[test]
441 fn comment_replaces_invalid_xml() {
442 assert_eq!(for_xml_comment("a\x01b"), "a b");
443 assert_eq!(for_xml_comment("a\x7Fb"), "a b");
444 }
445
446 #[test]
447 fn comment_preserves_non_ascii() {
448 assert_eq!(for_xml_comment("café"), "café");
449 }
450
451 #[test]
452 fn comment_writer_variant() {
453 let mut out = String::new();
454 write_xml_comment(&mut out, "a--b").unwrap();
455 assert_eq!(out, "a-~b");
456 }
457
458 #[test]
461 fn cdata_passthrough() {
462 assert_eq!(for_cdata("safe text"), "safe text");
463 assert_eq!(for_cdata(""), "");
464 }
465
466 #[test]
467 fn cdata_splits_closing_delimiter() {
468 assert_eq!(for_cdata("a]]>b"), "a]]]]><![CDATA[>b");
469 }
470
471 #[test]
472 fn cdata_double_split() {
473 assert_eq!(for_cdata("a]]>b]]>c"), "a]]]]><![CDATA[>b]]]]><![CDATA[>c");
474 }
475
476 #[test]
477 fn cdata_brackets_without_gt() {
478 assert_eq!(for_cdata("]]"), "]]");
479 assert_eq!(for_cdata("]"), "]");
480 assert_eq!(for_cdata("]]a"), "]]a");
481 }
482
483 #[test]
484 fn cdata_extra_brackets() {
485 assert_eq!(for_cdata("]]]>"), "]]]]]><![CDATA[>");
487 }
488
489 #[test]
490 fn cdata_replaces_invalid_xml() {
491 assert_eq!(for_cdata("a\x01b"), "a b");
492 }
493
494 #[test]
495 fn cdata_single_bracket_gt() {
496 assert_eq!(for_cdata("]>"), "]>");
498 }
499
500 #[test]
501 fn cdata_writer_variant() {
502 let mut out = String::new();
503 write_cdata(&mut out, "a]]>b").unwrap();
504 assert_eq!(out, "a]]]]><![CDATA[>b");
505 }
506
507 #[test]
510 fn xml11_encodes_entities() {
511 assert_eq!(for_xml11("<&>\"'"), "<&>"'");
512 }
513
514 #[test]
515 fn xml11_controls_as_references() {
516 assert_eq!(for_xml11("a\x01b"), "ab");
518 assert_eq!(for_xml11("a\x08b"), "ab");
519 assert_eq!(for_xml11("a\x0Bb"), "ab");
520 assert_eq!(for_xml11("a\x1Fb"), "ab");
521 }
522
523 #[test]
524 fn xml11_nel_passes_through() {
525 assert_eq!(for_xml11("a\u{0085}b"), "a\u{0085}b");
527 }
528
529 #[test]
530 fn xml11_del_and_c1_as_references() {
531 assert_eq!(for_xml11("a\x7Fb"), "ab");
532 assert_eq!(for_xml11("a\u{0080}b"), "a€b");
533 assert_eq!(for_xml11("a\u{009F}b"), "aŸb");
534 }
535
536 #[test]
537 fn xml11_nul_replaced_with_space() {
538 assert_eq!(for_xml11("a\x00b"), "a b");
539 }
540
541 #[test]
542 fn xml11_nonchars_replaced_with_space() {
543 assert_eq!(for_xml11("a\u{FDD0}b"), "a b");
544 }
545
546 #[test]
547 fn xml11_preserves_tab_lf_cr() {
548 assert_eq!(for_xml11("a\tb\nc\rd"), "a\tb\nc\rd");
549 }
550
551 #[test]
552 fn xml11_content_no_quotes() {
553 assert_eq!(for_xml11_content(r#"a"b'c"#), r#"a"b'c"#);
554 assert_eq!(for_xml11_content("a\x01b"), "ab");
555 }
556
557 #[test]
558 fn xml11_attribute_no_gt() {
559 assert_eq!(for_xml11_attribute("a>b"), "a>b");
560 assert_eq!(for_xml11_attribute("a\x01b"), "ab");
561 }
562}