| 1 | use crate::{ |
| 2 | Error, ExGuid, PropertySets, |
| 3 | active::{ActivePage, Changes}, |
| 4 | create::{current_timestamps, properties, string}, |
| 5 | document::Kind, |
| 6 | write::{PropertyObject, fresh_guid}, |
| 7 | }; |
| 8 | use serde::{Deserialize, Serialize}; |
| 9 | use std::{ |
| 10 | collections::{BTreeMap, BTreeSet}, |
| 11 | ops::Range, |
| 12 | sync::Arc, |
| 13 | }; |
| 14 | |
| 15 | /// The font sizes OneNote accepts, in points; sizes are written in half points. |
| 16 | pub const FONT_SIZES: std::ops::RangeInclusive<f32> = 6.0..=130.0; |
| 17 | |
| 18 | fn invalid(message: &'static str) -> Error { |
| 19 | Error { offset: 0, message } |
| 20 | } |
| 21 | |
| 22 | /// An explicit character-format change; omitted attributes retain their current values. |
| 23 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
| 24 | pub enum TextAttribute { |
| 25 | Bold(bool), |
| 26 | Italic(bool), |
| 27 | Underline(bool), |
| 28 | Strike(bool), |
| 29 | /// Enabling superscript clears subscript. |
| 30 | Superscript(bool), |
| 31 | /// Enabling subscript clears superscript. |
| 32 | Subscript(bool), |
| 33 | Font(String), |
| 34 | /// Points, from 6 through 130 in half-point increments. |
| 35 | /// OneNote 2010 clamps larger sizes despite the specification allowing 144. |
| 36 | FontSize(f32), |
| 37 | /// RGB, or None for automatic text color. |
| 38 | Color(Option<[u8; 3]>), |
| 39 | /// RGB, or None to clear highlighting. |
| 40 | Highlight(Option<[u8; 3]>), |
| 41 | /// Windows language id of the run, such as 1033 for US English. |
| 42 | Language(u32), |
| 43 | /// Hidden text, such as a hyperlink's field code. |
| 44 | Hidden(bool), |
| 45 | /// The run belongs to a hyperlink field. |
| 46 | Hyperlink(bool), |
| 47 | /// The run is a hyperlink's visible label or field code rather than surrounding text. |
| 48 | HyperlinkLabel(bool), |
| 49 | /// Only false: equation text is written whole through the page model. |
| 50 | Math(bool), |
| 51 | } |
| 52 | |
| 53 | impl TextAttribute { |
| 54 | fn property(&self) -> Result<(u32, Vec<u8>), Error> { |
| 55 | let boolean = |id, value: bool| (id | (u32::from(value) << 31), Vec::new()); |
| 56 | Ok(match self { |
| 57 | Self::Bold(value) => boolean(0x08001c04, *value), |
| 58 | Self::Italic(value) => boolean(0x08001c05, *value), |
| 59 | Self::Underline(value) => boolean(0x08001c06, *value), |
| 60 | Self::Strike(value) => boolean(0x08001c07, *value), |
| 61 | Self::Superscript(value) => boolean(0x08001c08, *value), |
| 62 | Self::Subscript(value) => boolean(0x08001c09, *value), |
| 63 | Self::Hidden(value) => boolean(0x08001e16, *value), |
| 64 | Self::Hyperlink(value) => boolean(0x08001e14, *value), |
| 65 | Self::HyperlinkLabel(value) => boolean(0x08001e19, *value), |
| 66 | Self::Math(true) => { |
| 67 | return Err(invalid("Equations are written through the page model")); |
| 68 | } |
| 69 | Self::Math(false) => boolean(0x08003401, false), |
| 70 | Self::Font(font) => { |
| 71 | if font.is_empty() || font.contains('\0') { |
| 72 | return Err(invalid("Font names must be nonempty and contain no NUL")); |
| 73 | } |
| 74 | (0x1c001c0a, string(font)) |
| 75 | } |
| 76 | Self::FontSize(points) => { |
| 77 | if !points.is_finite() |
| 78 | || !FONT_SIZES.contains(points) |
| 79 | || (points * 2.0).fract() != 0.0 |
| 80 | { |
| 81 | return Err(invalid( |
| 82 | "Font size must be 6 to 130 points in half-point increments", |
| 83 | )); |
| 84 | } |
| 85 | (0x10001c0b, ((*points * 2.0) as u16).to_le_bytes().to_vec()) |
| 86 | } |
| 87 | Self::Language(language) => (0x14001c3b, language.to_le_bytes().to_vec()), |
| 88 | Self::Color(color) | Self::Highlight(color) => ( |
| 89 | if matches!(self, Self::Color(_)) { |
| 90 | 0x14001c0c |
| 91 | } else { |
| 92 | 0x14001c0d |
| 93 | }, |
| 94 | color |
| 95 | .map_or(0xff000000, |[r, g, b]| u32::from_le_bytes([r, g, b, 0])) |
| 96 | .to_le_bytes() |
| 97 | .to_vec(), |
| 98 | ), |
| 99 | }) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | /// Character formatting set over a UTF-16 range, also removing the `cleared` properties |
| 104 | /// from the range's run styles so the text inherits them. |
| 105 | pub(crate) fn format_changes( |
| 106 | active: &ActivePage<'_>, |
| 107 | object: ExGuid, |
| 108 | range: Range<u32>, |
| 109 | attributes: &[TextAttribute], |
| 110 | cleared: &[crate::op::TextProperty], |
| 111 | ) -> Result<Changes, Error> { |
| 112 | formatted(active, object, range, attributes, cleared, false) |
| 113 | } |
| 114 | |
| 115 | /// `format_changes` clearing `cleared` from whole runs, fields and equations among them, |
| 116 | /// so they inherit what their paragraph style gives, as a restyle does. |
| 117 | pub(crate) fn cleared_changes( |
| 118 | active: &ActivePage<'_>, |
| 119 | object: ExGuid, |
| 120 | range: Range<u32>, |
| 121 | cleared: &[crate::op::TextProperty], |
| 122 | ) -> Result<Changes, Error> { |
| 123 | formatted(active, object, range, &[], cleared, true) |
| 124 | } |
| 125 | |
| 126 | fn formatted( |
| 127 | active: &ActivePage<'_>, |
| 128 | object: ExGuid, |
| 129 | range: Range<u32>, |
| 130 | attributes: &[TextAttribute], |
| 131 | cleared: &[crate::op::TextProperty], |
| 132 | fields: bool, |
| 133 | ) -> Result<Changes, Error> { |
| 134 | let cleared: Vec<u32> = cleared.iter().map(|property| property.id()).collect(); |
| 135 | if (attributes.is_empty() && cleared.is_empty()) || range.start > range.end { |
| 136 | return Err(invalid( |
| 137 | "Select a text range and at least one formatting attribute", |
| 138 | )); |
| 139 | } |
| 140 | let values = match attributes { |
| 141 | [] => Vec::new(), |
| 142 | attributes => attribute_values(attributes)?, |
| 143 | }; |
| 144 | let view = &active.view; |
| 145 | let parents = active.editable_parents(object)?; |
| 146 | let node = &view.nodes[&object]; |
| 147 | let Kind::RichText { |
| 148 | text, |
| 149 | runs, |
| 150 | boilerplate, |
| 151 | .. |
| 152 | } = &node.kind |
| 153 | else { |
| 154 | return Err(invalid("Select a rich-text object")); |
| 155 | }; |
| 156 | if *boilerplate { |
| 157 | return Err(invalid( |
| 158 | "Generated title fields cannot be formatted as ordinary text", |
| 159 | )); |
| 160 | } |
| 161 | let (mut valid_start, mut valid_end) = (range.start == 0, range.end == 0); |
| 162 | let mut offset = 0; |
| 163 | for character in text.chars() { |
| 164 | offset += character.len_utf16() as u32; |
| 165 | valid_start |= offset == range.start; |
| 166 | valid_end |= offset == range.end; |
| 167 | } |
| 168 | if !valid_start || !valid_end { |
| 169 | return Err(invalid( |
| 170 | "The format range splits a surrogate pair or exceeds the text", |
| 171 | )); |
| 172 | } |
| 173 | if range.is_empty() && !text.is_empty() { |
| 174 | return Err(invalid( |
| 175 | "Select characters, or an empty paragraph's insertion style", |
| 176 | )); |
| 177 | } |
| 178 | let resolved = view.text_runs(object)?; |
| 179 | let mut segments = Vec::new(); |
| 180 | for (i, run) in runs.iter().enumerate() { |
| 181 | let selected = if text.is_empty() { |
| 182 | true |
| 183 | } else { |
| 184 | run.start < range.end && range.start < run.end |
| 185 | }; |
| 186 | if selected { |
| 187 | let format = &resolved[i].format; |
| 188 | if !fields |
| 189 | && ([format.math, format.embedded_object].contains(&Some(true)) |
| 190 | || resolved[i].text.contains('\u{fffc}')) |
| 191 | { |
| 192 | return Err(invalid( |
| 193 | "This format range contains a field or embedded data", |
| 194 | )); |
| 195 | } |
| 196 | if run.start < range.start { |
| 197 | segments.push((i, range.start, false)); |
| 198 | } |
| 199 | segments.push((i, run.end.min(range.end), true)); |
| 200 | if range.end < run.end { |
| 201 | segments.push((i, run.end, false)); |
| 202 | } |
| 203 | } else { |
| 204 | segments.push((i, run.end, false)); |
| 205 | } |
| 206 | } |
| 207 | let modified = current_timestamps()?.0.to_le_bytes(); |
| 208 | let raw = &active.live.revision; |
| 209 | let mut target = PropertyObject::from_object(&raw.objects[&object])?; |
| 210 | let fields = PropertySets::parse(&target.bytes)?; |
| 211 | if fields.sets[0].iter().any(|p| p.id == 0x24003458) { |
| 212 | return Err(invalid("This text object contains associated run objects")); |
| 213 | } |
| 214 | if segments.len() != runs.len() && fields.sets[0].iter().any(|p| p.id == 0x40003499) { |
| 215 | return Err(invalid( |
| 216 | "Formatting boundaries cannot split preserved run data", |
| 217 | )); |
| 218 | } |
| 219 | let mut styles = BTreeMap::new(); |
| 220 | let mut changed = BTreeMap::new(); |
| 221 | let mut references = Vec::new(); |
| 222 | let mut ends = Vec::new(); |
| 223 | let mut updated = false; |
| 224 | let changes: Vec<_> = values |
| 225 | .iter() |
| 226 | .map(|(id, value)| (*id, value.as_slice())) |
| 227 | .collect(); |
| 228 | for &(i, end, selected) in &segments { |
| 229 | let previous = runs[i].format; |
| 230 | let key = (previous, selected); |
| 231 | let id = if let Some(id) = styles.get(&key) { |
| 232 | *id |
| 233 | } else if let Some(id) = previous.filter(|_| !selected) { |
| 234 | id |
| 235 | } else { |
| 236 | let mut style = match previous { |
| 237 | Some(id) => PropertyObject::from_object(&raw.objects[&id])?, |
| 238 | None => PropertyObject { |
| 239 | jcid: 0x12004d, |
| 240 | bytes: properties(&[])?, |
| 241 | global_ids: Arc::new(BTreeMap::new()), |
| 242 | }, |
| 243 | }; |
| 244 | if selected { |
| 245 | style.set(&changes)?; |
| 246 | style.remove(&cleared)?; |
| 247 | } |
| 248 | if let Some(id) = previous |
| 249 | .filter(|id| raw.objects[id].data == crate::ObjectData::Properties(&style.bytes)) |
| 250 | { |
| 251 | styles.insert(key, id); |
| 252 | id |
| 253 | } else { |
| 254 | if !PropertySets::parse(&style.bytes)?.sets[0] |
| 255 | .iter() |
| 256 | .any(|p| p.id == 0x14001c3b) |
| 257 | { |
| 258 | style.set(&[( |
| 259 | 0x14001c3b, |
| 260 | &resolved[i].format.language.unwrap_or(0x409).to_le_bytes(), |
| 261 | )])?; |
| 262 | } |
| 263 | let id = ExGuid { |
| 264 | guid: fresh_guid()?, |
| 265 | n: 1, |
| 266 | }; |
| 267 | style.reference(id)?; |
| 268 | changed.insert(id, style); |
| 269 | styles.insert(key, id); |
| 270 | id |
| 271 | } |
| 272 | }; |
| 273 | updated |= selected && previous != Some(id); |
| 274 | references.extend_from_slice(&target.reference(id)?); |
| 275 | ends.extend_from_slice(&end.to_le_bytes()); |
| 276 | } |
| 277 | if !updated { |
| 278 | return Ok(BTreeMap::new()); |
| 279 | } |
| 280 | ends.truncate(ends.len() - 4); |
| 281 | target.set(&[ |
| 282 | (0x24001e13, &references), |
| 283 | (0x1c001e12, &ends), |
| 284 | (0x14001d7a, &modified), |
| 285 | ])?; |
| 286 | changed.insert(object, target); |
| 287 | touch_ancestors(raw, parents, object, &modified, &mut changed)?; |
| 288 | Ok(changed) |
| 289 | } |
| 290 | |
| 291 | pub(crate) fn touch_ancestors( |
| 292 | raw: &crate::ResolvedRevision<'_>, |
| 293 | parents: &BTreeMap<ExGuid, Vec<ExGuid>>, |
| 294 | object: ExGuid, |
| 295 | modified: &[u8; 4], |
| 296 | changed: &mut BTreeMap<ExGuid, PropertyObject>, |
| 297 | ) -> Result<(), Error> { |
| 298 | let mut pending = parents.get(&object).cloned().unwrap_or_default(); |
| 299 | let mut ancestors = BTreeSet::new(); |
| 300 | while let Some(id) = pending.pop() { |
| 301 | if !ancestors.insert(id) { |
| 302 | continue; |
| 303 | } |
| 304 | let mut ancestor = PropertyObject::from_object(&raw.objects[&id])?; |
| 305 | ancestor.set(&[(0x14001d7a, modified)])?; |
| 306 | changed.insert(id, ancestor); |
| 307 | pending.extend(parents.get(&id).into_iter().flatten().copied()); |
| 308 | } |
| 309 | Ok(()) |
| 310 | } |
| 311 | |
| 312 | pub(crate) fn attribute_values(attributes: &[TextAttribute]) -> Result<Vec<(u32, Vec<u8>)>, Error> { |
| 313 | if attributes.is_empty() { |
| 314 | return Err(invalid("Choose at least one formatting attribute")); |
| 315 | } |
| 316 | let mut values = Vec::new(); |
| 317 | let mut seen = BTreeSet::new(); |
| 318 | for attribute in attributes { |
| 319 | let (id, value) = attribute.property()?; |
| 320 | if !seen.insert(id & 0x7fffffff) { |
| 321 | return Err(invalid("Specify each formatting attribute once")); |
| 322 | } |
| 323 | values.push((id, value)); |
| 324 | } |
| 325 | if attributes.contains(&TextAttribute::Superscript(true)) |
| 326 | && attributes.contains(&TextAttribute::Subscript(true)) |
| 327 | { |
| 328 | return Err(invalid("Text cannot be both superscript and subscript")); |
| 329 | } |
| 330 | // Setting either script position clears its mutually exclusive counterpart. |
| 331 | for (set, opposite) in [(0x88001c08, 0x08001c09), (0x88001c09, 0x08001c08)] { |
| 332 | if values.iter().any(|(id, _)| *id == set) && !seen.contains(&opposite) { |
| 333 | values.push((opposite, Vec::new())); |
| 334 | } |
| 335 | } |
| 336 | Ok(values) |
| 337 | } |