pub const W_NS: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
pub const RPR: &[&str] = &[
"ins",
"del",
"moveFrom",
"moveTo",
"rStyle",
"rFonts",
"b",
"bCs",
"i",
"iCs",
"caps",
"smallCaps",
"strike",
"dstrike",
"outline",
"shadow",
"emboss",
"imprint",
"noProof",
"snapToGrid",
"vanish",
"webHidden",
"color",
"spacing",
"w",
"kern",
"position",
"sz",
"szCs",
"highlight",
"u",
"effect",
"bdr",
"shd",
"fitText",
"vertAlign",
"rtl",
"cs",
"em",
"lang",
"eastAsianLayout",
"specVanish",
"oMath",
"rPrChange",
];
pub const PPR: &[&str] = &[
"pStyle",
"keepNext",
"keepLines",
"pageBreakBefore",
"framePr",
"widowControl",
"numPr",
"suppressLineNumbers",
"pBdr",
"shd",
"tabs",
"suppressAutoHyphens",
"kinsoku",
"wordWrap",
"overflowPunct",
"topLinePunct",
"autoSpaceDE",
"autoSpaceDN",
"bidi",
"adjustRightInd",
"snapToGrid",
"spacing",
"ind",
"contextualSpacing",
"mirrorIndents",
"suppressOverlap",
"jc",
"textDirection",
"textAlignment",
"textboxTightWrap",
"outlineLvl",
"divId",
"cnfStyle",
"rPr",
"sectPr",
"pPrChange",
];
pub struct Cx {
pub w: String,
ns: String,
}
impl Cx {
pub fn of(src: &super::super::Source) -> Cx {
Cx::of_xml(&src.xml)
}
pub fn of_xml(x: &str) -> Cx {
let start = x.char_indices().find(|&(i, c)| c == '<' && x[i + 1..].starts_with(|c: char| c.is_alphabetic())).map_or(0, |(i, _)| i);
let tag = &x[start..start + x[start..].find('>').unwrap_or(0)];
let ns: Vec<&str> = tag.split_whitespace().skip(1).filter(|a| a.starts_with("xmlns")).map(|a| a.trim_end_matches('/')).collect();
let w = ns
.iter()
.find(|a| a.ends_with(&format!("\"{W_NS}\"")))
.and_then(|a| a.strip_prefix("xmlns:").map(|p| format!("{}:", &p[..p.find('=').unwrap_or(0)])).or(Some(String::new())))
.unwrap_or_else(|| "w:".into());
Cx { w, ns: ns.join(" ") }
}
pub fn parse<T>(&self, raw: &str, f: impl FnOnce(&str, roxmltree::Node) -> T) -> Option<T> {
let text = format!("<x {}>{raw}</x>", self.ns);
let doc = roxmltree::Document::parse(&text).ok()?;
let el = doc.root_element().first_element_child()?;
Some(f(&text, el))
}
}
pub struct Props {
open: String,
close: String,
order: &'static [&'static str],
pub kids: Vec<(String, String)>,
}
impl Props {
pub fn parse(raw: Option<&str>, local: &str, cx: &Cx) -> Props {
let w = &cx.w;
let order = match local {
"pPr" => PPR,
"numPr" => &["ilvl", "numId", "numberingChange", "ins"],
_ => RPR,
};
let mut p = Props { open: format!("<{w}{local}>"), close: format!("</{w}{local}>"), order, kids: Vec::new() };
let parsed = raw.and_then(|r| {
cx.parse(r, |text, el| {
let open = el.first_child().map(|c| text[el.range().start..c.range().start].to_string());
(open, el.children().filter(|c| c.is_element()).map(|c| (c.tag_name().name().to_string(), text[c.range()].to_string())).collect())
})
});
if let Some((open, kids)) = parsed {
p.open = open.unwrap_or(p.open);
p.kids = kids;
}
p
}
pub fn reorder(&mut self, order: &'static [&'static str]) {
self.order = order;
}
fn rank(&self, name: &str) -> usize {
self.order.iter().position(|n| *n == name).unwrap_or(self.order.len() - 2)
}
pub fn get(&self, local: &str) -> Option<&str> {
self.kids.iter().find(|(n, _)| n == local).map(|(_, x)| x.as_str())
}
pub fn set(&mut self, local: &str, xml: String) {
if let Some(k) = self.kids.iter_mut().find(|(n, _)| n == local) {
k.1 = xml;
return;
}
let r = self.rank(local);
let at = self.kids.iter().position(|(n, _)| self.rank(n) > r).unwrap_or(self.kids.len());
self.kids.insert(at, (local.to_string(), xml));
}
pub fn add(&mut self, local: &str, xml: String) {
let r = self.rank(local);
let at = self.kids.iter().position(|(n, _)| self.rank(n) > r).unwrap_or(self.kids.len());
self.kids.insert(at, (local.to_string(), xml));
}
pub fn remove(&mut self, local: &str) {
self.kids.retain(|(n, _)| n != local);
}
pub fn attrs(&mut self, local: &str, set: &[(&str, Option<String>)], cx: &Cx) {
let w = &cx.w;
let mut attrs: Vec<(String, String)> = self
.get(local)
.and_then(|x| cx.parse(x, |text, el| attr_list(&text[el.range().start..el.first_child().map_or(el.range().end, |c| c.range().start)])))
.unwrap_or_default();
for (name, value) in set {
let at = attrs.iter().position(|(q, _)| q.rsplit(':').next() == Some(name));
match (at, value) {
(Some(i), Some(v)) => attrs[i].1 = esc(v),
(Some(i), None) => {
attrs.remove(i);
}
(None, Some(v)) => attrs.push((format!("{w}{name}"), esc(v))),
(None, None) => {}
}
}
let a: String = attrs.iter().map(|(q, v)| format!(" {q}=\"{v}\"")).collect();
self.set(local, format!("<{w}{local}{a}/>"));
}
pub fn xml(&self) -> Option<String> {
(!self.kids.is_empty()).then(|| format!("{}{}{}", self.open, self.kids.iter().map(|(_, x)| x.as_str()).collect::<String>(), self.close))
}
}
fn attr_list(tag: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut rest = tag.trim_start_matches('<');
rest = rest.trim_start_matches(|c: char| !c.is_whitespace());
while let Some(eq) = rest.find('=') {
let name = rest[..eq].trim().to_string();
let after = rest[eq + 1..].trim_start();
let Some(q) = after.chars().next() else { break };
let Some(end) = after[1..].find(q) else { break };
out.push((name, after[1..end + 1].to_string()));
rest = &after[end + 2..];
}
out
}
pub fn esc(s: &str) -> String {
s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
}