extend_docx/parse/
content_types.rs1use std::collections::HashMap;
2
3use crate::package::OoxmlPackage;
4use crate::xml::get_attribute;
5
6use super::context::ContentTypeLookup;
7
8pub fn parse_content_types(pkg: &OoxmlPackage) -> ContentTypeLookup {
9 let mut default_by_extension = HashMap::new();
10 let mut override_by_part_name = HashMap::new();
11
12 let Some(xml) = pkg
13 .parts
14 .get("[Content_Types].xml")
15 .map(|part| part.content.as_str())
16 else {
17 return ContentTypeLookup {
18 default_by_extension,
19 override_by_part_name,
20 };
21 };
22
23 for tag in find_opening_tags(xml, "Default") {
24 let extension = get_attribute(&tag, "Extension").map(|value| value.to_ascii_lowercase());
25 let content_type = get_attribute(&tag, "ContentType");
26 if let (Some(extension), Some(content_type)) = (extension, content_type) {
27 default_by_extension.insert(extension, content_type);
28 }
29 }
30
31 for tag in find_opening_tags(xml, "Override") {
32 let part_name = get_attribute(&tag, "PartName");
33 let content_type = get_attribute(&tag, "ContentType");
34 if let (Some(part_name), Some(content_type)) = (part_name, content_type) {
35 override_by_part_name.insert(part_name.clone(), content_type.clone());
36 if !part_name.starts_with('/') {
37 override_by_part_name.insert(format!("/{part_name}"), content_type);
38 }
39 }
40 }
41
42 ContentTypeLookup {
43 default_by_extension,
44 override_by_part_name,
45 }
46}
47
48fn find_opening_tags(xml: &str, local_name: &str) -> Vec<String> {
49 let open_prefix = format!("<{local_name}");
50 let mut tags = Vec::new();
51 let mut index = 0;
52
53 while index < xml.len() {
54 let Some(tag_start) = find_case_insensitive_at(xml, index, &open_prefix) else {
55 break;
56 };
57
58 if !has_word_boundary_after(xml, tag_start + open_prefix.len()) {
59 index = tag_start + 1;
60 continue;
61 }
62
63 let Some(relative_gt) = xml[tag_start..].find('>') else {
64 break;
65 };
66
67 let tag_end = tag_start + relative_gt + 1;
68 tags.push(xml[tag_start..tag_end].to_string());
69 index = tag_end;
70 }
71
72 tags
73}
74
75fn has_word_boundary_after(xml: &str, index: usize) -> bool {
76 xml.as_bytes()
77 .get(index)
78 .is_none_or(|byte| !byte.is_ascii_alphanumeric() && *byte != b'_')
79}
80
81fn find_case_insensitive_at(haystack: &str, start: usize, needle: &str) -> Option<usize> {
82 crate::xml::find_ignore_ascii_case(haystack, start, needle)
83}