1use std::collections::BTreeMap;
21
22use super::format::{automatic_styles, element, unnamed};
23use super::{Refused, is_paragraph, take_out_of_list};
24use crate::style::{Family, ListKind, Styles};
25use crate::xml::{Element, Node, Ns};
26
27pub fn heading_level(paragraph: &Element) -> Option<u8> {
29 paragraph.is(&Ns::Text, "h").then(|| {
30 paragraph
31 .attr_usize(&Ns::Text, "outline-level")
32 .and_then(|level| u8::try_from(level).ok())
33 .unwrap_or(1)
34 })
35}
36
37pub fn block_state(
41 content: &Element,
42 path: &[usize],
43 styles: &Styles,
44) -> (Option<u8>, Option<ListKind>) {
45 let Some(paragraph) = content.at(path).filter(|e| is_paragraph(e)) else {
46 return (None, None);
47 };
48 (
49 heading_level(paragraph),
50 list_kind_of(content, path, styles),
51 )
52}
53
54fn is_item(element: &Element) -> bool {
56 element.is(&Ns::Text, "list-item") || element.is(&Ns::Text, "list-header")
57}
58
59fn list_kind_of(content: &Element, path: &[usize], styles: &Styles) -> Option<ListKind> {
62 let (_, item_path) = path.split_last()?;
63 if !is_item(content.at(item_path)?) {
64 return None;
65 }
66 let mut at = item_path;
67 while let Some((_, above)) = at.split_last() {
68 at = above;
69 let Some(list) = content.at(at).filter(|e| e.is(&Ns::Text, "list")) else {
70 continue;
71 };
72 if let Some(name) = list.attr(&Ns::Text, "style-name") {
73 return Some(styles.list_kind(name).unwrap_or(ListKind::Bullet));
74 }
75 }
76 Some(ListKind::Bullet)
77}
78
79fn declares(content: &Element, spaces: &[Ns]) -> Result<(), Refused> {
81 spaces
82 .iter()
83 .all(|space| content.declares(space))
84 .then_some(())
85 .ok_or(Refused::Namespace)
86}
87
88pub fn set_heading(
104 content: &mut Element,
105 path: &[usize],
106 level: Option<u8>,
107 styles: &mut Styles,
108) -> Result<(), Refused> {
109 let paragraph = content
110 .at(path)
111 .filter(|e| is_paragraph(e))
112 .ok_or(Refused::NotFound)?;
113 let level = level.map(|level| level.clamp(1, 10));
114 if heading_level(paragraph) == level {
115 return Ok(());
116 }
117 declares(content, &[Ns::Text, Ns::Style, Ns::Fo])?;
118
119 let (style, minted) = match level {
120 Some(level) => heading_style(content, styles, level),
121 None => (body_style(content), None),
122 };
123 let name = content.name_for(&Ns::Text, if level.is_some() { "h" } else { "p" });
124 let outline = content.name_for(&Ns::Text, "outline-level");
125 let style_name = content.name_for(&Ns::Text, "style-name");
126 let paragraph = content.at_mut(path).ok_or(Refused::NotFound)?;
127 paragraph.name = name;
128 paragraph.remove_attr(&Ns::Text, "outline-level");
129 paragraph.remove_attr(&Ns::Text, "style-name");
130 if let Some(level) = level {
131 paragraph.set_attr(outline, level.to_string());
132 }
133 if let Some(style) = style {
134 paragraph.set_attr(style_name, style);
135 }
136 if let Some(minted) = minted {
137 let container = automatic_styles(content);
138 container.children.push(Node::Element(minted));
139 container.self_closing = false;
140 }
141 Ok(())
142}
143
144fn heading_style(
147 content: &Element,
148 styles: &mut Styles,
149 level: u8,
150) -> (Option<String>, Option<Element>) {
151 if let Some(own) = styles.heading_style(level) {
152 return (Some(own.to_owned()), None);
153 }
154 let mut style = element(content.name_for(&Ns::Style, "style"));
155 let set = |element: &mut Element, ns: Ns, local: &str, value: &str| {
156 element.set_attr(content.name_for(&ns, local), value);
157 };
158 set(&mut style, Ns::Style, "family", "paragraph");
159 let mut paragraph = element(content.name_for(&Ns::Style, "paragraph-properties"));
160 set(&mut paragraph, Ns::Fo, "margin-top", "0.423cm");
161 set(&mut paragraph, Ns::Fo, "margin-bottom", "0.212cm");
162 set(&mut paragraph, Ns::Fo, "keep-with-next", "always");
163 let mut text = element(content.name_for(&Ns::Style, "text-properties"));
164 let size = match level {
165 1 => "18pt",
166 2 => "14pt",
167 3 => "12pt",
168 _ => "11pt",
169 };
170 set(&mut text, Ns::Fo, "font-size", size);
171 set(&mut text, Ns::Style, "font-size-asian", size);
172 set(&mut text, Ns::Style, "font-size-complex", size);
173 set(&mut text, Ns::Fo, "font-weight", "bold");
174 set(&mut text, Ns::Style, "font-weight-asian", "bold");
175 set(&mut text, Ns::Style, "font-weight-complex", "bold");
176 for properties in [paragraph, text] {
177 style.children.push(Node::Element(properties));
178 }
179 style.self_closing = false;
180 let (name, fresh) = name_for(content, styles, style, &Family::Paragraph, "P");
181 (Some(name), fresh)
182}
183
184fn name_for(
188 content: &Element,
189 styles: &mut Styles,
190 mut style: Element,
191 family: &Family,
192 prefix: &str,
193) -> (String, Option<Element>) {
194 let wanted = unnamed(&style);
195 let family_name = style.attr(&Ns::Style, "family").map(ToOwned::to_owned);
196 if let Some(existing) = content
197 .child(&Ns::Office, "automatic-styles")
198 .into_iter()
199 .flat_map(Element::elements)
200 .filter(|s| {
201 s.is(&Ns::Style, "style") && s.attr(&Ns::Style, "family") == family_name.as_deref()
202 })
203 .find(|s| unnamed(s) == wanted)
204 .and_then(|s| s.attr(&Ns::Style, "name"))
205 {
206 return (existing.to_owned(), None);
207 }
208 let mut number = 1;
209 while styles.has_style(family, &format!("{prefix}{number}")) {
210 number += 1;
211 }
212 let name = format!("{prefix}{number}");
213 style.attrs.insert(
214 0,
215 crate::xml::Attribute {
216 name: content.name_for(&Ns::Style, "name"),
217 value: name.clone(),
218 },
219 );
220 styles.add(&style);
221 (name, Some(style))
222}
223
224fn body_style(content: &Element) -> Option<String> {
227 let body = content
228 .child(&Ns::Office, "body")
229 .and_then(|body| body.child(&Ns::Office, "text"))?;
230 let mut counts: BTreeMap<Option<&str>, usize> = BTreeMap::new();
231 for paragraph in body.elements().filter(|e| e.is(&Ns::Text, "p")) {
232 *counts
233 .entry(paragraph.attr(&Ns::Text, "style-name"))
234 .or_default() += 1;
235 }
236 counts
237 .into_iter()
238 .rev()
239 .max_by_key(|&(_, count)| count)
240 .and_then(|(style, _)| style.map(ToOwned::to_owned))
241}
242
243pub fn set_list(
263 content: &mut Element,
264 paths: &[Vec<usize>],
265 kind: Option<ListKind>,
266 styles: &mut Styles,
267) -> Result<(), Refused> {
268 for path in paths {
269 content
270 .at(path)
271 .filter(|e| is_paragraph(e))
272 .ok_or(Refused::NotFound)?;
273 }
274 declares(content, &[Ns::Text, Ns::Style, Ns::Fo])?;
275 let Some(kind) = kind else {
276 take_out(content, paths);
277 return Ok(());
278 };
279
280 let (style, minted) = list_style(content, styles, kind);
281 let listed: Vec<&Vec<usize>> = paths
282 .iter()
283 .filter(|path| list_kind_of(content, path, styles).is_some())
284 .collect();
285 let style_name = content.name_for(&Ns::Text, "style-name");
286 for path in &listed {
287 if list_kind_of(content, path, styles) != Some(kind) {
288 let item = path.len() - 1;
289 if let Some(list) = content
290 .at_mut(&path[..item - 1])
291 .filter(|e| e.is(&Ns::Text, "list"))
292 {
293 list.set_attr(style_name.clone(), style.clone());
294 }
295 }
296 }
297 let unlisted: Vec<Vec<usize>> = paths
298 .iter()
299 .filter(|path| list_kind_of(content, path, styles).is_none())
300 .cloned()
301 .collect();
302 wrap(content, &unlisted, &style);
303 if let Some(minted) = minted {
304 let container = automatic_styles(content);
305 container.children.push(Node::Element(minted));
306 container.self_closing = false;
307 }
308 Ok(())
309}
310
311fn take_out(content: &mut Element, paths: &[Vec<usize>]) {
314 let mut items: Vec<Vec<usize>> = paths
315 .iter()
316 .filter_map(|path| {
317 let (_, item) = path.split_last()?;
318 is_item(content.at(item)?).then(|| item.to_vec())
319 })
320 .collect();
321 items.sort();
322 items.dedup();
323 for item in items.iter().rev() {
324 let _ = take_out_of_list(content, item);
325 }
326}
327
328fn wrap(content: &mut Element, paths: &[Vec<usize>], style: &str) {
332 let mut runs: Vec<(Vec<usize>, Vec<usize>)> = Vec::new();
333 for path in paths {
334 let Some((&at, parent)) = path.split_last() else {
335 continue;
336 };
337 let joins = runs.last().is_some_and(|(above, indices)| {
338 above == parent
339 && indices.last().is_some_and(|&last| {
340 content.at(parent).is_some_and(|holder| {
341 holder.children[last + 1..at]
342 .iter()
343 .all(|node| matches!(node, Node::Text(t) if t.trim().is_empty()))
344 })
345 })
346 });
347 if joins {
348 if let Some((_, indices)) = runs.last_mut() {
349 indices.push(at);
350 }
351 } else {
352 runs.push((parent.to_vec(), vec![at]));
353 }
354 }
355 let list_name = content.name_for(&Ns::Text, "list");
356 let item_name = content.name_for(&Ns::Text, "list-item");
357 let style_name = content.name_for(&Ns::Text, "style-name");
358 for (parent, indices) in runs.iter().rev() {
359 let (Some(&first), Some(&last)) = (indices.first(), indices.last()) else {
360 continue;
361 };
362 let Some(holder) = content.at_mut(parent) else {
363 continue;
364 };
365 let mut wrapped = element(list_name.clone());
366 wrapped.set_attr(style_name.clone(), style);
367 wrapped.self_closing = false;
368 for node in &holder.children[first..=last] {
369 match node {
370 Node::Element(paragraph) if is_paragraph(paragraph) => {
371 let mut item = element(item_name.clone());
372 item.children.push(Node::Element(paragraph.clone()));
373 item.self_closing = false;
374 wrapped.children.push(Node::Element(item));
375 }
376 other => wrapped.children.push(other.clone()),
377 }
378 }
379 holder
380 .children
381 .splice(first..=last, [Node::Element(wrapped)]);
382 }
383}
384
385fn list_style(content: &Element, styles: &mut Styles, kind: ListKind) -> (String, Option<Element>) {
388 if let Some(own) = styles.list_style_for(kind) {
389 return (own.to_owned(), None);
390 }
391 let mut number = 1;
392 while styles.list_style(&format!("L{number}")).is_some() {
393 number += 1;
394 }
395 let name = format!("L{number}");
396 let set = |element: &mut Element, ns: Ns, local: &str, value: &str| {
397 element.set_attr(content.name_for(&ns, local), value);
398 };
399 let mut style = element(content.name_for(&Ns::Text, "list-style"));
400 set(&mut style, Ns::Style, "name", &name);
401 style.self_closing = false;
402 for level in 1..=6u8 {
403 let tag = match kind {
404 ListKind::Bullet => "list-level-style-bullet",
405 ListKind::Number => "list-level-style-number",
406 };
407 let mut entry = element(content.name_for(&Ns::Text, tag));
408 set(&mut entry, Ns::Text, "level", &level.to_string());
409 match kind {
410 ListKind::Bullet => set(&mut entry, Ns::Text, "bullet-char", "\u{2022}"),
411 ListKind::Number => {
412 set(&mut entry, Ns::Style, "num-suffix", ".");
413 set(&mut entry, Ns::Style, "num-format", "1");
414 }
415 }
416 let indent = format!("{:.3}cm", 0.635 * (f32::from(level) + 1.0));
417 let mut properties = element(content.name_for(&Ns::Style, "list-level-properties"));
418 set(
419 &mut properties,
420 Ns::Text,
421 "list-level-position-and-space-mode",
422 "label-alignment",
423 );
424 let mut alignment = element(content.name_for(&Ns::Style, "list-level-label-alignment"));
425 set(&mut alignment, Ns::Text, "label-followed-by", "listtab");
426 set(&mut alignment, Ns::Text, "list-tab-stop-position", &indent);
427 set(&mut alignment, Ns::Fo, "text-indent", "-0.635cm");
428 set(&mut alignment, Ns::Fo, "margin-left", &indent);
429 properties.children.push(Node::Element(alignment));
430 properties.self_closing = false;
431 entry.children.push(Node::Element(properties));
432 entry.self_closing = false;
433 style.children.push(Node::Element(entry));
434 }
435 styles.add(&style);
436 (name, Some(style))
437}