mod archive;
mod compatibility;
mod namespaces;
mod ooxml;
mod relationships;
mod review;
mod structure;
mod styles;
use std::collections::{HashMap, HashSet};
use std::fmt;
pub use archive::{DocumentParts, DocxLimits, extract_document_parts, extract_document_xml};
pub use ooxml::{
PackageParagraphId, ParagraphStructure, RevisionProjectionStatus, RevisionUnsupportedReason,
RevisionView, TextFormattingSpan, TextMaterialization, TextStyle, is_semantic_highlight_color,
};
pub use review::{
AttributedComment, AttributedRevision, CommentContent, DocumentReviewFacts, ReviewDetail,
ReviewFactLimits, ReviewFactSet, ReviewFactUnknownReason, ReviewPoint, ReviewSpan,
RevisionContent, RevisionFactKind,
};
pub use structure::{
BookmarkFact, DocumentStructureFacts, InternalReferenceFact, InternalReferenceRole,
NumberingHierarchyFact, ParagraphIndentation, ParagraphIndentationFact,
ParagraphOutlineLevelFact, SpanCoverage, StructuralFactSet, StructuralFactUnknownReason,
StructuralSpan,
};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct InternalParagraphId(String);
impl InternalParagraphId {
pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
let value = value.into();
if value.is_empty() || value.len() > 128 {
return Err(ProjectionError::InvalidInternalParagraphId);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug)]
pub struct ParagraphIdentityFacts<'a> {
pub ordinal: usize,
pub package_paragraph_id: Option<PackageParagraphId>,
pub text: &'a str,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProjectedParagraph {
pub id: InternalParagraphId,
pub ordinal: usize,
pub package_paragraph_id: Option<PackageParagraphId>,
pub style_id: Option<String>,
pub text: String,
pub formatting: Vec<TextFormattingSpan>,
pub structure: Option<ParagraphStructure>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DocumentProjection {
pub paragraphs: Vec<ProjectedParagraph>,
pub revision_status: RevisionProjectionStatus,
pub structural_facts: DocumentStructureFacts,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DocumentPackageProjection {
pub document: DocumentProjection,
pub review_facts: DocumentReviewFacts,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProjectionOptions {
pub revision_view: RevisionView,
pub text_materialization: TextMaterialization,
}
impl Default for ProjectionOptions {
fn default() -> Self {
Self {
revision_view: RevisionView::Current,
text_materialization: TextMaterialization::WordHost,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProjectionError {
ArchiveTooLarge,
InvalidArchive,
TooManyArchiveEntries,
InvalidPackageRelationships,
PackageRelationshipsTooLarge,
MissingDocumentXml,
DuplicateDocumentXml,
DuplicateStylesXml,
EncryptedDocumentXml,
UnsupportedCompression(u16),
DocumentXmlTooLarge,
SuspiciousCompressionRatio,
InvalidDocumentXmlEntry,
DocumentXmlIntegrity,
StylesXmlTooLarge,
InvalidStylesXmlEntry,
StylesXmlIntegrity,
InvalidDocumentXml,
InvalidStylesXml,
MissingDocumentBody,
TooManyParagraphs,
TooManyStructuralFacts,
InvalidInternalParagraphId,
DuplicateInternalParagraphId,
}
impl fmt::Display for ProjectionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
Self::InvalidArchive => "DOCX archive is invalid",
Self::TooManyArchiveEntries => "DOCX archive has too many entries",
Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
Self::PackageRelationshipsTooLarge => {
"DOCX package relationships exceed the configured size limit"
}
Self::MissingDocumentXml => "DOCX archive has no main document part",
Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
Self::DuplicateStylesXml => "DOCX archive has duplicate word/styles.xml entries",
Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
Self::UnsupportedCompression(_) => {
"selected DOCX package part uses unsupported compression"
}
Self::DocumentXmlTooLarge => {
"DOCX main document part exceeds the configured size limit"
}
Self::SuspiciousCompressionRatio => {
"selected DOCX package part exceeds the compression-ratio limit"
}
Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
Self::StylesXmlTooLarge => "word/styles.xml exceeds the configured size limit",
Self::InvalidStylesXmlEntry => "word/styles.xml has an invalid ZIP entry",
Self::StylesXmlIntegrity => "word/styles.xml failed size or CRC validation",
Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
Self::InvalidStylesXml => "word/styles.xml is invalid XML",
Self::MissingDocumentBody => "DOCX main document part has no document body",
Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
Self::TooManyStructuralFacts => {
"DOCX main document part produces too many structural facts"
}
Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
};
formatter.write_str(message)
}
}
impl std::error::Error for ProjectionError {}
pub fn project_docx<F>(
bytes: &[u8],
limits: DocxLimits,
allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
}
pub fn project_docx_with_options<F>(
bytes: &[u8],
limits: DocxLimits,
options: ProjectionOptions,
allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
let parts = extract_document_parts(bytes, limits)?;
let styles = parts.styles_xml.as_deref().map_or(
Err(StructuralFactUnknownReason::StylesPartUnavailable),
|styles| {
styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
},
);
project_document_xml_with_limit(
&parts.document_xml,
limits.maximum_paragraphs,
limits.maximum_structural_facts,
options,
styles.as_ref().map_err(|reason| *reason),
allocate_id,
)
}
pub fn project_docx_with_review_facts<F>(
bytes: &[u8],
limits: DocxLimits,
review_limits: ReviewFactLimits,
options: ProjectionOptions,
allocate_id: F,
) -> Result<DocumentPackageProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
let parts = archive::extract_projection_parts(bytes, limits, review_limits)?;
let styles = parts.styles.as_deref().map_or(
Err(StructuralFactUnknownReason::StylesPartUnavailable),
|styles| {
styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
},
);
let ProjectedDocumentWithReview {
document,
revisions,
comment_anchors,
} = project_document_xml_with_limit_and_review(
&parts.document,
limits.maximum_paragraphs,
limits.maximum_structural_facts,
options,
styles.as_ref().map_err(|reason| *reason),
Some(ooxml::ReviewProjectionLimits {
maximum_facts: review_limits.maximum_facts_per_family,
maximum_detail_bytes: review_limits.maximum_review_detail_bytes,
}),
allocate_id,
)?;
let review_facts = review::project_review_facts(
revisions.unwrap_or(ReviewFactSet::Unknown(
ReviewFactUnknownReason::InvalidDocument,
)),
comment_anchors.as_ref(),
&document,
parts.comments,
parts.comments_extended,
review_limits,
options.text_materialization,
);
Ok(DocumentPackageProjection {
document,
review_facts,
})
}
pub fn project_document_xml<F>(
xml: &[u8],
allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
}
pub fn project_document_xml_with_options<F>(
xml: &[u8],
options: ProjectionOptions,
allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
project_document_xml_with_limit(
xml,
DocxLimits::default().maximum_paragraphs,
DocxLimits::default().maximum_structural_facts,
options,
Err(StructuralFactUnknownReason::DocumentPartOnly),
allocate_id,
)
}
fn project_document_xml_with_limit<F>(
xml: &[u8],
maximum_paragraphs: usize,
maximum_structural_facts: usize,
options: ProjectionOptions,
styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
project_document_xml_with_limit_and_review(
xml,
maximum_paragraphs,
maximum_structural_facts,
options,
styles,
None,
allocate_id,
)
.map(|projection| projection.document)
}
struct ProjectedDocumentWithReview {
document: DocumentProjection,
revisions: Option<ReviewFactSet<AttributedRevision>>,
comment_anchors: Option<HashMap<String, ReviewSpan>>,
}
fn project_document_xml_with_limit_and_review<F>(
xml: &[u8],
maximum_paragraphs: usize,
maximum_structural_facts: usize,
options: ProjectionOptions,
styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
review_limits: Option<ooxml::ReviewProjectionLimits>,
mut allocate_id: F,
) -> Result<ProjectedDocumentWithReview, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
let projected = ooxml::project_document_xml(
xml,
maximum_paragraphs,
options.revision_view,
options.text_materialization,
review_limits,
)?;
let review_revisions = projected.review_revisions;
let review_comment_anchors = review_limits
.is_some()
.then_some(projected.review_comment_anchors);
let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
let mut ids = Vec::with_capacity(projected.paragraphs.len());
for paragraph in &projected.paragraphs {
let facts = ParagraphIdentityFacts {
ordinal: paragraph.ordinal,
package_paragraph_id: paragraph.package_paragraph_id,
text: ¶graph.text,
};
let id = allocate_id(facts)?;
if !seen_ids.insert(id.clone()) {
return Err(ProjectionError::DuplicateInternalParagraphId);
}
ids.push(id);
}
let texts = projected
.paragraphs
.iter()
.map(|paragraph| paragraph.text.as_str())
.collect::<Vec<_>>();
let properties = projected
.paragraphs
.iter()
.map(|paragraph| ¶graph.properties)
.collect::<Vec<_>>();
let structural_facts = structure::materialize_structure(
structure::RawStructureInput {
paragraph_texts: &texts,
properties: &properties,
bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
references: projected.references.as_deref().map_err(|reason| *reason),
},
styles,
maximum_structural_facts,
)?;
let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
paragraphs.push(ProjectedParagraph {
id,
ordinal: paragraph.ordinal,
package_paragraph_id: paragraph.package_paragraph_id,
style_id: paragraph.properties.style_id,
text: paragraph.text,
formatting: paragraph.formatting,
structure: paragraph.structure,
});
}
Ok(ProjectedDocumentWithReview {
document: DocumentProjection {
paragraphs,
revision_status: projected.revision_status,
structural_facts,
},
revisions: review_revisions,
comment_anchors: review_comment_anchors,
})
}