1use super::colors::normalize_hex_color;
2use crate::model::DocumentCompatibilitySettings;
3use crate::package::OoxmlPackage;
4use crate::xml::{
5 extract_balanced_tag_blocks, get_attribute, parse_integer_attribute, parse_on_off_attribute,
6};
7
8pub fn parse_document_default_tab_stop_twips(pkg: &OoxmlPackage) -> Option<i64> {
9 let settings_xml = crate::package::related_document_part(pkg, "settings")?
10 .content
11 .as_str();
12 let tab_stop_tag = super::scan::find_tag_token(settings_xml, "w:defaultTabStop")?;
13 parse_integer_attribute(&tab_stop_tag, "w:val").filter(|value| *value >= 0)
14}
15
16fn extract_body_xml(document_xml: &str) -> &str {
18 if let Some(body) = crate::xml_index::unique_inner_xml(document_xml, "w:body") {
19 if !body.is_empty() {
20 return body;
21 }
22 }
23 if let Some(start) = find_subsequence_ignore_ascii_case(document_xml, "<w:body") {
24 let after_open = &document_xml[start..];
25 let open_end = after_open.find('>').map(|index| start + index + 1);
26 let close_start = find_subsequence_ignore_ascii_case(document_xml, "</w:body>");
27 if let (Some(open_end), Some(close_start)) = (open_end, close_start) {
28 if open_end < close_start {
29 return &document_xml[open_end..close_start];
30 }
31 }
32 }
33 document_xml
34}
35
36pub fn parse_document_background_color(document_xml: &str) -> Option<String> {
38 let background_tag = super::scan::find_tag_token(document_xml, "w:background")?;
39 let color_value = find_namespaced_attribute_value(&background_tag, "w:color");
40 normalize_hex_color(color_value.as_deref())
41}
42
43pub fn extract_document_open_tag(document_xml: &str) -> Option<String> {
45 let start = find_subsequence_ignore_ascii_case(document_xml, "<w:document")?;
46 let after_open = &document_xml[start..];
47 let end = after_open.find('>')? + start + 1;
48 Some(document_xml[start..end].to_string())
49}
50
51pub fn extract_section_properties_xml(document_xml: &str) -> Option<String> {
53 let body_xml = extract_body_xml(document_xml);
54 let blocks = extract_balanced_tag_blocks(body_xml, "w:sectPr");
55 blocks.into_iter().last()
56}
57
58pub fn parse_document_page_count_from_app_properties(pkg: &OoxmlPackage) -> Option<i64> {
60 let app_xml = pkg
61 .parts
62 .get("docProps/app.xml")
63 .map(|part| part.content.as_str())
64 .unwrap_or("");
65
66 if app_xml.is_empty() {
67 return None;
68 }
69
70 let pages_raw = extract_simple_tag_text(app_xml, "Pages")?;
71 let parsed = pages_raw.trim().parse::<i64>().ok()?;
72 if parsed > 0 {
73 Some(parsed)
74 } else {
75 None
76 }
77}
78
79fn parse_first_on_off_setting(settings_xml: &str, tag_names: &[&str]) -> Option<bool> {
81 for tag_name in tag_names {
82 if let Some(tag) =
83 super::styles::direct_table_property(settings_xml, &format!("w:{tag_name}"))
84 {
85 return parse_on_off_attribute(tag, tag_name);
86 }
87 }
88 None
89}
90
91pub fn parse_document_compatibility_settings(
93 pkg: &OoxmlPackage,
94) -> Option<DocumentCompatibilitySettings> {
95 let settings_xml = crate::package::related_document_part(pkg, "settings")
96 .map(|part| part.content.as_str())
97 .unwrap_or("");
98
99 if settings_xml.is_empty() {
100 return None;
101 }
102
103 let settings_root = extract_balanced_tag_blocks(settings_xml, "w:settings")
104 .into_iter()
105 .next()
106 .unwrap_or_else(|| settings_xml.to_string());
107 let compat_xml =
108 super::styles::direct_table_property(&settings_root, "w:compat").unwrap_or_default();
109
110 let compatibility_mode = super::scan::find_all_tag_tokens(&compat_xml, "w:compatSetting")
111 .into_iter()
112 .find_map(|tag| {
113 if get_attribute(&tag, "w:name").as_deref() != Some("compatibilityMode")
114 || get_attribute(&tag, "w:uri").as_deref()
115 != Some("http://schemas.microsoft.com/office/word")
116 {
117 return None;
118 }
119 get_attribute(&tag, "w:val")?
120 .parse::<u32>()
121 .ok()
122 .map(i64::from)
123 });
124
125 let adjust_line_height_in_table =
126 parse_first_on_off_setting(compat_xml, &["adjustLineHeightInTable"]);
127 let no_leading = parse_first_on_off_setting(compat_xml, &["noLeading"]);
128 let enable_open_type_features =
129 super::scan::find_all_tag_tokens(&compat_xml, "w:compatSetting")
130 .into_iter()
131 .find_map(|tag| {
132 if get_attribute(&tag, "w:name").as_deref() != Some("enableOpenTypeFeatures")
133 || get_attribute(&tag, "w:uri").as_deref()
134 != Some("http://schemas.microsoft.com/office/word")
135 {
136 return None;
137 }
138 match get_attribute(&tag, "w:val").as_deref() {
139 Some("1" | "true") => Some(true),
140 Some("0" | "false") => Some(false),
141 _ => None,
142 }
143 });
144 let suppress_top_spacing_wp = parse_first_on_off_setting(compat_xml, &["suppressTopSpacingWP"]);
145 let truncate_font_heights_like_wp6 =
146 parse_first_on_off_setting(compat_xml, &["truncateFontHeightsLikeWP6"]);
147 let no_extra_line_spacing = parse_first_on_off_setting(compat_xml, &["noExtraLineSpacing"]);
148 let space_for_ul = parse_first_on_off_setting(compat_xml, &["spaceForUL"]);
149 let suppress_bottom_spacing =
150 parse_first_on_off_setting(compat_xml, &["suppressBottomSpacing"]);
151
152 let suppress_spacing_before_after_page_break =
153 parse_first_on_off_setting(&compat_xml, &["suppressSpBfAfterPgBrk"]);
154 let use_printer_metrics = parse_first_on_off_setting(&compat_xml, &["usePrinterMetrics"]);
155 let use_fixed_html_paragraph_spacing =
156 parse_first_on_off_setting(&compat_xml, &["doNotUseHTMLParagraphAutoSpacing"]);
157 let do_not_break_wrapped_tables = parse_first_on_off_setting(
158 &compat_xml,
159 &["doNotBreakWrappedTables", "dontBreakWrappedTables"],
160 );
161 let do_not_break_constrained_forced_table = parse_first_on_off_setting(
162 &compat_xml,
163 &[
164 "doNotBreakConstrainedForcedTable",
165 "dontBreakConstrainedForcedTable",
166 ],
167 );
168 let even_and_odd_headers = parse_first_on_off_setting(&settings_root, &["evenAndOddHeaders"]);
169
170 if adjust_line_height_in_table.is_none()
171 && no_leading.is_none()
172 && suppress_top_spacing_wp.is_none()
173 && truncate_font_heights_like_wp6.is_none()
174 && no_extra_line_spacing.is_none()
175 && space_for_ul.is_none()
176 && suppress_bottom_spacing.is_none()
177 && compatibility_mode.is_none()
178 && enable_open_type_features.is_none()
179 && suppress_spacing_before_after_page_break.is_none()
180 && use_printer_metrics.is_none()
181 && use_fixed_html_paragraph_spacing.is_none()
182 && do_not_break_wrapped_tables.is_none()
183 && do_not_break_constrained_forced_table.is_none()
184 && even_and_odd_headers.is_none()
185 {
186 return None;
187 }
188
189 Some(DocumentCompatibilitySettings {
190 adjust_line_height_in_table,
191 no_leading,
192 suppress_top_spacing_wp,
193 truncate_font_heights_like_wp6,
194 no_extra_line_spacing,
195 space_for_ul,
196 suppress_bottom_spacing,
197
198 compatibility_mode,
199 enable_open_type_features,
200 suppress_spacing_before_after_page_break,
201 use_printer_metrics,
202 use_fixed_html_paragraph_spacing,
203 do_not_break_wrapped_tables,
204 do_not_break_constrained_forced_table,
205 even_and_odd_headers,
206 })
207}
208
209fn extract_simple_tag_text(xml: &str, tag_name: &str) -> Option<String> {
210 let open = format!("<{tag_name}>");
211 let close = format!("</{tag_name}>");
212 let start = find_subsequence_ignore_ascii_case(xml, &open)?;
213 let content_start = start + open.len();
214 let close_start = find_subsequence_ignore_ascii_case(&xml[content_start..], &close)?;
215 Some(xml[content_start..content_start + close_start].to_string())
216}
217
218fn find_namespaced_attribute_value(tag_xml: &str, attribute: &str) -> Option<String> {
219 crate::xml::get_attribute(tag_xml, attribute).or_else(|| {
220 let pattern = format!("{attribute}=\"");
221 let start = find_subsequence_ignore_ascii_case(tag_xml, &pattern)?;
222 let value_start = start + pattern.len();
223 let value_end = tag_xml[value_start..].find('"')? + value_start;
224 Some(tag_xml[value_start..value_end].to_string())
225 })
226}
227
228fn find_subsequence_ignore_ascii_case(haystack: &str, needle: &str) -> Option<usize> {
229 if needle.is_empty() {
230 return Some(0);
231 }
232 crate::xml::find_ignore_ascii_case(haystack, 0, needle)
233}
234
235#[cfg(test)]
236mod indexed_body_bounds_tests {
237 #[test]
238 fn indexed_body_bounds_preserve_legacy_metadata_and_body_results() {
239 let binding = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
240 for content in [
241 "<w:body><w:p/><w:sectPr/></w:body>",
242 "<w:body></w:body><w:sectPr/>",
243 "<w:body/><w:sectPr/>",
244 "<w:bodyExtra/><w:body><w:sectPr/></w:body>",
245 "<w:body><w:body><w:sectPr/></w:body><w:sectPr/></w:body>",
246 "<w:body><w:sectPr/></w:body><w:body><w:p/></w:body>",
247 "<w:BODY><w:p/><w:sectPr/></w:BODY>",
248 "<w:body><w:p><w:r><w:t>λ😀<w:body></w:t></w:r></w:p><w:sectPr/></w:body>",
249 ] {
250 let xml = format!("<w:document xmlns:w=\"{binding}\">{content}</w:document>");
251 let expected = (super::extract_section_properties_xml(&xml), super::super::body::body_xml(&xml).to_owned());
252 let Some(index) = crate::xml_index::Index::canonical(&xml) else { continue; };
253 let _scope = crate::xml_index::scope(
254 [("part", xml.as_str())].into_iter(),
255 std::collections::HashMap::from([("part".to_owned(), index)]),
256 );
257 let actual = (super::extract_section_properties_xml(&xml), super::super::body::body_xml(&xml).to_owned());
258 assert_eq!(actual, expected);
259 }
260 }
261}