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