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 | |
| 14 | use 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. |
| 20 | pub 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("&"), |
| 25 | '<' => out.push_str("<"), |
| 26 | '>' => out.push_str(">"), |
| 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. |
| 34 | pub 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("&"), |
| 39 | '<' => out.push_str("<"), |
| 40 | '>' => out.push_str(">"), |
| 41 | '"' => out.push_str("""), |
| 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(" "), |
| 45 | '\r' => out.push_str(" "), |
| 46 | '\t' => out.push_str("	"), |
| 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. |
| 57 | pub 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. |
| 105 | fn 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)] |
| 121 | pub struct Out { |
| 122 | /// What has been written. |
| 123 | buf: String, |
| 124 | /// The elements left open, outermost first. |
| 125 | open: Vec<String>, |
| 126 | } |
| 127 | |
| 128 | impl 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 | } |