1mod archive;
8mod compatibility;
9mod namespaces;
10mod numbering;
11mod ooxml;
12mod relationships;
13mod review;
14mod structure;
15mod styles;
16
17use std::collections::{HashMap, HashSet};
18use std::fmt;
19
20pub use archive::{DocumentParts, DocxLimits, extract_document_parts, extract_document_xml};
21pub use ooxml::{
22 PackageParagraphId, ParagraphStructure, RevisionProjectionStatus, RevisionUnsupportedReason,
23 RevisionView, TextFormattingSpan, TextMaterialization, TextStyle,
24};
25pub use review::{
26 AttributedComment, AttributedRevision, CommentContent, DocumentReviewFacts, ReviewDetail,
27 ReviewFactLimits, ReviewFactSet, ReviewFactUnknownReason, ReviewPoint, ReviewSpan,
28 RevisionContent, RevisionFactKind,
29};
30pub use structure::{
31 BookmarkFact, DocumentStructureFacts, InternalReferenceFact, InternalReferenceRole,
32 NumberingHierarchyFact, ParagraphAlignmentFact, ParagraphAlignmentSource,
33 ParagraphAlignmentValue, ParagraphIndentation, ParagraphIndentationFact,
34 ParagraphOutlineLevelFact, SpanCoverage, StructuralFactSet, StructuralFactUnknownReason,
35 StructuralSpan,
36};
37pub use styles::is_semantic_highlight_color;
38
39#[derive(Clone, Debug, Eq, Hash, PartialEq)]
40pub struct InternalParagraphId(String);
41
42impl InternalParagraphId {
43 pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
50 let value = value.into();
51 if value.is_empty() || value.len() > 128 {
52 return Err(ProjectionError::InvalidInternalParagraphId);
53 }
54 Ok(Self(value))
55 }
56
57 #[must_use]
58 pub fn as_str(&self) -> &str {
59 &self.0
60 }
61}
62
63#[derive(Clone, Copy, Debug)]
64pub struct ParagraphIdentityFacts<'a> {
65 pub ordinal: usize,
66 pub package_paragraph_id: Option<PackageParagraphId>,
67 pub text: &'a str,
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct ProjectedParagraph {
72 pub id: InternalParagraphId,
73 pub ordinal: usize,
74 pub package_paragraph_id: Option<PackageParagraphId>,
75 pub style_id: Option<String>,
76 pub text: String,
77 pub formatting: Vec<TextFormattingSpan>,
78 pub structure: Option<ParagraphStructure>,
79 pub alignment: Option<ParagraphAlignmentFact>,
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum FormattingUnknownReason {
84 DocumentPartOnly,
85 StylesPartUnavailable,
86 UnsupportedStyles,
87}
88
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub enum FormattingProjectionStatus {
91 Complete,
92 Incomplete(FormattingUnknownReason),
93}
94
95impl Default for FormattingProjectionStatus {
96 fn default() -> Self {
97 Self::Incomplete(FormattingUnknownReason::DocumentPartOnly)
98 }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct DocumentProjection {
103 pub paragraphs: Vec<ProjectedParagraph>,
104 pub formatting_status: FormattingProjectionStatus,
105 pub revision_status: RevisionProjectionStatus,
106 pub structural_facts: DocumentStructureFacts,
107}
108
109#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct DocumentPackageProjection {
111 pub document: DocumentProjection,
112 pub review_facts: DocumentReviewFacts,
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub struct ProjectionOptions {
117 pub revision_view: RevisionView,
118 pub text_materialization: TextMaterialization,
119}
120
121impl Default for ProjectionOptions {
122 fn default() -> Self {
123 Self {
124 revision_view: RevisionView::Current,
125 text_materialization: TextMaterialization::WordHost,
126 }
127 }
128}
129
130#[derive(Clone, Debug, Eq, PartialEq)]
131pub enum ProjectionError {
132 ArchiveTooLarge,
133 InvalidArchive,
134 TooManyArchiveEntries,
135 InvalidPackageRelationships,
136 PackageRelationshipsTooLarge,
137 MissingDocumentXml,
138 DuplicateDocumentXml,
139 DuplicateStylesXml,
140 DuplicateNumberingXml,
141 EncryptedDocumentXml,
142 UnsupportedCompression(u16),
143 DocumentXmlTooLarge,
144 SuspiciousCompressionRatio,
145 InvalidDocumentXmlEntry,
146 DocumentXmlIntegrity,
147 StylesXmlTooLarge,
148 InvalidStylesXmlEntry,
149 StylesXmlIntegrity,
150 NumberingXmlTooLarge,
151 InvalidNumberingXmlEntry,
152 NumberingXmlIntegrity,
153 InvalidDocumentXml,
154 InvalidStylesXml,
155 InvalidNumberingXml,
156 MissingDocumentBody,
157 TooManyParagraphs,
158 TooManyStructuralFacts,
159 TooManyNumberingItems,
160 InvalidInternalParagraphId,
161 DuplicateInternalParagraphId,
162}
163
164impl fmt::Display for ProjectionError {
165 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
166 let message = match self {
167 Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
168 Self::InvalidArchive => "DOCX archive is invalid",
169 Self::TooManyArchiveEntries => "DOCX archive has too many entries",
170 Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
171 Self::PackageRelationshipsTooLarge => {
172 "DOCX package relationships exceed the configured size limit"
173 }
174 Self::MissingDocumentXml => "DOCX archive has no main document part",
175 Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
176 Self::DuplicateStylesXml => "DOCX archive has duplicate entries for its styles part",
177 Self::DuplicateNumberingXml => {
178 "DOCX archive has duplicate entries for its numbering part"
179 }
180 Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
181 Self::UnsupportedCompression(_) => {
182 "selected DOCX package part uses unsupported compression"
183 }
184 Self::DocumentXmlTooLarge => {
185 "DOCX main document part exceeds the configured size limit"
186 }
187 Self::SuspiciousCompressionRatio => {
188 "selected DOCX package part exceeds the compression-ratio limit"
189 }
190 Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
191 Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
192 Self::StylesXmlTooLarge => "DOCX styles part exceeds the configured size limit",
193 Self::InvalidStylesXmlEntry => "DOCX styles part has an invalid ZIP entry",
194 Self::StylesXmlIntegrity => "DOCX styles part failed size or CRC validation",
195 Self::NumberingXmlTooLarge => "DOCX numbering part exceeds the configured size limit",
196 Self::InvalidNumberingXmlEntry => "DOCX numbering part has an invalid ZIP entry",
197 Self::NumberingXmlIntegrity => "DOCX numbering part failed size or CRC validation",
198 Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
199 Self::InvalidStylesXml => "DOCX styles part is invalid XML",
200 Self::InvalidNumberingXml => "DOCX numbering part is invalid XML",
201 Self::MissingDocumentBody => "DOCX main document part has no document body",
202 Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
203 Self::TooManyStructuralFacts => {
204 "DOCX main document part produces too many structural facts"
205 }
206 Self::TooManyNumberingItems => "DOCX numbering part exceeds the configured item limit",
207 Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
208 Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
209 };
210 formatter.write_str(message)
211 }
212}
213
214impl std::error::Error for ProjectionError {}
215
216pub fn project_docx<F>(
223 bytes: &[u8],
224 limits: DocxLimits,
225 allocate_id: F,
226) -> Result<DocumentProjection, ProjectionError>
227where
228 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
229{
230 project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
231}
232
233pub fn project_docx_with_options<F>(
240 bytes: &[u8],
241 limits: DocxLimits,
242 options: ProjectionOptions,
243 allocate_id: F,
244) -> Result<DocumentProjection, ProjectionError>
245where
246 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
247{
248 let parts = extract_document_parts(bytes, limits)?;
249 let styles = parts.styles_xml.as_deref().map_or(
250 Err(StructuralFactUnknownReason::StylesPartUnavailable),
251 |styles| {
252 styles::parse_styles(styles, limits.maximum_styles)
253 .map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
254 },
255 );
256 let numbering = parse_optional_numbering(
257 parts.numbering_xml.as_deref(),
258 limits.maximum_numbering_items,
259 )?;
260 project_document_xml_with_limit(
261 &parts.document_xml,
262 limits.maximum_paragraphs,
263 limits.maximum_structural_facts,
264 options,
265 ProjectionDependencies {
266 styles: styles.as_ref().map_err(|reason| *reason),
267 numbering: numbering
268 .as_ref()
269 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
270 },
271 allocate_id,
272 )
273}
274
275pub fn project_docx_with_review_facts<F>(
287 bytes: &[u8],
288 limits: DocxLimits,
289 review_limits: ReviewFactLimits,
290 options: ProjectionOptions,
291 allocate_id: F,
292) -> Result<DocumentPackageProjection, ProjectionError>
293where
294 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
295{
296 let parts = archive::extract_projection_parts(bytes, limits, review_limits)?;
297 let styles = parts.styles.as_deref().map_or(
298 Err(StructuralFactUnknownReason::StylesPartUnavailable),
299 |styles| {
300 styles::parse_styles(styles, limits.maximum_styles)
301 .map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
302 },
303 );
304 let numbering =
305 parse_optional_numbering(parts.numbering.as_deref(), limits.maximum_numbering_items)?;
306 let ProjectedDocumentWithReview {
307 document,
308 revisions,
309 comment_anchors,
310 } = project_document_xml_with_limit_and_review(
311 &parts.document,
312 limits.maximum_paragraphs,
313 limits.maximum_structural_facts,
314 options,
315 ProjectionDependencies {
316 styles: styles.as_ref().map_err(|reason| *reason),
317 numbering: numbering
318 .as_ref()
319 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
320 },
321 Some(ooxml::ReviewProjectionLimits {
322 maximum_facts: review_limits.maximum_facts_per_family,
323 maximum_detail_bytes: review_limits.maximum_review_detail_bytes,
324 }),
325 allocate_id,
326 )?;
327 let review_facts = review::project_review_facts(
328 revisions.unwrap_or(ReviewFactSet::Unknown(
329 ReviewFactUnknownReason::InvalidDocument,
330 )),
331 comment_anchors.as_ref(),
332 &document,
333 parts.comments,
334 parts.comments_extended,
335 review_limits,
336 options.text_materialization,
337 );
338 Ok(DocumentPackageProjection {
339 document,
340 review_facts,
341 })
342}
343
344fn parse_optional_numbering(
345 xml: Option<&[u8]>,
346 maximum_items: usize,
347) -> Result<Option<numbering::NumberingCatalog>, ProjectionError> {
348 let Some(xml) = xml else {
349 return Ok(None);
350 };
351 match numbering::parse_numbering(xml, maximum_items) {
352 Ok(catalog) => Ok(Some(catalog)),
353 Err(ProjectionError::TooManyNumberingItems) => Err(ProjectionError::TooManyNumberingItems),
354 Err(_) => Ok(None),
355 }
356}
357
358pub fn project_document_xml<F>(
365 xml: &[u8],
366 allocate_id: F,
367) -> Result<DocumentProjection, ProjectionError>
368where
369 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
370{
371 project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
372}
373
374pub fn project_document_xml_with_options<F>(
381 xml: &[u8],
382 options: ProjectionOptions,
383 allocate_id: F,
384) -> Result<DocumentProjection, ProjectionError>
385where
386 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
387{
388 project_document_xml_with_limit(
389 xml,
390 DocxLimits::default().maximum_paragraphs,
391 DocxLimits::default().maximum_structural_facts,
392 options,
393 ProjectionDependencies {
394 styles: Err(StructuralFactUnknownReason::DocumentPartOnly),
395 numbering: Err(StructuralFactUnknownReason::DocumentPartOnly),
396 },
397 allocate_id,
398 )
399}
400
401fn project_document_xml_with_limit<F>(
402 xml: &[u8],
403 maximum_paragraphs: usize,
404 maximum_structural_facts: usize,
405 options: ProjectionOptions,
406 dependencies: ProjectionDependencies<'_>,
407 allocate_id: F,
408) -> Result<DocumentProjection, ProjectionError>
409where
410 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
411{
412 project_document_xml_with_limit_and_review(
413 xml,
414 maximum_paragraphs,
415 maximum_structural_facts,
416 options,
417 dependencies,
418 None,
419 allocate_id,
420 )
421 .map(|projection| projection.document)
422}
423
424struct ProjectedDocumentWithReview {
425 document: DocumentProjection,
426 revisions: Option<ReviewFactSet<AttributedRevision>>,
427 comment_anchors: Option<HashMap<String, ReviewSpan>>,
428}
429
430#[derive(Clone, Copy)]
431struct ProjectionDependencies<'a> {
432 styles: Result<&'a structure::StyleSheet, StructuralFactUnknownReason>,
433 numbering: Result<&'a numbering::NumberingCatalog, StructuralFactUnknownReason>,
434}
435
436fn project_document_xml_with_limit_and_review<F>(
437 xml: &[u8],
438 maximum_paragraphs: usize,
439 maximum_structural_facts: usize,
440 options: ProjectionOptions,
441 dependencies: ProjectionDependencies<'_>,
442 review_limits: Option<ooxml::ReviewProjectionLimits>,
443 mut allocate_id: F,
444) -> Result<ProjectedDocumentWithReview, ProjectionError>
445where
446 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
447{
448 let projected = ooxml::project_document_xml(
449 xml,
450 maximum_paragraphs,
451 options.revision_view,
452 options.text_materialization,
453 dependencies.styles.map_err(formatting_unknown_reason),
454 review_limits,
455 )?;
456 let review_revisions = projected.review_revisions;
457 let review_comment_anchors = review_limits
458 .is_some()
459 .then_some(projected.review_comment_anchors);
460 let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
461 let mut ids = Vec::with_capacity(projected.paragraphs.len());
462 for paragraph in &projected.paragraphs {
463 let facts = ParagraphIdentityFacts {
464 ordinal: paragraph.ordinal,
465 package_paragraph_id: paragraph.package_paragraph_id,
466 text: ¶graph.text,
467 };
468 let id = allocate_id(facts)?;
469 if !seen_ids.insert(id.clone()) {
470 return Err(ProjectionError::DuplicateInternalParagraphId);
471 }
472 ids.push(id);
473 }
474 let texts = projected
475 .paragraphs
476 .iter()
477 .map(|paragraph| paragraph.text.as_str())
478 .collect::<Vec<_>>();
479 let properties = projected
480 .paragraphs
481 .iter()
482 .map(|paragraph| ¶graph.properties)
483 .collect::<Vec<_>>();
484 let structural_facts = structure::materialize_structure(
485 structure::RawStructureInput {
486 paragraph_texts: &texts,
487 properties: &properties,
488 bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
489 references: projected.references.as_deref().map_err(|reason| *reason),
490 },
491 dependencies.styles,
492 dependencies.numbering,
493 maximum_structural_facts,
494 )?;
495 let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
496 for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
497 let alignment =
498 structure::resolve_paragraph_alignment(dependencies.styles, ¶graph.properties);
499 paragraphs.push(ProjectedParagraph {
500 id,
501 ordinal: paragraph.ordinal,
502 package_paragraph_id: paragraph.package_paragraph_id,
503 style_id: paragraph.properties.style_id,
504 text: paragraph.text,
505 formatting: paragraph.formatting,
506 structure: paragraph.structure,
507 alignment,
508 });
509 }
510 Ok(ProjectedDocumentWithReview {
511 document: DocumentProjection {
512 paragraphs,
513 formatting_status: projected.formatting_status,
514 revision_status: projected.revision_status,
515 structural_facts,
516 },
517 revisions: review_revisions,
518 comment_anchors: review_comment_anchors,
519 })
520}
521
522const fn formatting_unknown_reason(reason: StructuralFactUnknownReason) -> FormattingUnknownReason {
523 match reason {
524 StructuralFactUnknownReason::DocumentPartOnly => FormattingUnknownReason::DocumentPartOnly,
525 StructuralFactUnknownReason::StylesPartUnavailable => {
526 FormattingUnknownReason::StylesPartUnavailable
527 }
528 StructuralFactUnknownReason::UnsupportedStyles
529 | StructuralFactUnknownReason::UnsupportedNumbering
530 | StructuralFactUnknownReason::IncompleteBookmarkRanges
531 | StructuralFactUnknownReason::UnsupportedInternalReferences => {
532 FormattingUnknownReason::UnsupportedStyles
533 }
534 }
535}