Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_text/src/xml/write.rs

6.6 KiB, 1 run

created by r1870400018:22560, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Writing XML: escaping, the five entities XML actually has, and a small emitter.
2//!
3//! This is what *creates* markup. Editing existing markup does not come through here -- it goes
4//! through [`Xml::splice`](crate::xml::Xml::splice), which replaces bytes and copies the rest.
5//!
6//! # Only five entities
7//!
8//! XML predefines `&`, `<`, `>`, `"` and `'`, and nothing else. A numeric
9//! character reference is resolved as well, since a generator writes one for anything it is unsure
10//! of. ` ` is an HTML entity and is *not* XML: [`decode`] leaves it exactly as written rather
11//! than inventing a character, because a document that says ` ` without declaring it is a
12//! document with a bug in it, and quietly fixing it here would make the bug arrive somewhere else.
13
14use oxedyne_fe2o3_core::prelude::*;
15
16/// The text with the characters that cannot stand in character data escaped.
17///
18/// `<` and `&` must be escaped; `>` need not be, and is, because `]]>` in character data is an error
19/// and escaping every `>` is the cheap way never to write one.
20pub fn escape(text: &str) -> String {
21 let mut out = String::with_capacity(text.len());
22 for c in text.chars() {
23 match c {
24 '&' => out.push_str("&amp;"),
25 '<' => out.push_str("&lt;"),
26 '>' => out.push_str("&gt;"),
27 _ => out.push(c),
28 }
29 }
30 out
31}
32
33/// The text with the characters that cannot stand in a double-quoted attribute value escaped.
34pub fn escape_attr(text: &str) -> String {
35 let mut out = String::with_capacity(text.len());
36 for c in text.chars() {
37 match c {
38 '&' => out.push_str("&amp;"),
39 '<' => out.push_str("&lt;"),
40 '>' => out.push_str("&gt;"),
41 '"' => out.push_str("&quot;"),
42 // A newline in an attribute is folded to a space by every reader, so one that is meant to
43 // survive has to be written as a reference.
44 '\n' => out.push_str("&#10;"),
45 '\r' => out.push_str("&#13;"),
46 '\t' => out.push_str("&#9;"),
47 _ => out.push(c),
48 }
49 }
50 out
51}
52
53/// The text with entity and character references resolved.
54///
55/// A reference this does not know is left exactly as it was written. See the module's own note on
56/// why that is not laxity.
57pub fn decode(text: &str) -> String {
58 if !text.contains('&') {
59 // The overwhelmingly common case, and the one worth not allocating twice for.
60 return text.to_string();
61 }
62 let mut out = String::with_capacity(text.len());
63 let b = text.as_bytes();
64 let mut i = 0;
65 while i < b.len() {
66 if b[i] != b'&' {
67 // Step by whole characters, so a multi-byte one is copied whole.
68 let c = text[i..].chars().next().unwrap_or('&');
69 out.push(c);
70 i += c.len_utf8();
71 continue;
72 }
73 let end = match text[i..].find(';') {
74 Some(k) if k <= 12 => i + k,
75 _ => {
76 out.push('&');
77 i += 1;
78 continue;
79 }
80 };
81 let body = &text[i + 1..end];
82 let c = match body {
83 "amp" => Some('&'),
84 "lt" => Some('<'),
85 "gt" => Some('>'),
86 "quot" => Some('"'),
87 "apos" => Some('\''),
88 _ => num_ref(body),
89 };
90 match c {
91 Some(c) => {
92 out.push(c);
93 i = end + 1;
94 }
95 None => {
96 out.push('&');
97 i += 1;
98 }
99 }
100 }
101 out
102}
103
104/// The character a numeric reference names, where the body of a reference is one.
105fn num_ref(body: &str) -> Option<char> {
106 let rest = body.strip_prefix('#')?;
107 let n = match rest.strip_prefix('x').or_else(|| rest.strip_prefix('X')) {
108 Some(hex) => u32::from_str_radix(hex, 16).ok()?,
109 None => rest.parse::<u32>().ok()?,
110 };
111 char::from_u32(n)
112}
113
114/// A small emitter for building well-formed XML.
115///
116/// It tracks what is open, so a tag cannot be closed that was not opened and a document cannot be
117/// finished with something still open. That is worth having because the alternative -- pushing
118/// strings into a buffer -- produces a file Word rejects with a message naming neither the part nor
119/// the element.
120#[derive(Debug, Default)]
121pub struct Out {
122 /// What has been written.
123 buf: String,
124 /// The elements left open, outermost first.
125 open: Vec<String>,
126}
127
128impl Out {
129
130 /// A new emitter, holding nothing.
131 pub fn new() -> Self {
132 Self::default()
133 }
134
135 /// A new emitter that has written the XML declaration every Office part begins with.
136 pub fn declared() -> Self {
137 let mut out = Self::new();
138 out.buf.push_str(
139 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
140 out
141 }
142
143 /// Opens an element with the given attributes, as name and value pairs.
144 pub fn open(&mut self, name: &str, attrs: &[(&str, &str)]) {
145 self.tag(name, attrs, false);
146 self.open.push(name.to_string());
147 }
148
149 /// Writes an element that holds nothing.
150 pub fn empty(&mut self, name: &str, attrs: &[(&str, &str)]) {
151 self.tag(name, attrs, true);
152 }
153
154 /// Writes an element holding one run of text.
155 pub fn leaf(&mut self, name: &str, attrs: &[(&str, &str)], text: &str) {
156 self.tag(name, attrs, false);
157 self.buf.push_str(&escape(text));
158 self.buf.push_str("</");
159 self.buf.push_str(name);
160 self.buf.push('>');
161 }
162
163 /// Adds a run of text, escaped.
164 pub fn text(&mut self, text: &str) {
165 self.buf.push_str(&escape(text));
166 }
167
168 /// Adds markup already built, exactly as it stands.
169 ///
170 /// For a fragment that came from somewhere that has already escaped it. Nothing is checked, which
171 /// is why the name says what it does.
172 pub fn raw(&mut self, markup: &str) {
173 self.buf.push_str(markup);
174 }
175
176 /// Closes the innermost open element, which must be the one named.
177 ///
178 /// A refusal leaves the emitter exactly as it was, so a caller that catches one and carries on is
179 /// not writing into a document whose stack this quietly unwound.
180 pub fn close(&mut self, name: &str) -> Outcome<()> {
181 match self.open.last() {
182 Some(open) if open == name => {}
183 Some(open) => return Err(err!(
184 "</{}> was asked for while <{}> is the innermost element open.", name, open;
185 Bug, Mismatch)),
186 None => return Err(err!(
187 "</{}> was asked for with nothing open.", name; Bug)),
188 }
189 self.open.pop();
190 self.buf.push_str("</");
191 self.buf.push_str(name);
192 self.buf.push('>');
193 Ok(())
194 }
195
196 /// The document, which must have nothing left open.
197 pub fn finish(self) -> Outcome<String> {
198 if let Some(open) = self.open.last() {
199 return Err(err!(
200 "The document was finished with <{}> still open.", open; Bug, Missing));
201 }
202 Ok(self.buf)
203 }
204
205 /// Writes a tag, open or empty.
206 fn tag(&mut self, name: &str, attrs: &[(&str, &str)], empty: bool) {
207 self.buf.push('<');
208 self.buf.push_str(name);
209 for (k, v) in attrs {
210 self.buf.push(' ');
211 self.buf.push_str(k);
212 self.buf.push_str("=\"");
213 self.buf.push_str(&escape_attr(v));
214 self.buf.push('"');
215 }
216 match empty {
217 true => self.buf.push_str("/>"),
218 false => self.buf.push('>'),
219 }
220 }
221}