1mod archive;
8mod compatibility;
9mod namespaces;
10mod ooxml;
11mod relationships;
12mod review;
13mod structure;
14mod styles;
15
16use std::collections::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 (document, revisions) = project_document_xml_with_limit_and_review(
254 &parts.document,
255 limits.maximum_paragraphs,
256 limits.maximum_structural_facts,
257 options,
258 styles.as_ref().map_err(|reason| *reason),
259 Some(review_limits.maximum_facts_per_family),
260 allocate_id,
261 )?;
262 let review_facts = review::project_review_facts(
263 revisions.unwrap_or(ReviewFactSet::Unknown(
264 ReviewFactUnknownReason::InvalidDocument,
265 )),
266 parts.comments,
267 parts.comments_extended,
268 review_limits,
269 );
270 Ok(DocumentPackageProjection {
271 document,
272 review_facts,
273 })
274}
275
276pub fn project_document_xml<F>(
283 xml: &[u8],
284 allocate_id: F,
285) -> Result<DocumentProjection, ProjectionError>
286where
287 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
288{
289 project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
290}
291
292pub fn project_document_xml_with_options<F>(
299 xml: &[u8],
300 options: ProjectionOptions,
301 allocate_id: F,
302) -> Result<DocumentProjection, ProjectionError>
303where
304 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
305{
306 project_document_xml_with_limit(
307 xml,
308 DocxLimits::default().maximum_paragraphs,
309 DocxLimits::default().maximum_structural_facts,
310 options,
311 Err(StructuralFactUnknownReason::DocumentPartOnly),
312 allocate_id,
313 )
314}
315
316fn project_document_xml_with_limit<F>(
317 xml: &[u8],
318 maximum_paragraphs: usize,
319 maximum_structural_facts: usize,
320 options: ProjectionOptions,
321 styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
322 allocate_id: F,
323) -> Result<DocumentProjection, ProjectionError>
324where
325 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
326{
327 project_document_xml_with_limit_and_review(
328 xml,
329 maximum_paragraphs,
330 maximum_structural_facts,
331 options,
332 styles,
333 None,
334 allocate_id,
335 )
336 .map(|(projection, _)| projection)
337}
338
339fn project_document_xml_with_limit_and_review<F>(
340 xml: &[u8],
341 maximum_paragraphs: usize,
342 maximum_structural_facts: usize,
343 options: ProjectionOptions,
344 styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
345 maximum_review_facts: Option<usize>,
346 mut allocate_id: F,
347) -> Result<
348 (
349 DocumentProjection,
350 Option<ReviewFactSet<AttributedRevision>>,
351 ),
352 ProjectionError,
353>
354where
355 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
356{
357 let projected = ooxml::project_document_xml(
358 xml,
359 maximum_paragraphs,
360 options.revision_view,
361 options.text_materialization,
362 maximum_review_facts,
363 )?;
364 let review_revisions = projected.review_revisions;
365 let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
366 let mut ids = Vec::with_capacity(projected.paragraphs.len());
367 for paragraph in &projected.paragraphs {
368 let facts = ParagraphIdentityFacts {
369 ordinal: paragraph.ordinal,
370 package_paragraph_id: paragraph.package_paragraph_id,
371 text: ¶graph.text,
372 };
373 let id = allocate_id(facts)?;
374 if !seen_ids.insert(id.clone()) {
375 return Err(ProjectionError::DuplicateInternalParagraphId);
376 }
377 ids.push(id);
378 }
379 let texts = projected
380 .paragraphs
381 .iter()
382 .map(|paragraph| paragraph.text.as_str())
383 .collect::<Vec<_>>();
384 let properties = projected
385 .paragraphs
386 .iter()
387 .map(|paragraph| paragraph.properties.clone())
388 .collect::<Vec<_>>();
389 let structural_facts = structure::materialize_structure(
390 structure::RawStructureInput {
391 paragraph_texts: &texts,
392 properties: &properties,
393 bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
394 references: projected.references.as_deref().map_err(|reason| *reason),
395 },
396 styles,
397 maximum_structural_facts,
398 )?;
399 let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
400 for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
401 paragraphs.push(ProjectedParagraph {
402 id,
403 ordinal: paragraph.ordinal,
404 package_paragraph_id: paragraph.package_paragraph_id,
405 text: paragraph.text,
406 formatting: paragraph.formatting,
407 structure: paragraph.structure,
408 });
409 }
410 Ok((
411 DocumentProjection {
412 paragraphs,
413 revision_status: projected.revision_status,
414 structural_facts,
415 },
416 review_revisions,
417 ))
418}