1use crate::{
2 Error, ExGuid, PropertySets,
3 active::{ActivePage, Changes},
4 create::{current_timestamps, properties, string},
5 document::Kind,
6 write::{PropertyObject, fresh_guid},
7};
8use serde::{Deserialize, Serialize};
9use 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.
16pub const FONT_SIZES: std::ops::RangeInclusive<f32> = 6.0..=130.0;
17
18fn 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)]
24pub 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
53impl 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.
105pub(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.
117pub(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
126fn 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
291pub(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
312pub(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}