pdfboss_markdown/
report.rs1use pdfboss_write::Standard14;
5use std::collections::BTreeMap;
6
7#[derive(Debug, Default, Clone, PartialEq)]
9pub struct Report {
10 pub replaced: BTreeMap<char, u32>,
12 pub skipped_html: u32,
14}
15
16impl Report {
17 pub fn is_empty(&self) -> bool {
19 self.replaced.is_empty() && self.skipped_html == 0
20 }
21
22 pub fn summary(&self) -> String {
25 let has_replaced = !self.replaced.is_empty();
26 let has_html = self.skipped_html > 0;
27
28 if !has_replaced && !has_html {
29 return String::new();
30 }
31
32 let mut parts = Vec::new();
33
34 if has_replaced {
35 let total: u32 = self.replaced.values().sum();
36 let char_plural = if total == 1 {
37 "character"
38 } else {
39 "characters"
40 };
41 let chars_str = self
42 .replaced
43 .iter()
44 .map(|(ch, count)| format!("'{}'×{}", ch, count))
45 .collect::<Vec<_>>()
46 .join(", ");
47 parts.push(format!(
48 "replaced {} {} unavailable in the standard fonts: {}",
49 total, char_plural, chars_str
50 ));
51 }
52
53 if has_html {
54 let fragment_plural = if self.skipped_html == 1 {
55 "raw html fragment"
56 } else {
57 "raw html fragments"
58 };
59 parts.push(format!("skipped {} {}", self.skipped_html, fragment_plural));
60 }
61
62 parts.join("; ")
63 }
64}
65
66pub(crate) fn sanitize(text: &str, font: Standard14, report: &mut Report) -> String {
70 text.chars()
71 .map(|ch| {
72 if ch == '\n' {
73 return ch;
74 }
75 let mut buffer = [0u8; 4];
76 let encoded = font.encode(ch.encode_utf8(&mut buffer)).is_ok();
77 if encoded && font.width(ch).is_some() {
78 return ch;
79 }
80 *report.replaced.entry(ch).or_insert(0) += 1;
81 '?'
82 })
83 .collect()
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn replaces_unencodable_chars_and_tallies() {
92 let mut report = Report::default();
93 let clean = sanitize("ok 🎉🎉 中", Standard14::Helvetica, &mut report);
94 assert_eq!(clean, "ok ?? ?");
95 assert_eq!(report.replaced.get(&'🎉'), Some(&2));
96 assert_eq!(report.replaced.get(&'中'), Some(&1));
97 }
98
99 #[test]
100 fn newline_survives_as_the_hard_break_marker() {
101 let mut report = Report::default();
102 assert_eq!(sanitize("a\nb", Standard14::Courier, &mut report), "a\nb");
103 assert!(report.is_empty());
104 }
105
106 #[test]
107 fn summary_names_chars_counts_and_html() {
108 let mut report = Report {
109 skipped_html: 1,
110 ..Report::default()
111 };
112 sanitize("中", Standard14::Helvetica, &mut report);
113 let summary = report.summary();
114 assert!(summary.contains("'中'"), "{summary}");
115 assert!(summary.contains("1 raw html fragment"), "{summary}");
116 }
117}