///|
priv struct ZipWriteEntry {
path : String
content : Bytes
}
///|
priv enum ContentTypeXmlEntry {
DefaultContentType(extension~ : String, content_type~ : String)
OverrideContentType(part_name~ : String, content_type~ : String)
}
///|
/// Embeds or replaces the DOCX style map part.
pub fn embed_style_map(
docx : BytesView,
style_map : String,
) -> Bytes raise DocxError {
let entries = unzip_write_entries(docx)
let zip = zip_archive_from_write_entries(entries)
let relationships = write_relationships_with_embedded_style_map(zip)
let content_types = write_content_types_with_embedded_style_map(zip)
replace_or_append_zip_entry(
entries,
"mammoth/style-map",
@utf8.encode(style_map),
)
replace_or_append_zip_entry(
entries,
"word/_rels/document.xml.rels",
@utf8.encode(relationships),
)
replace_or_append_zip_entry(
entries,
"[Content_Types].xml",
@utf8.encode(content_types),
)
zip_write_entries(entries)
}
///|
fn unzip_write_entries(
data : BytesView,
) -> Array[ZipWriteEntry] raise DocxError {
let archive = @mbtzip.read(data) catch {
err => raise InvalidZip(message="invalid ZIP archive: \{repr(err)}")
}
let entries : Array[ZipWriteEntry] = []
for entry in archive.entries() {
entries.push({ path: entry.name(), content: entry.data().to_owned() })
}
entries
}
///|
fn zip_archive_from_write_entries(
entries : Array[ZipWriteEntry],
) -> ZipArchive raise DocxError {
let source : Array[(String, BytesView)] = []
for entry in entries {
source.push((entry.path, entry.content))
}
@zip.zip_archive_entries(source.iter())
}
///|
fn replace_or_append_zip_entry(
entries : Array[ZipWriteEntry],
path : String,
content : Bytes,
) -> Unit {
let canonical_path = path.to_lower()
for index in 0.. Bytes raise DocxError {
let archive = @mbtzip.Archive::new()
for entry in entries {
archive.add(entry.path, entry.content, compression=Deflate)
}
@mbtzip.write(archive) catch {
err => raise InvalidZip(message="failed to write ZIP archive: \{repr(err)}")
}
}
///|
fn write_relationships_with_embedded_style_map(
zip : ZipArchive,
) -> String raise DocxError {
let path = "word/_rels/document.xml.rels"
let bytes = match zip.read_bytes(path) {
Some(value) => value
None => raise MissingPart(message="missing DOCX part: " + path)
}
let root = read_mutation_relationships(
path,
bytes,
local_input_linear_xml_budget(bytes),
)
let relationships = read_relationships_xml(root).relationships
write_relationships_xml(upsert_embedded_style_map_relationship(relationships))
}
///|
fn upsert_embedded_style_map_relationship(
relationships : Array[Relationship],
) -> Array[Relationship] raise DocxError {
let updated : Array[Relationship] = []
let mut found = false
for relationship in relationships {
if relationship.id == "rMammothStyleMap" {
if found {
raise InvalidXml(
message="duplicate embedded style-map relationship Id: rMammothStyleMap",
)
}
updated.push(embedded_style_map_relationship())
found = true
} else {
updated.push(relationship)
}
}
if !found {
updated.push(embedded_style_map_relationship())
}
updated
}
///|
fn embedded_style_map_relationship() -> Relationship {
{
id: "rMammothStyleMap",
relationship_type: "http://schemas.zwobble.org/mammoth/style-map",
target: "/mammoth/style-map",
target_mode: None,
}
}
///|
fn write_relationships_xml(relationships : Array[Relationship]) -> String {
let children : Array[XmlNode] = []
for relationship in relationships {
let attributes : Map[String, String] = {
"Id": relationship.id,
"Type": relationship.relationship_type,
"Target": relationship.target,
}
match relationship.target_mode {
Some(target_mode) => attributes["TargetMode"] = target_mode
None => ()
}
children.push(XmlElement(xml_element("Relationship", attributes~)))
}
write_xml_string(xml_element("Relationships", children~), namespaces={
"": "http://schemas.openxmlformats.org/package/2006/relationships",
})
}
///|
fn write_content_types_with_embedded_style_map(
zip : ZipArchive,
) -> String raise DocxError {
let path = "[Content_Types].xml"
let style_map_part_name = match zip.resolve_path("mammoth/style-map") {
Some(physical_path) => "/" + physical_path
None => "/mammoth/style-map"
}
let root = read_xml_string(
read_required_zip_text(zip, path),
namespace_map=office_namespace_map(),
)
let entries = read_content_type_xml_entries(root)
write_content_types_xml(
upsert_embedded_style_map_content_type(entries, style_map_part_name),
)
}
///|
fn read_content_type_xml_entries(
root : XmlElement,
) -> Array[ContentTypeXmlEntry] {
let entries : Array[ContentTypeXmlEntry] = []
for node in root.children {
match node {
XmlElement(element) =>
match element.name {
"content-types:Default" => {
let extension = element.attributes.get_or_default("Extension", "")
let content_type = element.attributes.get_or_default(
"ContentType", "",
)
if extension != "" && content_type != "" {
entries.push(DefaultContentType(extension~, content_type~))
}
}
"content-types:Override" => {
let part_name = element.attributes.get_or_default("PartName", "")
let content_type = element.attributes.get_or_default(
"ContentType", "",
)
if part_name != "" && content_type != "" {
entries.push(OverrideContentType(part_name~, content_type~))
}
}
_ => ()
}
_ => ()
}
}
entries
}
///|
fn upsert_embedded_style_map_content_type(
entries : Array[ContentTypeXmlEntry],
style_map_part_name : String,
) -> Array[ContentTypeXmlEntry] raise DocxError {
let updated : Array[ContentTypeXmlEntry] = []
let mut found = false
for entry in entries {
match entry {
OverrideContentType(part_name~, content_type~) =>
if is_embedded_style_map_part_name(part_name) {
if found {
raise InvalidXml(
message="duplicate embedded style-map content-type override after OPC path normalization",
)
}
updated.push(embedded_style_map_content_type(style_map_part_name))
found = true
} else {
updated.push(OverrideContentType(part_name~, content_type~))
}
_ => updated.push(entry)
}
}
if !found {
updated.push(embedded_style_map_content_type(style_map_part_name))
}
updated
}
///|
fn is_embedded_style_map_part_name(part_name : String) -> Bool {
if !part_name.has_prefix("/") {
return false
}
match @opc.resolve_part_target("", part_name) {
Some(path) =>
@opc.part_name_key(path) == @opc.part_name_key("mammoth/style-map")
None => false
}
}
///|
fn embedded_style_map_content_type(part_name : String) -> ContentTypeXmlEntry {
OverrideContentType(part_name~, content_type="text/prs.mammoth.style-map")
}
///|
fn write_content_types_xml(entries : Array[ContentTypeXmlEntry]) -> String {
let children : Array[XmlNode] = []
for entry in entries {
match entry {
DefaultContentType(extension~, content_type~) =>
children.push(
XmlElement(
xml_element("Default", attributes={
"Extension": extension,
"ContentType": content_type,
}),
),
)
OverrideContentType(part_name~, content_type~) =>
children.push(
XmlElement(
xml_element("Override", attributes={
"PartName": part_name,
"ContentType": content_type,
}),
),
)
}
}
write_xml_string(xml_element("Types", children~), namespaces={
"": "http://schemas.openxmlformats.org/package/2006/content-types",
})
}
///|
fn read_required_zip_text(
zip : ZipArchive,
path : String,
) -> String raise DocxError {
match zip.read_text(path) {
Some(text) => text
None => raise MissingPart(message="missing DOCX part: " + path)
}
}