1use crate::model::{
2 DocNode, ParagraphChildNode, ParagraphNode, ParagraphNodeType, TextRunNode, TextRunNodeType,
3};
4
5use super::body::body_xml;
6use super::context::ParseContext;
7use super::paragraph::parse_paragraph;
8use super::table::parse_table;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct BodyTokenRange {
12 pub start: usize,
13 pub end: usize,
14 pub kind: BodyTokenKind,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum BodyTokenKind {
19 Table,
20 Paragraph,
21}
22
23pub fn extract_body_token_ranges(body_xml: &str) -> Vec<BodyTokenRange> {
24 let mut ranges = Vec::new();
25 for (name, kind) in [
26 ("w:tbl", BodyTokenKind::Table),
27 ("w:p", BodyTokenKind::Paragraph),
28 ] {
29 ranges.extend(
30 crate::xml::extract_balanced_tag_ranges(body_xml, name)
31 .into_iter()
32 .map(|range| BodyTokenRange {
33 start: range.start,
34 end: range.end,
35 kind,
36 }),
37 );
38 }
39 ranges.sort_by_key(|range| range.start);
40 let mut covered_end = 0;
41 ranges.retain(|range| {
42 if range.end <= covered_end {
43 return false;
44 }
45 covered_end = range.end;
46 true
47 });
48 ranges
49}
50
51pub fn is_go_back_bookmark_paragraph(paragraph_xml: &str) -> bool {
52 if !contains_case_insensitive(paragraph_xml, "w:name=\"_GoBack\"") {
53 return false;
54 }
55
56 !contains_case_insensitive_tag(paragraph_xml, "w:r")
57 && !contains_case_insensitive_tag(paragraph_xml, "w:drawing")
58}
59
60pub fn parse_document_xml(
61 document_xml: &str,
62 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
63) -> Vec<DocNode> {
64 let body = body_xml(document_xml);
65 parse_ranges(body, extract_body_token_ranges(body), context)
66}
67
68fn parse_token(
69 body: &str,
70 token: &BodyTokenRange,
71 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
72) -> Option<DocNode> {
73 let xml = &body[token.start..token.end];
74 if token.kind == BodyTokenKind::Table {
75 Some(DocNode::Table(parse_table(xml, context)))
76 } else if !is_go_back_bookmark_paragraph(xml) {
77 Some(DocNode::Paragraph(parse_paragraph(xml, context)))
78 } else {
79 None
80 }
81}
82
83fn parse_ranges(
84 body: &str,
85 ranges: Vec<BodyTokenRange>,
86 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
87) -> Vec<DocNode> {
88 let mut nodes = Vec::with_capacity(ranges.len());
89 for token in ranges {
90 if let Some(node) = parse_token(body, &token, context) {
91 nodes.push(node);
92 }
93 }
94 nonempty_nodes(nodes)
95}
96
97fn nonempty_nodes(mut nodes: Vec<DocNode>) -> Vec<DocNode> {
98 if nodes.is_empty() {
99 nodes.push(DocNode::Paragraph(ParagraphNode {
100 review_content: None,
101 r#type: ParagraphNodeType::Paragraph,
102 block_id: None,
103 children: vec![ParagraphChildNode::Text(TextRunNode {
104 r#type: TextRunNodeType::Text,
105 text: String::new(),
106 style: None,
107 link: None,
108 note_reference: None,
109 })],
110 style: None,
111 paragraph_mark_style: None,
112 source_paragraph_mark_style: None,
113 source_paragraph_mark_formatting: None,
114 source_paragraph_mark_properties_xml: None,
115 paragraph_mark_deleted: None,
116 source_xml: None,
117 source_text_patch: None,
118 source_run_provenance: None,
119 }));
120 }
121
122 nodes
123}
124
125#[cfg(not(target_arch = "wasm32"))]
126pub(crate) fn parse_document_xml_parallel(
127 document: &str,
128 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
129 indexes: &crate::xml_index::Scope<'_>,
130) -> Vec<DocNode> {
131 let body = body_xml(document);
132 let ranges = extract_body_token_ranges(body);
133 let bytes = ranges
134 .iter()
135 .map(|range| range.end - range.start)
136 .sum::<usize>();
137 let largest = ranges
138 .iter()
139 .map(|range| range.end - range.start)
140 .max()
141 .unwrap_or(0);
142 let pool = (bytes.saturating_sub(largest) >= 16 * 1024 && ranges.len() >= 16)
143 .then(super::namespaces::worker_pool)
144 .flatten();
145 let Some(pool) = pool else {
146 return parse_ranges(body, ranges, context);
147 };
148 let workers = pool.current_num_threads() + 1;
149 let mut order: Vec<_> = (0..ranges.len()).collect();
150 order.sort_unstable_by_key(|id| std::cmp::Reverse(ranges[*id].end - ranges[*id].start));
151 let mut weights = vec![0usize; workers];
152 let mut assignments = vec![0usize; ranges.len()];
153 for id in order {
154 let worker = (0..workers).min_by_key(|worker| weights[*worker]).unwrap();
155 assignments[id] = worker;
156 weights[worker] += ranges[id].end - ranges[id].start;
157 }
158 let mut results: Vec<Option<(Option<DocNode>, Vec<String>)>> =
159 (0..ranges.len()).map(|_| None).collect();
160 let mut groups: Vec<Vec<_>> = (0..workers).map(|_| Vec::new()).collect();
161 for ((range, result), worker) in ranges.iter().zip(results.iter_mut()).zip(assignments) {
162 groups[worker].push((range, result));
163 }
164 let mut groups = groups.into_iter();
165 let caller = groups.next().unwrap();
166 let relationships = &context.relationships;
167 let content_types = &context.content_types;
168 let styles = context.styles();
169 let parts = context.parts;
170 let binary_assets = context.binary_assets;
171 let mut earlier_warnings = std::mem::take(&mut *context.warnings.borrow_mut());
172 pool.in_place_scope(|scope| {
173 for group in groups {
174 if group.is_empty() {
175 continue;
176 }
177 let snapshot = indexes.snapshot();
178 scope.spawn(move |_| {
179 let _indexes = snapshot.enter();
180 let _styles = super::style::run_style_cache_scope();
181 super::style::bind_style_cache(styles);
182 let context = ParseContext {
183 relationships: relationships.clone(),
184 content_types: content_types.clone(),
185 parts,
186 binary_assets,
187 style_sheet: styles,
188 warnings: std::cell::RefCell::new(Vec::new()),
189 };
190 for (range, result) in group {
191 let node = parse_token(body, range, &context);
192 *result = Some((node, std::mem::take(&mut *context.warnings.borrow_mut())));
193 }
194 });
195 }
196 for (range, result) in caller {
197 let node = parse_token(body, range, context);
198 *result = Some((node, std::mem::take(&mut *context.warnings.borrow_mut())));
199 }
200 });
201 let mut nodes = Vec::with_capacity(results.len());
202 for result in results {
203 let (node, warnings) = result.expect("joined body task");
204 if let Some(node) = node {
205 nodes.push(node);
206 }
207 earlier_warnings.extend(warnings);
208 }
209 *context.warnings.borrow_mut() = earlier_warnings;
210 nonempty_nodes(nodes)
211}
212
213fn contains_case_insensitive(haystack: &str, needle: &str) -> bool {
214 find_case_insensitive_at(haystack, 0, needle).is_some()
215}
216
217fn contains_case_insensitive_tag(haystack: &str, tag_name: &str) -> bool {
218 find_case_insensitive_at(haystack, 0, &format!("<{tag_name}")).is_some()
219 || find_case_insensitive_at(haystack, 0, &format!("<{tag_name}/")).is_some()
220 || find_case_insensitive_at(haystack, 0, &format!("<{tag_name} ")).is_some()
221}
222
223fn find_case_insensitive_at(haystack: &str, start: usize, needle: &str) -> Option<usize> {
224 crate::xml::find_ignore_ascii_case(haystack, start, needle)
225}
226
227#[cfg(test)]
228mod tests {
229 use std::cell::RefCell;
230 use std::collections::HashMap;
231
232 use super::*;
233 use crate::parse::context::{ContentTypeLookup, ParseContext, ParsedStyleSheet};
234
235 #[test]
236 fn extract_body_xml_returns_inner_content() {
237 let xml = "<w:document><w:body><w:p/></w:body></w:document>";
238 assert_eq!(super::super::body::extract_body_xml(xml), "<w:p/>");
239 }
240
241 #[test]
242 fn is_go_back_bookmark_paragraph_detects_hidden_bookmark() {
243 let xml = r#"<w:p><w:bookmarkStart w:name="_GoBack"/></w:p>"#;
244 assert!(is_go_back_bookmark_paragraph(xml));
245 let with_run = r#"<w:p><w:bookmarkStart w:name="_GoBack"/><w:r><w:t>x</w:t></w:r></w:p>"#;
246 assert!(!is_go_back_bookmark_paragraph(with_run));
247 }
248
249 #[test]
250 fn parse_document_xml_skips_go_back_paragraph() {
251 let xml = r#"<w:body><w:p><w:bookmarkStart w:name="_GoBack"/></w:p><w:p><w:r><w:t>Hi</w:t></w:r></w:p></w:body>"#;
252 let context = ParseContext {
253 relationships: HashMap::new(),
254 content_types: ContentTypeLookup::default(),
255 parts: &HashMap::new(),
256 binary_assets: &HashMap::new(),
257 style_sheet: ParsedStyleSheet::empty(),
258 warnings: RefCell::new(Vec::new()),
259 };
260 let nodes = parse_document_xml(xml, &context);
261 assert_eq!(nodes.len(), 1);
262 }
263
264 #[test]
265 fn drawing_text_stays_out_of_the_anchor_paragraph() {
266 let xml = r#"<w:body><w:p><w:r><w:t>Before</w:t></w:r><w:r><w:drawing><wpg:wgp><wps:wsp><wps:txbx><w:txbxContent><w:p><w:r><w:t>Shape story</w:t></w:r></w:p></w:txbxContent></wps:txbx></wps:wsp></wpg:wgp></w:drawing></w:r><w:r><w:t>After</w:t></w:r></w:p></w:body>"#;
267 let context = ParseContext {
268 relationships: HashMap::new(),
269 content_types: ContentTypeLookup::default(),
270 parts: &HashMap::new(),
271 binary_assets: &HashMap::new(),
272 style_sheet: ParsedStyleSheet::empty(),
273 warnings: RefCell::new(Vec::new()),
274 };
275 let nodes = parse_document_xml(xml, &context);
276 let DocNode::Paragraph(paragraph) = &nodes[0] else {
277 panic!("paragraph")
278 };
279 let text: String = paragraph
280 .children
281 .iter()
282 .filter_map(|child| match child {
283 ParagraphChildNode::Text(run) => Some(run.text.as_str()),
284 _ => None,
285 })
286 .collect();
287 assert_eq!(text, "BeforeAfter");
288 assert_eq!(
289 paragraph.source_xml.as_deref(),
290 Some(&xml[8..xml.len() - 9])
291 );
292
293 let with_control = xml.replace("<w:r><w:t>Shape story</w:t></w:r>",
294 "<w:sdt><w:sdtPr><w:text/></w:sdtPr><w:sdtContent><w:r><w:t>Shape story</w:t></w:r></w:sdtContent></w:sdt>");
295 let nodes = parse_document_xml(&with_control, &context);
296 let DocNode::Paragraph(paragraph) = &nodes[0] else {
297 panic!("paragraph")
298 };
299 assert!(!paragraph
300 .children
301 .iter()
302 .any(|child| matches!(child, ParagraphChildNode::FormField(_))));
303 let nodes = parse_document_xml("<w:body><w:p><w:sdt><w:sdtPr><w:text/></w:sdtPr><w:sdtContent><w:r><w:t>Body control</w:t></w:r></w:sdtContent></w:sdt></w:p></w:body>", &context);
304 let DocNode::Paragraph(paragraph) = &nodes[0] else {
305 panic!("paragraph")
306 };
307 assert!(paragraph
308 .children
309 .iter()
310 .any(|child| matches!(child, ParagraphChildNode::FormField(_))));
311 }
312}
313
314#[cfg(all(test, not(target_arch = "wasm32")))]
315mod parallel_tests {
316 #[test]
317 fn balanced_body_preserves_nodes_and_warning_order() {
318 let content = "content & detail ".repeat(40);
319 let mut xml = "<w:document><w:body>".to_owned();
320 for index in 0..80 {
321 xml.push_str(&format!(
322 "<w:p><w:r><w:t>{index} {content}</w:t></w:r></w:p>"
323 ));
324 if index % 9 == 0 {
325 xml.push_str("<w:p><m:oMath><m:unknown/></m:oMath></w:p>");
326 }
327 }
328 xml.push_str("</w:body></w:document>");
329 let mut parts = std::collections::HashMap::new();
330 parts.insert(
331 "body".into(),
332 crate::package::OoxmlPart {
333 name: "body".into(),
334 content: xml.clone(),
335 },
336 );
337 let assets = std::collections::HashMap::new();
338 let context = crate::parse::context::ParseContext {
339 relationships: Default::default(),
340 content_types: Default::default(),
341 parts: &parts,
342 binary_assets: &assets,
343 style_sheet: crate::parse::context::ParsedStyleSheet::empty(),
344 warnings: std::cell::RefCell::new(vec!["prior warning".into()]),
345 };
346 let indexes =
347 crate::xml_index::scope([("body", xml.as_str())].into_iter(), Default::default());
348 let serial = super::parse_document_xml(&xml, &context);
349 let warnings = context.warnings.replace(vec!["prior warning".into()]);
350 let parallel = super::parse_document_xml_parallel(&xml, &context, &indexes);
351 assert_eq!(
352 serde_json::to_value(serial).unwrap(),
353 serde_json::to_value(parallel).unwrap()
354 );
355 assert_eq!(warnings, *context.warnings.borrow());
356 }
357}