Skip to main content

easydoc_writer/util/
xml_insert.rs

1//! XML post-processing helpers for attributes not natively supported
2//! by `docx-rs` (e.g. `noWrap`, `numFmt`).
3
4/// Inserts `content` immediately after the **n-th** occurrence of `pattern`
5/// (0-indexed) inside `xml`.  Returns the modified string, or the original
6/// if the pattern occurs fewer than `n + 1` times.
7///
8/// This is used for targeted XML post-processing where `docx-rs` does not
9/// natively emit certain OOXML elements (e.g. `<w:noWrap/>`, `<w:numFmt>`).
10///
11/// # Examples
12///
13/// ```
14/// use easydoc_writer::util::insert_after_nth;
15///
16/// let s = "<a/><b/><a/>";
17/// let out = insert_after_nth(s, "<a/>", 1, "<!-- insert -->");
18/// assert_eq!(out, "<a/><b/><a/><!-- insert -->");
19///
20/// let first = insert_after_nth(s, "<a/>", 0, "<!-- first -->");
21/// assert_eq!(first, "<a/><!-- first --><b/><a/>");
22/// ```
23#[must_use]
24pub fn insert_after_nth(xml: &str, pattern: &str, n: usize, content: &str) -> String {
25    let mut offset = 0usize;
26    let mut remaining = xml;
27    let mut count = 0usize;
28
29    while let Some(pos) = remaining.find(pattern) {
30        if count == n {
31            let insert_at = offset + pos + pattern.len();
32            let mut result = String::with_capacity(xml.len() + content.len());
33            result.push_str(&xml[..insert_at]);
34            result.push_str(content);
35            result.push_str(&xml[insert_at..]);
36            return result;
37        }
38        count += 1;
39        let advance = pos + pattern.len();
40        offset += advance;
41        remaining = &remaining[advance..];
42    }
43
44    xml.to_string()
45}
46
47/// 在线性时间内将 `contents` 依次插入到 `pattern` 第 `n` 次出现之后。
48///
49/// 与反复调用 [`insert_after_nth`] 不同(每次 O(n) 扫描,累计 O(n²)),
50/// 本函数单次扫描 XML,按序定位所有插入点后一次性构建结果,整体 O(n)。
51///
52/// `contents` 中的元素按索引对应 `pattern` 的出现序号(0-indexed);
53/// 出现次数不足的条目被忽略。
54///
55/// # Examples
56///
57/// ```
58/// use easydoc_writer::util::insert_many_after_nth;
59///
60/// let s = "<a/><b/><a/><b/><a/>";
61/// let out = insert_many_after_nth(s, "<b/>", &["!".to_owned(), "?".to_owned()]);
62/// assert_eq!(out, "<a/><b/>!<a/><b/>?<a/>");
63/// ```
64#[must_use]
65pub fn insert_many_after_nth(xml: &str, pattern: &str, contents: &[String]) -> String {
66    if contents.is_empty() {
67        return xml.to_owned();
68    }
69
70    let mut result =
71        String::with_capacity(xml.len() + contents.iter().map(String::len).sum::<usize>());
72    let mut search_from = 0usize;
73    let mut occurrence = 0usize;
74
75    while occurrence < contents.len() {
76        let Some(rel) = xml[search_from..].find(pattern) else {
77            // 剩余内容无更多匹配,追加到结尾
78            result.push_str(&xml[search_from..]);
79            return result;
80        };
81        let abs = search_from + rel;
82        result.push_str(&xml[search_from..abs + pattern.len()]);
83        result.push_str(&contents[occurrence]);
84        search_from = abs + pattern.len();
85        occurrence += 1;
86    }
87
88    result.push_str(&xml[search_from..]);
89    result
90}
91
92/// Inserts `<w:noWrap/>` into the cell-property XML fragment for a cell
93/// whose `wrap` attribute is `false` (i.e. text should **not** wrap).
94///
95/// Targets either:
96/// - `<w:tcW ... />` -- when the cell has an explicit width set, or
97/// - `<w:tcPr>` / `<w:tcPr />` -- as a fallback when no width is present.
98///
99/// In OOXML, the **absence** of `<w:noWrap/>` means wrapping is enabled,
100/// so we only need to insert it when wrapping is disabled.
101#[must_use]
102pub fn insert_no_wrap(xml: &str, cell_index: usize) -> String {
103    // Prefer inserting after <w:tcW ... /> when it exists.
104    if xml.matches("<w:tcW").count() > cell_index {
105        return insert_after_nth(xml, "<w:tcW", cell_index, "<w:noWrap/>");
106    }
107    // Fallback: insert after <w:tcPr> (covers both empty and non-empty forms).
108    if xml.matches("<w:tcPr").count() > cell_index {
109        return insert_after_nth(xml, "<w:tcPr", cell_index, "<w:noWrap/>");
110    }
111    xml.to_string()
112}
113
114/// Inserts `<w:numFmt w:val="FORMAT"/>` into the paragraph run-properties
115/// of the cell at `cell_index`.
116///
117/// The number format is placed inside `<w:rPr>` that appears as a direct
118/// child of `<w:pPr>` (the paragraph-level default run properties), which
119/// is the canonical location for cell-level number formatting in OOXML.
120#[must_use]
121pub fn insert_num_fmt(xml: &str, cell_index: usize, format: &str) -> String {
122    let pattern = "<w:pPr><w:rPr";
123    if xml.matches(pattern).count() > cell_index {
124        let num_fmt = format!("<w:numFmt w:val=\"{format}\"/>");
125        return insert_after_nth(xml, pattern, cell_index, &num_fmt);
126    }
127    xml.to_string()
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn insert_after_nth_first() {
136        let out = insert_after_nth("AAxBBxAA", "AA", 0, "!");
137        assert_eq!(out, "AA!xBBxAA");
138    }
139
140    #[test]
141    fn insert_after_nth_second() {
142        // n=1 means "after the 2nd occurrence" (0-indexed).
143        let out = insert_after_nth("AAxBBxAA", "AA", 1, "!");
144        assert_eq!(out, "AAxBBxAA!");
145    }
146
147    #[test]
148    fn insert_after_nth_not_found() {
149        let out = insert_after_nth("xBBx", "AA", 0, "!");
150        assert_eq!(out, "xBBx");
151    }
152
153    #[test]
154    fn insert_after_nth_out_of_bounds() {
155        let out = insert_after_nth("AA", "AA", 5, "!");
156        assert_eq!(out, "AA");
157    }
158
159    #[test]
160    fn insert_many_basic() {
161        let out = insert_many_after_nth("<a/><b/><a/><b/><a/>", "<b/>", &["!".into(), "?".into()]);
162        assert_eq!(out, "<a/><b/>!<a/><b/>?<a/>");
163    }
164
165    #[test]
166    fn insert_many_empty_contents_returns_original() {
167        let xml = "<a/><b/><a/>";
168        let out = insert_many_after_nth(xml, "<b/>", &[]);
169        assert_eq!(out, xml);
170    }
171
172    #[test]
173    fn insert_many_more_contents_than_matches() {
174        // 出现次数不足:多余的 contents 被忽略
175        let out = insert_many_after_nth("<a/><b/>", "<b/>", &["!".into(), "?".into()]);
176        assert_eq!(out, "<a/><b/>!");
177    }
178
179    #[test]
180    fn insert_many_no_match_returns_original() {
181        let xml = "<a/><a/>";
182        let out = insert_many_after_nth(xml, "<b/>", &["!".into()]);
183        assert_eq!(out, xml);
184    }
185
186    #[test]
187    fn insert_many_preserves_remaining_tail() {
188        // 插入点之后的内容应完整保留
189        let out = insert_many_after_nth("<a/><b/>TAIL", "<b/>", &["!".into()]);
190        assert_eq!(out, "<a/><b/>!TAIL");
191    }
192}