1use crate::model::{DocModel, DocModelMetadata, DocumentNoteDefinition};
2use std::cell::RefCell;
3
4use crate::package::OoxmlPackage;
5
6use super::content_types::parse_content_types;
7use super::context::ParseContext;
8#[cfg(target_arch = "wasm32")]
9use super::document::parse_document_xml;
10use super::metadata::{
11 extract_document_open_tag, extract_section_properties_xml, parse_document_background_color,
12 parse_document_compatibility_settings, parse_document_default_tab_stop_twips,
13 parse_document_page_count_from_app_properties,
14};
15use super::numbering::parse_numbering_with_assets;
16use super::relationships::parse_part_relationships;
17use super::sections::{
18 parse_document_comments, parse_document_notes_from_part, parse_document_sections,
19 parse_footer_sections, parse_header_sections,
20};
21use super::styles::{acquire_document_style_sheet, release_document_style_metadata};
22
23pub fn build_doc_model_with_options(pkg: &OoxmlPackage, options: &crate::ParseOptions) -> DocModel {
24 let mut model = build_doc_model(pkg);
25 if options.include_3d_models {
26 super::model3d::attach_models(&mut model, pkg, options);
27 }
28 model
29}
30
31pub fn build_doc_model(pkg: &OoxmlPackage) -> DocModel {
32 #[cfg(not(target_arch = "wasm32"))]
33 {
34 let bytes = pkg.parts.values().map(|part| part.content.len())
35 .chain(pkg.binary_assets.values().map(Vec::len))
36 .fold(0usize, usize::saturating_add);
37 if let Some(pool) = (bytes >= 64 * 1024)
38 .then(super::namespaces::worker_pool).flatten()
39 {
40 let mut identity = None;
41 let mut model = pool.in_place_scope(|scope| {
42 scope.spawn(|_| {
43 identity = Some(crate::structured::package_identity(pkg));
44 });
45 build_doc_model_contents(pkg)
46 });
47 crate::structured::assign_source_ids(&mut model, &identity.expect("joined identity task"));
48 return model;
49 }
50 }
51 let identity = crate::structured::package_identity(pkg);
52 let mut model = build_doc_model_contents(pkg);
53 crate::structured::assign_source_ids(&mut model, &identity);
54 model
55}
56
57fn build_doc_model_contents(pkg: &OoxmlPackage) -> DocModel {
58 let _style_cache = super::style::run_style_cache_scope();
59 let binary_assets = &pkg.binary_assets;
60 let (normalized, indexes) = super::namespaces::normalized_package(pkg);
61 let pkg = &normalized;
62 let _relationship_cache = crate::package::relationship_cache_scope(pkg);
63 let _xml_index = crate::xml_index::scope(
64 pkg.parts
65 .values()
66 .map(|part| (part.name.as_str(), part.content.as_str())),
67 indexes,
68 );
69 let mut warnings = pkg.warnings.clone();
70 warnings.extend(
71 crate::source_graph::preserved_features(pkg)
72 .into_iter()
73 .map(|feature| {
74 format!(
75 "{}: {} occurrence(s) of {} have support level {}",
76 feature.part_name, feature.occurrences, feature.feature, feature.support
77 )
78 }),
79 );
80 let main_part = crate::package::main_document_part_name(pkg);
81 let source_part_names = ["footnotes", "endnotes", "comments"]
82 .into_iter()
83 .map(|kind| {
84 (
85 kind.to_string(),
86 crate::package::related_document_part_name(pkg, kind),
87 )
88 })
89 .chain(std::iter::once(("document".into(), main_part.clone())))
90 .collect();
91 let document_xml = pkg.parts.get(&main_part).map(|part| part.content.as_str());
92 if document_xml.is_none() {
93 warnings.push("Missing word/document.xml".to_string());
94 }
95
96 let resolved_document_xml = document_xml.unwrap_or("");
97 let document_open_tag = extract_document_open_tag(resolved_document_xml);
98 let document_background_color = parse_document_background_color(resolved_document_xml);
99 let document_page_count = parse_document_page_count_from_app_properties(pkg);
100 let compatibility = parse_document_compatibility_settings(pkg);
101 let default_tab_stop_twips = parse_document_default_tab_stop_twips(pkg);
102 let section_properties_xml = extract_section_properties_xml(resolved_document_xml);
103 let content_types = parse_content_types(pkg);
104 let style_handle = acquire_document_style_sheet(pkg);
105 let style_sheet: &super::context::ParsedStyleSheet = std::borrow::Borrow::borrow(&style_handle);
106 super::style::bind_style_cache(style_sheet);
107 let numbering_definitions = parse_numbering_with_assets(pkg, &content_types, binary_assets);
108 let document_relationships = parse_part_relationships(pkg, &main_part);
109
110 let mut part_cache =
111 super::sections::PartNodeCache::new(pkg, &content_types, style_sheet, binary_assets);
112 let header_sections = parse_header_sections(
113 pkg,
114 resolved_document_xml,
115 &document_relationships,
116 &mut warnings,
117 &mut part_cache,
118 );
119 let footer_sections = parse_footer_sections(
120 pkg,
121 resolved_document_xml,
122 &document_relationships,
123 &mut warnings,
124 &mut part_cache,
125 );
126 let sections = parse_document_sections(
127 resolved_document_xml,
128 &document_relationships,
129 &mut warnings,
130 &mut part_cache,
131 );
132 drop(part_cache);
133 let footnotes = parse_document_notes_from_part(
134 pkg,
135 &crate::package::related_document_part_name(pkg, "footnotes"),
136 "w:footnote",
137 &content_types,
138 style_sheet,
139 &mut warnings,
140 binary_assets,
141 );
142 let endnotes = parse_document_notes_from_part(
143 pkg,
144 &crate::package::related_document_part_name(pkg, "endnotes"),
145 "w:endnote",
146 &content_types,
147 style_sheet,
148 &mut warnings,
149 binary_assets,
150 );
151 let comments = parse_document_comments(pkg, &content_types, style_sheet, binary_assets);
152
153 let context = ParseContext {
154 relationships: document_relationships.clone(),
155 content_types: content_types.clone(),
156 parts: &pkg.parts,
157 binary_assets,
158 style_sheet: style_sheet,
159 warnings: RefCell::new(Vec::new()),
160 };
161
162 #[cfg(not(target_arch = "wasm32"))]
163 let nodes =
164 super::document::parse_document_xml_parallel(resolved_document_xml, &context, &_xml_index);
165 #[cfg(target_arch = "wasm32")]
166 let nodes = parse_document_xml(resolved_document_xml, &context);
167 warnings.extend(context.warnings.borrow().iter().cloned());
168
169 let (paragraph_styles, default_paragraph_style_id) =
170 release_document_style_metadata(style_handle);
171 let model = DocModel {
172 nodes,
173 metadata: DocModelMetadata {
174 document_id: None,
175 source_part_names: Some(source_part_names),
176 source_parts: pkg.parts.len() as i64,
177 warnings,
178 document_page_count,
179 document_open_tag,
180 document_background_color,
181 section_properties_xml,
182 sections: Some(sections),
183 header_sections,
184 footer_sections,
185 paragraph_styles,
186 default_paragraph_style_id,
187 default_tab_stop_twips,
188 document_settings_imported: crate::package::related_document_part(pkg, "settings")
189 .is_some()
190 .then_some(true),
191 line_spacing_compatibility_imported: crate::package::related_document_part(
192 pkg, "settings",
193 )
194 .is_some()
195 .then_some(true),
196 numbering_definitions,
197 compatibility,
198 footnotes: clone_notes_if_non_empty(footnotes),
199 endnotes: clone_notes_if_non_empty(endnotes),
200 comments: if comments.is_empty() {
201 None
202 } else {
203 Some(comments)
204 },
205 },
206 };
207 model
208}
209
210fn clone_notes_if_non_empty(
211 notes: Vec<DocumentNoteDefinition>,
212) -> Option<Vec<DocumentNoteDefinition>> {
213 if notes.is_empty() {
214 return None;
215 }
216
217 Some(notes)
218}
219
220#[cfg(test)]
221mod tests {
222 use std::collections::HashMap;
223
224 use super::*;
225 use crate::package::OoxmlPart;
226
227 #[test]
228 fn build_doc_model_warns_when_document_missing() {
229 let pkg = OoxmlPackage {
230 parts: HashMap::new(),
231 binary_assets: HashMap::new(),
232 warnings: Vec::new(),
233 };
234 let model = build_doc_model(&pkg);
235 assert_eq!(model.nodes.len(), 1);
236 assert!(model
237 .metadata
238 .warnings
239 .iter()
240 .any(|warning| warning.contains("Missing word/document.xml")));
241 }
242
243 #[test]
244 fn build_doc_model_parses_empty_document() {
245 let mut parts = HashMap::new();
246 parts.insert(
247 "word/document.xml".to_string(),
248 OoxmlPart {
249 name: "word/document.xml".to_string(),
250 content: "<w:document><w:body/></w:document>".to_string(),
251 },
252 );
253 let pkg = OoxmlPackage {
254 parts,
255 binary_assets: HashMap::new(),
256 warnings: Vec::new(),
257 };
258 let model = build_doc_model(&pkg);
259 assert_eq!(model.nodes.len(), 1);
260 assert!(model.metadata.sections.is_some());
261 }
262
263 #[test]
264 fn build_doc_model_preserves_adapter_warnings() {
265 let pkg = OoxmlPackage {
266 parts: HashMap::new(),
267 binary_assets: HashMap::new(),
268 warnings: vec!["Legacy DOC adapter warning".to_string()],
269 };
270 let model = build_doc_model(&pkg);
271 assert!(model
272 .metadata
273 .warnings
274 .iter()
275 .any(|warning| warning == "Legacy DOC adapter warning"));
276 }
277}
278
279#[cfg(test)]
280mod package_preparation_tests {
281 #[test]
282 fn parallel_build_preserves_identity_and_normalized_source_count() {
283 let mut parts = std::collections::HashMap::new();
284 for index in 0..6 {
285 let name = format!("part-{index}.xml");
286 parts.insert(name.clone(), crate::package::OoxmlPart { name, content: format!("<w:root xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"><w:t>{}</w:t></w:root>", "text & detail ".repeat(1000)) });
287 }
288 let package = crate::package::OoxmlPackage {
289 parts,
290 binary_assets: std::collections::HashMap::from([("asset".into(), vec![5; 32 * 1024])]),
291 warnings: vec!["prior warning".into()],
292 };
293 let expected_identity = crate::structured::package_identity(&package);
294 let (expected, _) = crate::parse::namespaces::normalized_package(&package);
295 let mut serial = super::build_doc_model_contents(&package);
296 crate::structured::assign_source_ids(&mut serial, &expected_identity);
297 let actual = super::build_doc_model(&package);
298 assert_eq!(actual.metadata.document_id.as_deref(), Some(expected_identity.as_str()));
299 assert_eq!(actual.metadata.source_parts, expected.parts.len() as i64);
300 assert_eq!(serde_json::to_string(&actual).unwrap(), serde_json::to_string(&serial).unwrap());
301 }
302}
303
304#[cfg(all(test, not(target_arch = "wasm32")))]
305mod identity_overlap_tests {
306 use super::*;
307 use std::collections::HashMap;
308 use crate::package::OoxmlPart;
309
310 #[test]
311 fn overlapped_identity_matches_serial_construction() {
312 for asset_size in [0, 65536, 262145] {
313 let document = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>content</w:t></w:r></w:p></w:body></w:document>"#;
314 let part = OoxmlPart { name: "word/document.xml".into(), content: document.into() };
315 let mut package = OoxmlPackage { parts: HashMap::from([(part.name.clone(), part)]), binary_assets: HashMap::new(), warnings: Vec::new() };
316 package.binary_assets.insert("word/media/payload.bin".into(), (0..asset_size).map(|n| n as u8).collect());
317 let mut serial = build_doc_model_contents(&package);
318 crate::structured::assign_source_ids(&mut serial, &crate::structured::package_identity(&package));
319 let parallel = build_doc_model(&package);
320 assert_eq!(serde_json::to_string(&serial).unwrap(), serde_json::to_string(¶llel).unwrap());
321 package.binary_assets.get_mut("word/media/payload.bin").unwrap().push(7);
322 let changed = build_doc_model(&package);
323 assert_ne!(parallel.metadata.document_id, changed.metadata.document_id);
324 }
325 }
326}