1mod archive;
8mod compatibility;
9mod namespaces;
10mod ooxml;
11mod relationships;
12mod review;
13mod structure;
14mod styles;
15
16use std::collections::{HashMap, HashSet};
17use std::fmt;
18
19pub use archive::{DocumentParts, DocxLimits, extract_document_parts, extract_document_xml};
20pub use ooxml::{
21 PackageParagraphId, ParagraphStructure, RevisionProjectionStatus, RevisionUnsupportedReason,
22 RevisionView, TextFormattingSpan, TextMaterialization, TextStyle, is_semantic_highlight_color,
23};
24pub use review::{
25 AttributedComment, AttributedRevision, CommentContent, DocumentReviewFacts, ReviewDetail,
26 ReviewFactLimits, ReviewFactSet, ReviewFactUnknownReason, ReviewPoint, ReviewSpan,
27 RevisionContent, RevisionFactKind,
28};
29pub use structure::{
30 BookmarkFact, DocumentStructureFacts, InternalReferenceFact, InternalReferenceRole,
31 NumberingHierarchyFact, ParagraphIndentation, ParagraphIndentationFact, SpanCoverage,
32 StructuralFactSet, StructuralFactUnknownReason, StructuralSpan,
33};
34
35#[derive(Clone, Debug, Eq, Hash, PartialEq)]
36pub struct InternalParagraphId(String);
37
38impl InternalParagraphId {
39 pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
46 let value = value.into();
47 if value.is_empty() || value.len() > 128 {
48 return Err(ProjectionError::InvalidInternalParagraphId);
49 }
50 Ok(Self(value))
51 }
52
53 #[must_use]
54 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57}
58
59#[derive(Clone, Copy, Debug)]
60pub struct ParagraphIdentityFacts<'a> {
61 pub ordinal: usize,
62 pub package_paragraph_id: Option<PackageParagraphId>,
63 pub text: &'a str,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct ProjectedParagraph {
68 pub id: InternalParagraphId,
69 pub ordinal: usize,
70 pub package_paragraph_id: Option<PackageParagraphId>,
71 pub text: String,
72 pub formatting: Vec<TextFormattingSpan>,
73 pub structure: Option<ParagraphStructure>,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct DocumentProjection {
78 pub paragraphs: Vec<ProjectedParagraph>,
79 pub revision_status: RevisionProjectionStatus,
80 pub structural_facts: DocumentStructureFacts,
81}
82
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub struct DocumentPackageProjection {
85 pub document: DocumentProjection,
86 pub review_facts: DocumentReviewFacts,
87}
88
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub struct ProjectionOptions {
91 pub revision_view: RevisionView,
92 pub text_materialization: TextMaterialization,
93}
94
95impl Default for ProjectionOptions {
96 fn default() -> Self {
97 Self {
98 revision_view: RevisionView::Current,
99 text_materialization: TextMaterialization::WordHost,
100 }
101 }
102}
103
104#[derive(Clone, Debug, Eq, PartialEq)]
105pub enum ProjectionError {
106 ArchiveTooLarge,
107 InvalidArchive,
108 TooManyArchiveEntries,
109 InvalidPackageRelationships,
110 PackageRelationshipsTooLarge,
111 MissingDocumentXml,
112 DuplicateDocumentXml,
113 DuplicateStylesXml,
114 EncryptedDocumentXml,
115 UnsupportedCompression(u16),
116 DocumentXmlTooLarge,
117 SuspiciousCompressionRatio,
118 InvalidDocumentXmlEntry,
119 DocumentXmlIntegrity,
120 StylesXmlTooLarge,
121 InvalidStylesXmlEntry,
122 StylesXmlIntegrity,
123 InvalidDocumentXml,
124 InvalidStylesXml,
125 MissingDocumentBody,
126 TooManyParagraphs,
127 TooManyStructuralFacts,
128 InvalidInternalParagraphId,
129 DuplicateInternalParagraphId,
130}
131
132impl fmt::Display for ProjectionError {
133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134 let message = match self {
135 Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
136 Self::InvalidArchive => "DOCX archive is invalid",
137 Self::TooManyArchiveEntries => "DOCX archive has too many entries",
138 Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
139 Self::PackageRelationshipsTooLarge => {
140 "DOCX package relationships exceed the configured size limit"
141 }
142 Self::MissingDocumentXml => "DOCX archive has no main document part",
143 Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
144 Self::DuplicateStylesXml => "DOCX archive has duplicate word/styles.xml entries",
145 Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
146 Self::UnsupportedCompression(_) => {
147 "selected DOCX package part uses unsupported compression"
148 }
149 Self::DocumentXmlTooLarge => {
150 "DOCX main document part exceeds the configured size limit"
151 }
152 Self::SuspiciousCompressionRatio => {
153 "selected DOCX package part exceeds the compression-ratio limit"
154 }
155 Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
156 Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
157 Self::StylesXmlTooLarge => "word/styles.xml exceeds the configured size limit",
158 Self::InvalidStylesXmlEntry => "word/styles.xml has an invalid ZIP entry",
159 Self::StylesXmlIntegrity => "word/styles.xml failed size or CRC validation",
160 Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
161 Self::InvalidStylesXml => "word/styles.xml is invalid XML",
162 Self::MissingDocumentBody => "DOCX main document part has no document body",
163 Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
164 Self::TooManyStructuralFacts => {
165 "DOCX main document part produces too many structural facts"
166 }
167 Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
168 Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
169 };
170 formatter.write_str(message)
171 }
172}
173
174impl std::error::Error for ProjectionError {}
175
176pub fn project_docx<F>(
183 bytes: &[u8],
184 limits: DocxLimits,
185 allocate_id: F,
186) -> Result<DocumentProjection, ProjectionError>
187where
188 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
189{
190 project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
191}
192
193pub fn project_docx_with_options<F>(
200 bytes: &[u8],
201 limits: DocxLimits,
202 options: ProjectionOptions,
203 allocate_id: F,
204) -> Result<DocumentProjection, ProjectionError>
205where
206 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
207{
208 let parts = extract_document_parts(bytes, limits)?;
209 let styles = parts.styles_xml.as_deref().map_or(
210 Err(StructuralFactUnknownReason::StylesPartUnavailable),
211 |styles| {
212 styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
213 },
214 );
215 project_document_xml_with_limit(
216 &parts.document_xml,
217 limits.maximum_paragraphs,
218 limits.maximum_structural_facts,
219 options,
220 styles.as_ref().map_err(|reason| *reason),
221 allocate_id,
222 )
223}
224
225pub fn project_docx_with_review_facts<F>(
237 bytes: &[u8],
238 limits: DocxLimits,
239 review_limits: ReviewFactLimits,
240 options: ProjectionOptions,
241 allocate_id: F,
242) -> Result<DocumentPackageProjection, ProjectionError>
243where
244 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
245{
246 let parts = archive::extract_projection_parts(bytes, limits, review_limits)?;
247 let styles = parts.styles.as_deref().map_or(
248 Err(StructuralFactUnknownReason::StylesPartUnavailable),
249 |styles| {
250 styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
251 },
252 );
253 let ProjectedDocumentWithReview {
254 document,
255 revisions,
256 comment_anchors,
257 } = project_document_xml_with_limit_and_review(
258 &parts.document,
259 limits.maximum_paragraphs,
260 limits.maximum_structural_facts,
261 options,
262 styles.as_ref().map_err(|reason| *reason),
263 Some(ooxml::ReviewProjectionLimits {
264 maximum_facts: review_limits.maximum_facts_per_family,
265 maximum_detail_bytes: review_limits.maximum_review_detail_bytes,
266 }),
267 allocate_id,
268 )?;
269 let review_facts = review::project_review_facts(
270 revisions.unwrap_or(ReviewFactSet::Unknown(
271 ReviewFactUnknownReason::InvalidDocument,
272 )),
273 comment_anchors.as_ref(),
274 &document,
275 parts.comments,
276 parts.comments_extended,
277 review_limits,
278 options.text_materialization,
279 );
280 Ok(DocumentPackageProjection {
281 document,
282 review_facts,
283 })
284}
285
286pub fn project_document_xml<F>(
293 xml: &[u8],
294 allocate_id: F,
295) -> Result<DocumentProjection, ProjectionError>
296where
297 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
298{
299 project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
300}
301
302pub fn project_document_xml_with_options<F>(
309 xml: &[u8],
310 options: ProjectionOptions,
311 allocate_id: F,
312) -> Result<DocumentProjection, ProjectionError>
313where
314 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
315{
316 project_document_xml_with_limit(
317 xml,
318 DocxLimits::default().maximum_paragraphs,
319 DocxLimits::default().maximum_structural_facts,
320 options,
321 Err(StructuralFactUnknownReason::DocumentPartOnly),
322 allocate_id,
323 )
324}
325
326fn project_document_xml_with_limit<F>(
327 xml: &[u8],
328 maximum_paragraphs: usize,
329 maximum_structural_facts: usize,
330 options: ProjectionOptions,
331 styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
332 allocate_id: F,
333) -> Result<DocumentProjection, ProjectionError>
334where
335 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
336{
337 project_document_xml_with_limit_and_review(
338 xml,
339 maximum_paragraphs,
340 maximum_structural_facts,
341 options,
342 styles,
343 None,
344 allocate_id,
345 )
346 .map(|projection| projection.document)
347}
348
349struct ProjectedDocumentWithReview {
350 document: DocumentProjection,
351 revisions: Option<ReviewFactSet<AttributedRevision>>,
352 comment_anchors: Option<HashMap<String, ReviewSpan>>,
353}
354
355fn project_document_xml_with_limit_and_review<F>(
356 xml: &[u8],
357 maximum_paragraphs: usize,
358 maximum_structural_facts: usize,
359 options: ProjectionOptions,
360 styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
361 review_limits: Option<ooxml::ReviewProjectionLimits>,
362 mut allocate_id: F,
363) -> Result<ProjectedDocumentWithReview, ProjectionError>
364where
365 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
366{
367 let projected = ooxml::project_document_xml(
368 xml,
369 maximum_paragraphs,
370 options.revision_view,
371 options.text_materialization,
372 review_limits,
373 )?;
374 let review_revisions = projected.review_revisions;
375 let review_comment_anchors = review_limits
376 .is_some()
377 .then_some(projected.review_comment_anchors);
378 let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
379 let mut ids = Vec::with_capacity(projected.paragraphs.len());
380 for paragraph in &projected.paragraphs {
381 let facts = ParagraphIdentityFacts {
382 ordinal: paragraph.ordinal,
383 package_paragraph_id: paragraph.package_paragraph_id,
384 text: ¶graph.text,
385 };
386 let id = allocate_id(facts)?;
387 if !seen_ids.insert(id.clone()) {
388 return Err(ProjectionError::DuplicateInternalParagraphId);
389 }
390 ids.push(id);
391 }
392 let texts = projected
393 .paragraphs
394 .iter()
395 .map(|paragraph| paragraph.text.as_str())
396 .collect::<Vec<_>>();
397 let properties = projected
398 .paragraphs
399 .iter()
400 .map(|paragraph| paragraph.properties.clone())
401 .collect::<Vec<_>>();
402 let structural_facts = structure::materialize_structure(
403 structure::RawStructureInput {
404 paragraph_texts: &texts,
405 properties: &properties,
406 bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
407 references: projected.references.as_deref().map_err(|reason| *reason),
408 },
409 styles,
410 maximum_structural_facts,
411 )?;
412 let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
413 for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
414 paragraphs.push(ProjectedParagraph {
415 id,
416 ordinal: paragraph.ordinal,
417 package_paragraph_id: paragraph.package_paragraph_id,
418 text: paragraph.text,
419 formatting: paragraph.formatting,
420 structure: paragraph.structure,
421 });
422 }
423 Ok(ProjectedDocumentWithReview {
424 document: DocumentProjection {
425 paragraphs,
426 revision_status: projected.revision_status,
427 structural_facts,
428 },
429 revisions: review_revisions,
430 comment_anchors: review_comment_anchors,
431 })
432}