Skip to main content

extend_docx/parse/
paragraph.rs

1use crate::model::{
2    FormFieldCheckboxSizeMode, FormFieldCheckboxWidgetSettings, FormFieldDropdownWidgetSettings,
3    FormFieldOption, FormFieldRunNode, FormFieldRunNodeType, FormFieldSourceKind,
4    FormFieldSourcePreview,
5    FormFieldTextWidgetSettings, FormFieldType, FormFieldWidgetSettings, NoteReference,
6    NoteReferenceKind, ParagraphChildNode, ParagraphNode, ParagraphNodeType,
7    ParagraphSourceTextPatch, ParagraphSourceTextPatchRun, SharedTextStyle, TextRunNode,
8    TextRunNodeType, TextStyle,
9};
10use crate::parse::context::ParseContext;
11use crate::parse::images::{parse_run_image_block, parse_run_image_tokens};
12use crate::parse::re;
13use crate::parse::style::parse_run_style_in_table;
14use crate::parse::util::{
15    active_xml as prefer_alternate_content_choice, decode_hex_code_point, decode_xml_attribute,
16    is_windows_metafile_content_type,
17    normalize_legacy_form_display_value, on_off_value_to_boolean, parse_on_off_tag_value,
18    parse_relationships_from_parts, strip_text_box_content, svg_data_uri,
19};
20use crate::xml::{
21    decode_xml_entities, extract_balanced_tag_blocks, extract_balanced_tag_ranges,
22    extract_direct_child_tag_ranges, get_attribute, get_attribute_ref, parse_integer_attribute,
23    parse_on_off_attribute,
24};
25
26#[derive(Clone, Debug)]
27pub struct ParagraphRunToken {
28    pub xml: String,
29    pub start: usize,
30    pub end: usize,
31    pub link: Option<String>,
32    pub text_links: Vec<(usize, Option<String>)>,
33}
34
35#[derive(Clone, Debug)]
36struct RunToken<'a> {
37    xml: std::borrow::Cow<'a, str>,
38    start: usize,
39    end: usize,
40    link: Option<String>,
41    text_links: Vec<(usize, Option<String>)>,
42}
43
44#[derive(Clone, Debug)]
45pub struct ParsedRunTextToken {
46    pub text: String,
47    pub source_start: usize,
48    pub note_reference: Option<NoteReference>,
49}
50
51struct IndexedPlainRun<'a> {
52    properties: &'a str,
53    scripts: u8,
54    tokens: Vec<ParsedRunTextToken>,
55}
56
57#[derive(Default)]
58struct PlainRunBuilder<'a> {
59    properties: Option<&'a str>,
60    properties_end: usize,
61    seen_child: bool,
62    scripts: u8,
63    tokens: Vec<ParsedRunTextToken>,
64}
65
66fn plain_property_leaf(name: &str) -> bool {
67    matches!(
68        name,
69        "w:rStyle"
70            | "w:rFonts"
71            | "w:b"
72            | "w:bCs"
73            | "w:i"
74            | "w:iCs"
75            | "w:caps"
76            | "w:smallCaps"
77            | "w:strike"
78            | "w:dstrike"
79            | "w:outline"
80            | "w:shadow"
81            | "w:emboss"
82            | "w:imprint"
83            | "w:noProof"
84            | "w:snapToGrid"
85            | "w:vanish"
86            | "w:webHidden"
87            | "w:color"
88            | "w:spacing"
89            | "w:w"
90            | "w:kern"
91            | "w:position"
92            | "w:sz"
93            | "w:szCs"
94            | "w:highlight"
95            | "w:u"
96            | "w:effect"
97            | "w:bdr"
98            | "w:shd"
99            | "w:fitText"
100            | "w:vertAlign"
101            | "w:rtl"
102            | "w:cs"
103            | "w:em"
104            | "w:lang"
105            | "w:eastAsianLayout"
106            | "w:specVanish"
107            | "w:oMath"
108            | "w14:ligatures"
109            | "w14:contextualAlternates"
110    )
111}
112
113impl<'a> PlainRunBuilder<'a> {
114    fn visit(&mut self, name: &str, xml: &'a str, start: usize, content: Option<&str>) -> bool {
115        if start == 0 {
116            return name == "w:r";
117        }
118        self.seen_child = true;
119        if start < self.properties_end {
120            return content.is_none() && plain_property_leaf(name);
121        }
122        let text = match name {
123            "w:rPr" => {
124                if self.properties.replace(xml).is_some() {
125                    return false;
126                }
127                self.properties_end = start + xml.len();
128                return true;
129            }
130            "w:t" => {
131                // Paired text preserves the legacy script scanner's boundaries.
132                let Some(text) = content else {
133                    return false;
134                };
135                if memchr::memchr(b'<', text.as_bytes()).is_some() {
136                    return false;
137                }
138                let text = if memchr::memchr2(b'\r', b'\n', text.as_bytes()).is_some() {
139                    decode_xml_entities(&text.replace("\r\n", " ").replace(['\r', '\n'], " "))
140                } else {
141                    decode_xml_entities(text)
142                };
143                if !text.is_ascii() {
144                    for ch in text.chars() {
145                        match ch as u32 {
146                            0x2e80..=0x9fff | 0xac00..=0xd7af => self.scripts |= 1,
147                            0x0590..=0x08ff | 0xfb1d..=0xfefc => self.scripts |= 2,
148                            _ => {}
149                        }
150                    }
151                }
152                text
153            }
154            "w:tab" if content.is_none() => "\t".into(),
155            "w:br" | "w:cr" if content.is_none() => "\n".into(),
156            "w:noBreakHyphen" if content.is_none() => "\u{2011}".into(),
157            "w:softHyphen" if content.is_none() => "\u{00ad}".into(),
158            "w:instrText" => return content.is_none_or(|text| !text.contains('<')),
159            "w:lastRenderedPageBreak" if content.is_none() => return true,
160            _ => return false,
161        };
162        self.tokens.push(ParsedRunTextToken {
163            text,
164            source_start: start,
165            note_reference: None,
166        });
167        true
168    }
169
170    fn finish(self) -> Option<IndexedPlainRun<'a>> {
171        self.seen_child.then_some(IndexedPlainRun {
172            properties: self.properties.unwrap_or(""),
173            scripts: self.scripts,
174            tokens: self.tokens,
175        })
176    }
177}
178
179fn contains_xml_control(xml: &str) -> bool {
180    memchr::memchr2_iter(b'?', b'!', xml.as_bytes())
181        .any(|position| position != 0 && xml.as_bytes()[position - 1] == b'<')
182}
183
184fn indexed_plain_run(xml: &str) -> Option<IndexedPlainRun<'_>> {
185    if contains_xml_control(xml) {
186        return None;
187    }
188    let mut run = PlainRunBuilder::default();
189    let complete = crate::xml_index::for_each_element(xml, |element| {
190        run.visit(
191            element.name,
192            element.xml,
193            element.source_start,
194            element.content(),
195        )
196    })?;
197    complete.then(|| run.finish()).flatten()
198}
199
200struct IndexedPlainParagraphRun<'a> {
201    xml: &'a str,
202    run: IndexedPlainRun<'a>,
203}
204
205fn indexed_plain_paragraph(xml: &str) -> Option<Vec<IndexedPlainParagraphRun<'_>>> {
206    if contains_xml_control(xml) {
207        return None;
208    }
209    let mut runs = Vec::new();
210    let mut active: Option<(usize, usize, &str, PlainRunBuilder<'_>)> = None;
211    let mut properties_end = 0;
212    let mut seen_properties = false;
213    let complete = crate::xml_index::for_each_element(xml, |element| {
214        if let Some((start, end, _, run)) = active.as_mut() {
215            if element.source_start < *end {
216                return run.visit(
217                    element.name,
218                    element.xml,
219                    element.source_start - *start,
220                    element.content(),
221                );
222            }
223            let (_, _, run_xml, run) = active.take().unwrap();
224            let Some(run) = run.finish() else {
225                return false;
226            };
227            runs.push(IndexedPlainParagraphRun { xml: run_xml, run });
228        }
229        if element.source_start == 0 {
230            return element.name == "w:p";
231        }
232        if element.source_start < properties_end {
233            return matches!(element.name, "w:rPr" | "w:numPr" | "w:pBdr" | "w:tabs")
234                || (element.content().is_none()
235                    && (plain_property_leaf(element.name)
236                        || matches!(
237                            element.name,
238                            "w:pStyle"
239                                | "w:keepNext"
240                                | "w:keepLines"
241                                | "w:pageBreakBefore"
242                                | "w:framePr"
243                                | "w:widowControl"
244                                | "w:ilvl"
245                                | "w:numId"
246                                | "w:suppressLineNumbers"
247                                | "w:top"
248                                | "w:left"
249                                | "w:bottom"
250                                | "w:right"
251                                | "w:between"
252                                | "w:bar"
253                                | "w:tab"
254                                | "w:suppressAutoHyphens"
255                                | "w:kinsoku"
256                                | "w:wordWrap"
257                                | "w:overflowPunct"
258                                | "w:topLinePunct"
259                                | "w:autoSpaceDE"
260                                | "w:autoSpaceDN"
261                                | "w:bidi"
262                                | "w:adjustRightInd"
263                                | "w:ind"
264                                | "w:contextualSpacing"
265                                | "w:mirrorIndents"
266                                | "w:suppressOverlap"
267                                | "w:jc"
268                                | "w:textDirection"
269                                | "w:textAlignment"
270                                | "w:textboxTightWrap"
271                                | "w:outlineLvl"
272                                | "w:divId"
273                                | "w:cnfStyle"
274                                | "w:del"
275                                | "w:ins"
276                        )));
277        }
278        match element.name {
279            "w:pPr" if !seen_properties => {
280                seen_properties = true;
281                properties_end = element.source_start + element.xml.len();
282                true
283            }
284            "w:r" => {
285                let mut run = PlainRunBuilder::default();
286                if !run.visit(element.name, element.xml, 0, element.content()) {
287                    return false;
288                }
289                active = Some((
290                    element.source_start,
291                    element.source_start + element.xml.len(),
292                    element.xml,
293                    run,
294                ));
295                true
296            }
297            _ => false,
298        }
299    })?;
300    if !complete {
301        return None;
302    }
303    if let Some((_, _, run_xml, run)) = active {
304        runs.push(IndexedPlainParagraphRun {
305            xml: run_xml,
306            run: run.finish()?,
307        });
308    }
309    Some(runs)
310}
311
312fn run_text_xml(run_xml: &str) -> std::borrow::Cow<'_, str> {
313    let mut active = prefer_alternate_content_choice(run_xml);
314    for range in extract_balanced_tag_ranges(&active, "w:txbxContent")
315        .into_iter()
316        .rev()
317    {
318        active
319            .to_mut()
320            .replace_range(range.start..range.end, &" ".repeat(range.end - range.start));
321    }
322    active
323}
324
325pub fn parse_run_text_tokens(run_xml: &str) -> Vec<ParsedRunTextToken> {
326    let normalized_run_xml = prefer_alternate_content_choice(run_xml);
327    parse_active_run_text_tokens(&normalized_run_xml)
328}
329
330fn parse_active_run_text_tokens(normalized_run_xml: &str) -> Vec<ParsedRunTextToken> {
331    if let Some(tokens) = indexed_run_text_tokens(normalized_run_xml) {
332        return tokens;
333    }
334    if let Some(tokens) = stream_run_text_tokens(normalized_run_xml) {
335        return tokens;
336    }
337    let token_pattern = re::get_unchecked(
338        r#"(?is)<w:t\b(?:[^>]*[^/>])?>([\s\S]*?)</w:t>|<a:t\b(?:[^>]*[^/>])?>([\s\S]*?)</a:t>|<w:footnoteReference\b[^>]*w:id="(-?\d+)"[^>]*/>|<w:endnoteReference\b[^>]*w:id="(-?\d+)"[^>]*/>|<w:tab\b[^>]*/>|<a:tab\b[^>]*/>|<w:br\b[^>]*/>|<w:cr\b[^>]*/>|<a:br\b[^>]*/>|<w:noBreakHyphen\b[^>]*/>|<w:softHyphen\b[^>]*/>|<w:sym\b[^>]*/>|</w:p>"#,
339    );
340    let mut parts = Vec::new();
341    for caps in token_pattern.captures_iter(normalized_run_xml) {
342        let source_start = caps.get(0).unwrap().start();
343        if let Some(m) = caps.get(1) {
344            // Literal XML line endings are text whitespace; explicit break
345            // elements carry the document's line-break semantics.
346            let text = if m.as_str().contains(['\r', '\n']) {
347                decode_xml_entities(&m.as_str().replace("\r\n", " ").replace(['\r', '\n'], " "))
348            } else {
349                decode_xml_entities(m.as_str())
350            };
351            parts.push(ParsedRunTextToken {
352                source_start,
353                text,
354                note_reference: None,
355            });
356            continue;
357        }
358        if let Some(m) = caps.get(2) {
359            parts.push(ParsedRunTextToken {
360                source_start,
361                text: decode_xml_entities(m.as_str()),
362                note_reference: None,
363            });
364            continue;
365        }
366        if let Some(m) = caps.get(3) {
367            let reference_id: i64 = m.as_str().parse().unwrap_or(0);
368            if reference_id >= 0 {
369                parts.push(ParsedRunTextToken {
370                    source_start,
371                    text: String::new(),
372                    note_reference: Some(NoteReference {
373                        kind: NoteReferenceKind::Footnote,
374                        id: reference_id,
375                    }),
376                });
377            }
378            continue;
379        }
380        if let Some(m) = caps.get(4) {
381            let reference_id: i64 = m.as_str().parse().unwrap_or(0);
382            if reference_id >= 0 {
383                parts.push(ParsedRunTextToken {
384                    source_start,
385                    text: String::new(),
386                    note_reference: Some(NoteReference {
387                        kind: NoteReferenceKind::Endnote,
388                        id: reference_id,
389                    }),
390                });
391            }
392            continue;
393        }
394        let marker = caps
395            .get(0)
396            .map(|m| m.as_str().to_ascii_lowercase())
397            .unwrap_or_default();
398        if marker.starts_with("<w:nobreakhyphen")
399            || marker.starts_with("<w:softhyphen")
400            || marker.starts_with("<w:sym")
401        {
402            let raw = caps.get(0).unwrap().as_str();
403            let text = if marker.starts_with("<w:nobreakhyphen") {
404                "\u{2011}".to_string()
405            } else if marker.starts_with("<w:softhyphen") {
406                "\u{00ad}".to_string()
407            } else {
408                let code =
409                    get_attribute(raw, "w:char").and_then(|v| u32::from_str_radix(&v, 16).ok());
410                let font = get_attribute(raw, "w:font").unwrap_or_default();
411                code.and_then(|code| {
412                    super::symbols::symbol_character(&font, code).or_else(|| char::from_u32(code))
413                })
414                .unwrap_or('\u{fffd}')
415                .to_string()
416            };
417            parts.push(ParsedRunTextToken {
418                text,
419                source_start,
420                note_reference: None,
421            });
422        } else if marker == "</w:p>" {
423            parts.push(ParsedRunTextToken {
424                source_start,
425                text: "\n".to_string(),
426                note_reference: None,
427            });
428        } else if marker.starts_with("<w:tab") || marker.starts_with("<a:tab") {
429            parts.push(ParsedRunTextToken {
430                source_start,
431                text: "\t".to_string(),
432                note_reference: None,
433            });
434        } else {
435            parts.push(ParsedRunTextToken {
436                source_start,
437                text: "\n".to_string(),
438                note_reference: None,
439            });
440        }
441    }
442    parts
443}
444
445fn indexed_run_text_tokens(xml: &str) -> Option<Vec<ParsedRunTextToken>> {
446    let bytes = xml.as_bytes();
447    if memchr::memchr2_iter(b'?', b'!', bytes)
448        .any(|position| position != 0 && bytes[position - 1] == b'<')
449    {
450        return None;
451    }
452    let mut tokens = Vec::new();
453    let complete = crate::xml_index::for_each_element(xml, |element| {
454        let text = match element.name {
455            "w:r" | "w:rPr" => return true,
456            "w:t" | "a:t" => {
457                let Some(text) = element.content() else {
458                    return true;
459                };
460                if memchr::memchr(b'<', text.as_bytes()).is_some() {
461                    return false;
462                }
463                if element.name == "w:t" && memchr::memchr2(b'\r', b'\n', text.as_bytes()).is_some()
464                {
465                    decode_xml_entities(&text.replace("\r\n", " ").replace(['\r', '\n'], " "))
466                } else {
467                    decode_xml_entities(text)
468                }
469            }
470            "w:tab" | "a:tab" if element.content().is_none() => "\t".into(),
471            "w:br" | "w:cr" | "a:br" if element.content().is_none() => "\n".into(),
472            "w:noBreakHyphen" if element.content().is_none() => "\u{2011}".into(),
473            "w:softHyphen" if element.content().is_none() => "\u{00ad}".into(),
474            name => {
475                return ![
476                    "w:t",
477                    "a:t",
478                    "w:p",
479                    "w:footnoteReference",
480                    "w:endnoteReference",
481                    "w:tab",
482                    "a:tab",
483                    "w:br",
484                    "w:cr",
485                    "a:br",
486                    "w:noBreakHyphen",
487                    "w:softHyphen",
488                    "w:sym",
489                ]
490                .iter()
491                .any(|expected| name.eq_ignore_ascii_case(expected));
492            }
493        };
494        tokens.push(ParsedRunTextToken {
495            text,
496            source_start: element.source_start,
497            note_reference: None,
498        });
499        true
500    })?;
501    complete.then_some(tokens)
502}
503
504fn stream_run_text_tokens(xml: &str) -> Option<Vec<ParsedRunTextToken>> {
505    use quick_xml::events::Event;
506    if xml.contains("<!") {
507        return None;
508    }
509    let mut reader = quick_xml::Reader::from_str(xml);
510    let mut tokens = Vec::new();
511    loop {
512        let source_start = reader.buffer_position() as usize;
513        let event = reader.read_event().ok()?;
514        if let Event::Start(tag) | Event::Empty(tag) = &event {
515            if tag.as_ref().contains('<') {
516                return None;
517            }
518        }
519        match event {
520            Event::Start(tag) if matches!(tag.name().as_ref(), "w:t" | "a:t") => {
521                let word_text = tag.name().as_ref() == "w:t";
522                let text = reader.read_text(tag.name()).ok()?;
523                let text = text.as_ref();
524                if text.contains('<') {
525                    return None;
526                }
527                let text = if word_text && text.contains(['\r', '\n']) {
528                    decode_xml_entities(&text.replace("\r\n", " ").replace(['\r', '\n'], " "))
529                } else {
530                    decode_xml_entities(text)
531                };
532                tokens.push(ParsedRunTextToken {
533                    text,
534                    source_start,
535                    note_reference: None,
536                });
537            }
538            Event::Empty(tag) => {
539                let name = tag.name();
540                let name = name.as_ref();
541                let text = match name {
542                    "w:footnoteReference" | "w:endnoteReference" => {
543                        let raw = tag.as_ref();
544                        let mut ids = 0;
545                        for attribute in tag.attributes() {
546                            let attribute = attribute.ok()?;
547                            ids += usize::from(attribute.key.as_ref().eq_ignore_ascii_case("w:id"));
548                        }
549                        if ids != 1 {
550                            return None;
551                        }
552                        let Some(id) = raw
553                            .split_once("w:id=\"")
554                            .and_then(|(_, value)| value.split_once('"').map(|v| v.0))
555                        else {
556                            return None;
557                        };
558                        let digits = id.strip_prefix('-').unwrap_or(id);
559                        if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
560                            continue;
561                        }
562                        let id = id.parse::<i64>().unwrap_or(0);
563                        if id >= 0 {
564                            tokens.push(ParsedRunTextToken {
565                                text: String::new(),
566                                source_start,
567                                note_reference: Some(NoteReference {
568                                    kind: if name == "w:footnoteReference" {
569                                        NoteReferenceKind::Footnote
570                                    } else {
571                                        NoteReferenceKind::Endnote
572                                    },
573                                    id,
574                                }),
575                            });
576                        }
577                        continue;
578                    }
579                    "w:tab" | "a:tab" => "\t".to_owned(),
580                    "w:br" | "w:cr" | "a:br" => "\n".to_owned(),
581                    "w:noBreakHyphen" => "\u{2011}".to_owned(),
582                    "w:softHyphen" => "\u{00ad}".to_owned(),
583                    "w:sym" => {
584                        let code = get_attribute(tag.as_ref(), "w:char")
585                            .and_then(|v| u32::from_str_radix(&v, 16).ok());
586                        let font = get_attribute(tag.as_ref(), "w:font").unwrap_or_default();
587                        code.and_then(|code| {
588                            super::symbols::symbol_character(&font, code)
589                                .or_else(|| char::from_u32(code))
590                        })
591                        .unwrap_or('\u{fffd}')
592                        .to_string()
593                    }
594                    _ => {
595                        if [
596                            "w:footnoteReference",
597                            "w:endnoteReference",
598                            "w:tab",
599                            "a:tab",
600                            "w:br",
601                            "w:cr",
602                            "a:br",
603                            "w:noBreakHyphen",
604                            "w:softHyphen",
605                            "w:sym",
606                        ]
607                        .iter()
608                        .any(|expected| name.eq_ignore_ascii_case(expected))
609                        {
610                            return None;
611                        }
612                        continue;
613                    }
614                };
615                tokens.push(ParsedRunTextToken {
616                    text,
617                    source_start,
618                    note_reference: None,
619                });
620            }
621            Event::End(tag) if tag.name().as_ref().eq_ignore_ascii_case("w:p") => {
622                if !xml[source_start..reader.buffer_position() as usize]
623                    .eq_ignore_ascii_case("</w:p>")
624                {
625                    return None;
626                }
627                tokens.push(ParsedRunTextToken {
628                    text: "\n".into(),
629                    source_start,
630                    note_reference: None,
631                });
632            }
633            Event::Start(tag)
634                if ["w:t", "a:t"]
635                    .iter()
636                    .any(|name| tag.name().as_ref().eq_ignore_ascii_case(name)) =>
637            {
638                return None
639            }
640            Event::Eof => return Some(tokens),
641            _ => {}
642        }
643    }
644}
645
646pub fn parse_run_text(run_xml: &str) -> String {
647    parse_run_text_tokens(run_xml)
648        .into_iter()
649        .map(|token| token.text)
650        .collect()
651}
652
653fn hyperlink_href_from_tag(
654    hyperlink_tag: &str,
655    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
656) -> Option<String> {
657    let relationship_id = get_attribute(hyperlink_tag, "r:id");
658    let anchor = get_attribute_ref(hyperlink_tag, "w:anchor").map(decode_xml_entities);
659    let relationship_target = relationship_id
660        .as_deref()
661        .and_then(|id| context.relationships.get(id).cloned());
662    if relationship_id.is_some() && relationship_target.is_none() {
663        context.push_warning(format!(
664            "Missing hyperlink relationship target for {}",
665            relationship_id.unwrap_or_default()
666        ));
667    }
668    if let (Some(anchor), Some(target)) = (anchor.as_deref(), relationship_target.as_deref()) {
669        return Some(format!(
670            "{}#{anchor}",
671            target.split('#').next().unwrap_or(target)
672        ));
673    }
674    if let Some(target) = relationship_target {
675        return Some(target);
676    }
677    anchor.map(|a| format!("#{a}"))
678}
679
680fn hyperlink_href_from_field_instruction(raw_instruction: &str) -> Option<String> {
681    if raw_instruction.is_empty() {
682        return None;
683    }
684    let instruction = decode_xml_entities(raw_instruction);
685    if re::get_unchecked(r"(?i)^\s*(?:REF|PAGEREF|NOTEREF)\b").is_match(&instruction) {
686        if !re::get_unchecked(r"(?i)\s\\h(?:\s|$)").is_match(&instruction) {
687            return None;
688        }
689        return re::get_unchecked(r#"(?i)^\s*(?:REF|PAGEREF|NOTEREF)\s+(?:"([^"]+)"|([^\s\\]+))"#)
690            .captures(&instruction)
691            .and_then(|captures| captures.get(1).or_else(|| captures.get(2)))
692            .map(|anchor| format!("#{}", anchor.as_str()));
693    }
694    if !re::get_unchecked(r"(?i)^\s*HYPERLINK\b").is_match(&instruction) {
695        return None;
696    }
697    let anchor_match = re::get(r#"(?i)\\l\s+(?:"([^"]+)"|([^\s\\]+))"#)
698        .and_then(|re| re.captures(&instruction))
699        .and_then(|c| {
700            c.get(1)
701                .or_else(|| c.get(2))
702                .map(|m| m.as_str().to_string())
703        });
704    let explicit_target_match = re::get(r#"(?i)^\s*HYPERLINK\b\s+(?:"([^"]+)"|([^\s\\]+))"#)
705        .and_then(|re| re.captures(&instruction))
706        .and_then(|c| {
707            c.get(1)
708                .or_else(|| c.get(2))
709                .map(|m| m.as_str().to_string())
710        });
711    if let Some(target) = explicit_target_match {
712        if let Some(ref anchor) = anchor_match {
713            return Some(format!(
714                "{}#{anchor}",
715                target.split('#').next().unwrap_or(&target)
716            ));
717        }
718        return Some(target);
719    }
720    anchor_match.map(|a| format!("#{a}"))
721}
722
723pub fn parse_paragraph_runs(
724    paragraph_xml: &str,
725    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
726) -> Vec<ParagraphRunToken> {
727    paragraph_runs(paragraph_xml, context)
728        .into_iter()
729        .map(|run| ParagraphRunToken {
730            xml: run.xml.into_owned(),
731            start: run.start,
732            end: run.end,
733            link: run.link,
734            text_links: run.text_links,
735        })
736        .collect()
737}
738
739fn paragraph_nested_tag_ranges(paragraph_xml: &str, tag_name: &str) -> Vec<crate::xml::TagRange> {
740    let mut ranges = extract_balanced_tag_ranges(paragraph_xml, tag_name);
741    let mut cursor = 0usize;
742    while cursor < ranges.len() {
743        let range = &ranges[cursor];
744        let content_start =
745            crate::xml::tag_token_end(paragraph_xml, range.start).unwrap_or(range.end);
746        if content_start < range.end {
747            let content = &paragraph_xml[content_start..range.end];
748            if super::scan::contains_tag(content, tag_name) {
749                let nested = extract_balanced_tag_ranges(content, tag_name);
750                ranges.extend(nested.into_iter().map(|range| crate::xml::TagRange {
751                    start: content_start + range.start,
752                    end: content_start + range.end,
753                }));
754            }
755        }
756        cursor += 1;
757    }
758    ranges.sort_unstable_by_key(|range| range.start);
759    ranges
760}
761
762fn paragraph_runs<'a>(
763    paragraph_xml: &'a str,
764    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
765) -> Vec<RunToken<'a>> {
766    let mut run_ranges = extract_balanced_tag_ranges(paragraph_xml, "w:r");
767    if super::scan::contains_tag(paragraph_xml, "w:del")
768        || super::scan::contains_tag(paragraph_xml, "w:moveFrom")
769    {
770        let mut deleted = extract_balanced_tag_ranges(paragraph_xml, "w:del");
771        deleted.extend(extract_balanced_tag_ranges(paragraph_xml, "w:moveFrom"));
772        run_ranges.retain(|run| {
773            !deleted.iter().any(|range| run.start >= range.start && run.end <= range.end)
774        });
775    }
776    if run_ranges.is_empty() {
777        return Vec::new();
778    }
779
780    let has_complex_fields = super::scan::contains_tag(paragraph_xml, "w:fldChar");
781    if !has_complex_fields
782        && !super::scan::contains_tag(paragraph_xml, "w:hyperlink")
783        && !super::scan::contains_tag(paragraph_xml, "w:fldSimple")
784    {
785        return run_ranges
786            .into_iter()
787            .map(|range| RunToken {
788                xml: std::borrow::Cow::Borrowed(&paragraph_xml[range.start..range.end]),
789                start: range.start,
790                end: range.end,
791                link: None,
792                text_links: Vec::new(),
793            })
794            .collect();
795    }
796
797    let mut hyperlink_ranges: Vec<_> = paragraph_nested_tag_ranges(paragraph_xml, "w:hyperlink")
798        .into_iter()
799        .map(|range| {
800            let hyperlink_xml = &paragraph_xml[range.start..range.end];
801            let hyperlink_tag = re::get(r"(?i)<w:hyperlink\b[^>]*>")
802                .and_then(|re| re.find(hyperlink_xml))
803                .map(|m| m.as_str().to_string())
804                .unwrap_or_default();
805            let href = hyperlink_href_from_tag(&hyperlink_tag, context);
806            (range, href)
807        })
808        .collect();
809    hyperlink_ranges.extend(
810        paragraph_nested_tag_ranges(paragraph_xml, "w:fldSimple")
811            .into_iter()
812            .filter_map(|range| {
813                let tag = super::scan::find_tag_token_ref(
814                    &paragraph_xml[range.start..range.end],
815                    "w:fldSimple",
816                )
817                .unwrap_or("");
818                let href = get_attribute(tag, "w:instr")
819                    .and_then(|instruction| hyperlink_href_from_field_instruction(&instruction))?;
820                Some((range, Some(href)))
821            }),
822    );
823    hyperlink_ranges.sort_by_key(|(range, _)| range.start);
824
825    let mut field_links_by_run: std::collections::HashMap<usize, String> =
826        std::collections::HashMap::new();
827    let mut field_text_links_by_run: std::collections::HashMap<
828        usize,
829        Vec<(usize, Option<String>)>,
830    > = std::collections::HashMap::new();
831    #[derive(Default)]
832    struct FieldState {
833        instruction: String,
834        link: Option<String>,
835        separated: bool,
836    }
837    let mut fields: Vec<FieldState> = Vec::new();
838    let field_tokens = re::get_unchecked(
839        r#"(?is)<w:instrText\b(?:[^>]*[^/>])?>([\s\S]*?)</w:instrText>|<w:fldChar\b[^>]*>|<w:t\b(?:[^>]*[^/>])?>[\s\S]*?</w:t>|<a:t\b(?:[^>]*[^/>])?>[\s\S]*?</a:t>|<w:(?:drawing|pict)\b[^>]*>|<w:(?:sym|tab|br|cr|noBreakHyphen|softHyphen|footnoteReference|endnoteReference)\b[^>]*/>|<a:(?:tab|br)\b[^>]*/>"#,
840    );
841
842    for (run_index, range) in run_ranges.iter().enumerate() {
843        if !has_complex_fields
844            || (fields.is_empty()
845                && !paragraph_xml[range.start..range.end].contains("<mc:AlternateContent")
846                && crate::xml::find_ignore_ascii_case(
847                    &paragraph_xml[range.start..range.end],
848                    0,
849                    "<w:fldChar",
850                )
851                .is_none())
852        {
853            field_text_links_by_run.insert(run_index, vec![(0, None)]);
854            continue;
855        }
856        let run_xml = run_text_xml(&paragraph_xml[range.start..range.end]);
857        let mut text_links = vec![(0, fields.iter().rev().find_map(|field| field.link.clone()))];
858        for token in field_tokens.captures_iter(&run_xml) {
859            if let Some(instruction) = token.get(1) {
860                if let Some(field) = fields.last_mut().filter(|field| !field.separated) {
861                    field.instruction.push_str(instruction.as_str());
862                }
863                continue;
864            }
865            let tag = token.get(0).unwrap().as_str();
866            if tag.to_ascii_lowercase().starts_with("<w:fldchar") {
867                match get_attribute(tag, "w:fldCharType").as_deref() {
868                    Some("begin") => fields.push(FieldState::default()),
869                    Some("separate") => {
870                        if let Some(field) = fields.last_mut() {
871                            field.link = hyperlink_href_from_field_instruction(&field.instruction);
872                            field.separated = true;
873                        }
874                    }
875                    Some("end") => {
876                        fields.pop();
877                    }
878                    _ => {}
879                }
880                text_links.push((
881                    token.get(0).unwrap().end(),
882                    fields.iter().rev().find_map(|field| field.link.clone()),
883                ));
884            } else {
885                let link = fields.iter().rev().find_map(|field| field.link.clone());
886                if let Some(link) = link.as_ref() {
887                    field_links_by_run
888                        .entry(run_index)
889                        .or_insert_with(|| link.clone());
890                }
891            }
892        }
893        field_text_links_by_run.insert(run_index, text_links);
894    }
895
896    let mut hyperlink_range_cursor = 0usize;
897    let mut active_hyperlinks: Vec<&(crate::xml::TagRange, Option<String>)> = Vec::new();
898    run_ranges
899        .into_iter()
900        .enumerate()
901        .map(|(run_index, range)| {
902            active_hyperlinks.retain(|(hyperlink, _)| hyperlink.end > range.start);
903            while hyperlink_range_cursor < hyperlink_ranges.len()
904                && hyperlink_ranges[hyperlink_range_cursor].0.start < range.end
905            {
906                let hyperlink = &hyperlink_ranges[hyperlink_range_cursor];
907                if hyperlink.0.end > range.start {
908                    active_hyperlinks.push(hyperlink);
909                }
910                hyperlink_range_cursor += 1;
911            }
912            let field_links = field_text_links_by_run
913                .remove(&run_index)
914                .unwrap_or_default();
915            let overlapping = &active_hyperlinks;
916            let hyperlink_href = overlapping
917                .iter()
918                .rev()
919                .find(|(hyperlink_range, _)| {
920                    range.start >= hyperlink_range.start && range.end <= hyperlink_range.end
921                })
922                .and_then(|(_, href)| href.clone());
923            let mut offsets: Vec<_> = field_links.iter().map(|(offset, _)| *offset).collect();
924            for (hyperlink, _) in overlapping {
925                offsets.push(hyperlink.start.saturating_sub(range.start));
926                if hyperlink.end < range.end {
927                    offsets.push(hyperlink.end - range.start);
928                }
929            }
930            offsets.sort_unstable();
931            offsets.dedup();
932            let text_links = offsets
933                .into_iter()
934                .map(|offset| {
935                    let position = range.start + offset;
936                    let href = overlapping
937                        .iter()
938                        .rev()
939                        .find(|(hyperlink, _)| {
940                            position >= hyperlink.start && position < hyperlink.end
941                        })
942                        .and_then(|(_, href)| href.clone())
943                        .or_else(|| {
944                            field_links
945                                .iter()
946                                .rev()
947                                .find(|(start, _)| *start <= offset)
948                                .and_then(|(_, href)| href.clone())
949                        });
950                    (offset, href)
951                })
952                .collect();
953            let link = hyperlink_href.or_else(|| field_links_by_run.get(&run_index).cloned());
954            RunToken {
955                xml: std::borrow::Cow::Borrowed(&paragraph_xml[range.start..range.end]),
956                start: range.start,
957                end: range.end,
958                link,
959                text_links,
960            }
961        })
962        .collect()
963}
964
965#[derive(Clone, Debug)]
966pub struct ParagraphFormFieldToken {
967    pub start: usize,
968    pub end: usize,
969    pub field: FormFieldRunNode,
970}
971
972fn decode_active_x_binary_markup(binary: &[u8]) -> String {
973    let utf16: String = binary
974        .chunks_exact(2)
975        .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
976        .filter_map(|code_unit| char::from_u32(code_unit as u32))
977        .collect();
978    if re::get(r"(?i)input\s+type=").is_some_and(|re| re.is_match(&utf16)) {
979        return utf16;
980    }
981    binary.iter().map(|&b| b as char).collect()
982}
983
984pub fn parse_run_active_x_checkbox_field(
985    run_xml: &str,
986    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
987    style: Option<TextStyle>,
988    link: Option<String>,
989) -> Option<FormFieldRunNode> {
990    parse_run_active_x_checkbox_field_shared(run_xml, context, style.map(Into::into), link)
991}
992
993fn parse_active_x_source_preview(
994    run_xml: &str,
995    control_start: usize,
996    control_tag: &str,
997    relationship_id: &str,
998    checked: bool,
999    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1000) -> Option<Box<FormFieldSourcePreview>> {
1001    let shape_id = decode_xml_entities(&get_attribute(control_tag, "w:shapeid")?);
1002    if shape_id.is_empty() {
1003        return None;
1004    }
1005    let object = extract_balanced_tag_ranges(run_xml, "w:object")
1006        .into_iter()
1007        .find(|range| range.start < control_start && control_start < range.end)?;
1008    let object_xml = &run_xml[object.start..object.end];
1009    let mut matches = extract_direct_child_tag_ranges(object_xml)
1010        .into_iter()
1011        .filter(|range| range.tag_name.eq_ignore_ascii_case("v:shape"))
1012        .filter(|range| {
1013            let shape_xml = &object_xml[range.start..range.end];
1014            let Some(tag_end) = crate::xml::tag_token_end(shape_xml, 1) else {
1015                return false;
1016            };
1017            get_attribute(&shape_xml[..tag_end], "id")
1018                .is_some_and(|id| decode_xml_entities(&id) == shape_id)
1019        });
1020    let matched = matches.next()?;
1021    if matches.next().is_some() {
1022        return None;
1023    }
1024    let shape_xml = &object_xml[matched.start..matched.end];
1025    let children = extract_direct_child_tag_ranges(shape_xml);
1026    let mut image_data = children
1027        .iter()
1028        .filter(|range| range.tag_name.eq_ignore_ascii_case("v:imagedata"));
1029    let data_range = image_data.next()?;
1030    if image_data.next().is_some()
1031        || children.iter().any(|range| {
1032            range.tag_name.eq_ignore_ascii_case("v:group")
1033                || range.tag_name.eq_ignore_ascii_case("v:shape")
1034                || range.tag_name.eq_ignore_ascii_case("v:textbox")
1035        })
1036    {
1037        return None;
1038    }
1039    let image_relationship =
1040        get_attribute(&shape_xml[data_range.start..data_range.end], "r:id")?;
1041    let part_name = context.relationships.get(&image_relationship)?;
1042    if context.binary_assets.get(part_name)?.is_empty() {
1043        return None;
1044    }
1045    let mut image = parse_run_image_block(shape_xml, context)?;
1046    if is_windows_metafile_content_type(image.content_type.as_deref(), image.part_name.as_deref())
1047        && !matches!(image.content_type.as_deref(), Some("image/svg+xml" | "image/png"))
1048    {
1049        let svg = crate::wmf::wmf_bitmap_preview_to_svg(image.data.as_deref()?)?;
1050        image.src = Some(svg_data_uri(&svg));
1051        image.content_type = Some("image/svg+xml".to_owned());
1052    }
1053    if image.part_name.as_deref() != Some(part_name.as_str())
1054        || image.data.as_ref().is_none_or(Vec::is_empty)
1055        || image.src.as_deref().is_none_or(str::is_empty)
1056        || image
1057            .content_type
1058            .as_deref()
1059            .is_none_or(|content_type| !content_type.starts_with("image/"))
1060    {
1061        return None;
1062    }
1063    Some(Box::new(FormFieldSourcePreview {
1064        relationship_id: relationship_id.to_owned(),
1065        shape_id,
1066        checked,
1067        image: Box::new(image),
1068    }))
1069}
1070
1071fn parse_run_active_x_checkbox_field_shared(
1072    run_xml: &str,
1073    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1074    style: Option<SharedTextStyle>,
1075    link: Option<String>,
1076) -> Option<FormFieldRunNode> {
1077    if !re::get(r"(?i)<w:object\b").is_some_and(|re| re.is_match(run_xml))
1078        || !re::get(r"(?i)<w:control\b").is_some_and(|re| re.is_match(run_xml))
1079    {
1080        return None;
1081    }
1082    let control = re::get(r"(?i)<w:control\b[^>]*/?>")
1083        .and_then(|re| re.find(run_xml))?;
1084    let control_tag = control.as_str();
1085    let control_relationship_id = get_attribute(&control_tag, "r:id")?;
1086    let active_x_part_name = context.relationships.get(&control_relationship_id)?.clone();
1087    let active_x_xml = context.parts.get(&active_x_part_name)?.content.clone();
1088    let ocx_tag = re::get(r"(?i)<ax:ocx\b[^>]*/?>")
1089        .and_then(|re| re.find(&active_x_xml))
1090        .map(|m| m.as_str().to_string())
1091        .unwrap_or_default();
1092    let binary_relationship_id = get_attribute(&ocx_tag, "r:id")?;
1093    let active_x_relationships = parse_relationships_from_parts(context.parts, &active_x_part_name);
1094    let active_x_binary_part_name = active_x_relationships.get(&binary_relationship_id)?.clone();
1095    let active_x_binary = context.binary_assets.get(&active_x_binary_part_name)?;
1096    if active_x_binary.is_empty() {
1097        return None;
1098    }
1099    let markup = decode_active_x_binary_markup(active_x_binary);
1100    if !re::get(r#"(?i)input\s+type\s*=\s*"checkbox""#).is_some_and(|re| re.is_match(&markup)) {
1101        return None;
1102    }
1103    let name = re::get(r#"(?i)\bname\s*=\s*"([^"]+)""#)
1104        .and_then(|re| re.captures(&markup))
1105        .and_then(|c| c.get(1))
1106        .map(|m| m.as_str().trim().to_string());
1107    let checked = re::get(r"(?i)\bchecked\b").is_some_and(|re| re.is_match(&markup));
1108    let source_preview = parse_active_x_source_preview(
1109        run_xml,
1110        control.start(),
1111        control_tag,
1112        &control_relationship_id,
1113        checked,
1114        context,
1115    );
1116    Some(FormFieldRunNode {
1117        r#type: FormFieldRunNodeType::FormField,
1118        field_type: FormFieldType::Checkbox,
1119        source_kind: Some(FormFieldSourceKind::Legacy),
1120        id: None,
1121        tag: None,
1122        title: None,
1123        placeholder: None,
1124        checked: Some(checked),
1125        value: None,
1126        options: None,
1127        widget: Some(FormFieldWidgetSettings {
1128            name,
1129            enabled: None,
1130            calc_on_exit: None,
1131            text: None,
1132            checkbox: None,
1133            dropdown: None,
1134        }),
1135        checked_symbol: Some("☒".to_string()),
1136        unchecked_symbol: Some("☐".to_string()),
1137        style,
1138        link,
1139        source_xml: Some(run_xml.to_string()),
1140        source_preview,
1141    })
1142}
1143
1144fn parse_legacy_form_field_from_range(
1145    paragraph_xml: &str,
1146    runs: &[RunToken<'_>],
1147    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1148    paragraph_style_id: Option<&str>,
1149    table_run_style: Option<&TextStyle>,
1150    start_run_index: usize,
1151    separate_run_index: Option<usize>,
1152    end_run_index: usize,
1153    raw_instruction: &str,
1154    ff_data_xml: Option<&str>,
1155) -> Option<FormFieldRunNode> {
1156    let instruction = decode_xml_entities(raw_instruction)
1157        .split_whitespace()
1158        .collect::<Vec<_>>()
1159        .join(" ");
1160    let normalized_instruction = instruction.to_ascii_uppercase();
1161    let field_type = if normalized_instruction.contains("FORMCHECKBOX") {
1162        FormFieldType::Checkbox
1163    } else if normalized_instruction.contains("FORMDROPDOWN") {
1164        FormFieldType::Dropdown
1165    } else if normalized_instruction.contains("FORMTEXT") {
1166        FormFieldType::Text
1167    } else {
1168        return None;
1169    };
1170
1171    let result_start_run_index = separate_run_index
1172        .map(|i| i + 1)
1173        .unwrap_or((end_run_index).min(start_run_index + 1));
1174    let result_runs = if result_start_run_index < end_run_index {
1175        &runs[result_start_run_index..end_run_index]
1176    } else {
1177        &[]
1178    };
1179    let fallback_result_text = normalize_legacy_form_display_value(Some(
1180        &result_runs
1181            .iter()
1182            .map(|r| parse_run_text(&r.xml))
1183            .collect::<String>(),
1184    ));
1185    let style_run_xml = result_runs
1186        .iter()
1187        .find(|run| {
1188            !parse_run_text(&run.xml).trim().is_empty()
1189                || re::get(r"(?i)<w:sym\b").is_some_and(|re| re.is_match(&run.xml))
1190        })
1191        .or_else(|| runs.get(start_run_index))
1192        .map(|r| r.xml.as_ref());
1193    let style = style_run_xml.and_then(|xml| {
1194        parse_run_style_in_table(xml, context, paragraph_style_id, table_run_style)
1195    });
1196    let link = runs
1197        .get(start_run_index..=end_run_index.min(runs.len().saturating_sub(1)))
1198        .and_then(|slice| slice.iter().find_map(|r| r.link.clone()));
1199    let field_xml = runs
1200        .get(start_run_index)
1201        .zip(runs.get(end_run_index))
1202        .map(|(start, end)| paragraph_xml[start.start..end.end].to_string());
1203
1204    let name_tag = ff_data_xml.and_then(|xml| {
1205        re::get(r"(?i)<w:name\b[^>]*/?>")
1206            .and_then(|re| re.find(xml))
1207            .map(|m| m.as_str().to_string())
1208    });
1209    let enabled = ff_data_xml.and_then(|xml| parse_on_off_attribute(xml, "enabled"));
1210    let calc_on_exit = ff_data_xml.and_then(|xml| parse_on_off_attribute(xml, "calcOnExit"));
1211    let mut widget_settings = FormFieldWidgetSettings {
1212        name: name_tag
1213            .as_deref()
1214            .and_then(|tag| get_attribute(tag, "w:val"))
1215            .and_then(|value| decode_xml_attribute(Some(&value))),
1216        enabled,
1217        calc_on_exit,
1218        text: None,
1219        checkbox: None,
1220        dropdown: None,
1221    };
1222
1223    match field_type {
1224        FormFieldType::Checkbox => {
1225            let checkbox_xml = ff_data_xml
1226                .and_then(|xml| {
1227                    extract_balanced_tag_blocks(xml, "w:checkBox")
1228                        .into_iter()
1229                        .next()
1230                })
1231                .or_else(|| {
1232                    ff_data_xml.and_then(|xml| {
1233                        re::get(r"(?i)<w:checkBox\b[^>]*/?>")
1234                            .and_then(|re| re.find(xml))
1235                            .map(|m| m.as_str().to_string())
1236                    })
1237                });
1238            let default_tag = checkbox_xml.as_deref().and_then(|xml| {
1239                re::get(r"(?i)<w:default\b[^>]*/?>")
1240                    .and_then(|re| re.find(xml))
1241                    .map(|m| m.as_str().to_string())
1242            });
1243            let checked_tag = checkbox_xml.as_deref().and_then(|xml| {
1244                re::get(r"(?i)<w:checked\b[^>]*/?>")
1245                    .and_then(|re| re.find(xml))
1246                    .map(|m| m.as_str().to_string())
1247            });
1248            let size_auto_tag = checkbox_xml.as_deref().and_then(|xml| {
1249                re::get(r"(?i)<w:sizeAuto\b[^>]*/?>")
1250                    .and_then(|re| re.find(xml))
1251                    .map(|m| m.as_str().to_string())
1252            });
1253            let size_tag = checkbox_xml.as_deref().and_then(|xml| {
1254                re::get(r"(?i)<w:size\b[^>]*/?>")
1255                    .and_then(|re| re.find(xml))
1256                    .map(|m| m.as_str().to_string())
1257            });
1258            let default_checked = parse_on_off_tag_value(default_tag.as_deref(), &["w:val"]);
1259            let checked =
1260                parse_on_off_tag_value(checked_tag.as_deref(), &["w:val"]).or(default_checked);
1261            let size_value = size_tag
1262                .as_deref()
1263                .and_then(|t| parse_integer_attribute(t, "w:val"));
1264            let size_pt = size_value
1265                .filter(|v| *v > 0)
1266                .map(|v| ((v as f64) / 2.0 * 100.0).round() / 100.0);
1267            let size_mode = if size_auto_tag.is_some() {
1268                Some(FormFieldCheckboxSizeMode::Auto)
1269            } else if size_pt.is_some() {
1270                Some(FormFieldCheckboxSizeMode::Exact)
1271            } else {
1272                None
1273            };
1274            if default_checked.is_some() || size_mode.is_some() || size_pt.is_some() {
1275                widget_settings.checkbox = Some(FormFieldCheckboxWidgetSettings {
1276                    default_checked,
1277                    size_mode,
1278                    size_pt,
1279                });
1280            }
1281            return Some(FormFieldRunNode {
1282                r#type: FormFieldRunNodeType::FormField,
1283                field_type: FormFieldType::Checkbox,
1284                source_kind: Some(FormFieldSourceKind::Legacy),
1285                id: None,
1286                tag: None,
1287                title: None,
1288                placeholder: None,
1289                checked: Some(checked.unwrap_or(false)),
1290                value: None,
1291                options: None,
1292                widget: Some(widget_settings),
1293                checked_symbol: Some("☒".to_string()),
1294                unchecked_symbol: Some("☐".to_string()),
1295                style,
1296                link,
1297                source_xml: field_xml,
1298                source_preview: None,
1299            });
1300        }
1301        FormFieldType::Dropdown => {
1302            let dropdown_xml = ff_data_xml
1303                .and_then(|xml| {
1304                    extract_balanced_tag_blocks(xml, "w:ddList")
1305                        .into_iter()
1306                        .next()
1307                })
1308                .or_else(|| {
1309                    ff_data_xml.and_then(|xml| {
1310                        re::get(r"(?i)<w:ddList\b[^>]*/?>")
1311                            .and_then(|re| re.find(xml))
1312                            .map(|m| m.as_str().to_string())
1313                    })
1314                });
1315            let options: Vec<FormFieldOption> = dropdown_xml
1316                .as_deref()
1317                .map(|xml| {
1318                    re::get(r"(?i)<w:listEntry\b[^>]*/?>")
1319                        .map(|re| {
1320                            re.find_iter(xml)
1321                                .filter_map(|m| {
1322                                    let entry = get_attribute(m.as_str(), "w:val")
1323                                        .and_then(|value| decode_xml_attribute(Some(&value)))?
1324                                        .trim()
1325                                        .to_string();
1326                                    if entry.is_empty() {
1327                                        None
1328                                    } else {
1329                                        Some(FormFieldOption {
1330                                            display_text: entry.clone(),
1331                                            value: Some(entry),
1332                                        })
1333                                    }
1334                                })
1335                                .collect()
1336                        })
1337                        .unwrap_or_default()
1338                })
1339                .unwrap_or_default();
1340            let default_tag = dropdown_xml.as_deref().and_then(|xml| {
1341                re::get(r"(?i)<w:default\b[^>]*/?>")
1342                    .and_then(|re| re.find(xml))
1343                    .map(|m| m.as_str().to_string())
1344            });
1345            let default_value = default_tag
1346                .as_deref()
1347                .and_then(|tag| get_attribute(tag, "w:val"))
1348                .as_deref()
1349                .and_then(|value| decode_xml_attribute(Some(&value)))
1350                .map(|v| v.trim().to_string())
1351                .filter(|v| !v.is_empty());
1352            let dropdown_default_value = default_value.clone();
1353            if dropdown_default_value.is_some() {
1354                widget_settings.dropdown = Some(FormFieldDropdownWidgetSettings {
1355                    default_value: dropdown_default_value,
1356                });
1357            }
1358            let value = fallback_result_text
1359                .or(default_value)
1360                .or_else(|| options.first().map(|o| o.display_text.clone()));
1361            return Some(FormFieldRunNode {
1362                r#type: FormFieldRunNodeType::FormField,
1363                field_type: FormFieldType::Dropdown,
1364                source_kind: Some(FormFieldSourceKind::Legacy),
1365                id: None,
1366                tag: None,
1367                title: None,
1368                placeholder: None,
1369                checked: None,
1370                value,
1371                options: if options.is_empty() {
1372                    None
1373                } else {
1374                    Some(options)
1375                },
1376                widget: Some(widget_settings),
1377                checked_symbol: None,
1378                unchecked_symbol: None,
1379                style,
1380                link,
1381                source_xml: field_xml,
1382                source_preview: None,
1383            });
1384        }
1385        FormFieldType::Text => {
1386            let text_input_xml = ff_data_xml
1387                .and_then(|xml| {
1388                    extract_balanced_tag_blocks(xml, "w:textInput")
1389                        .into_iter()
1390                        .next()
1391                })
1392                .or_else(|| {
1393                    ff_data_xml.and_then(|xml| {
1394                        re::get(r"(?i)<w:textInput\b[^>]*/?>")
1395                            .and_then(|re| re.find(xml))
1396                            .map(|m| m.as_str().to_string())
1397                    })
1398                });
1399            let type_tag = text_input_xml.as_deref().and_then(|xml| {
1400                re::get(r"(?i)<w:type\b[^>]*/?>")
1401                    .and_then(|re| re.find(xml))
1402                    .map(|m| m.as_str().to_string())
1403            });
1404            let default_tag = text_input_xml.as_deref().and_then(|xml| {
1405                re::get(r"(?i)<w:default\b[^>]*/?>")
1406                    .and_then(|re| re.find(xml))
1407                    .map(|m| m.as_str().to_string())
1408            });
1409            let max_length_tag = text_input_xml.as_deref().and_then(|xml| {
1410                re::get(r"(?i)<w:maxLength\b[^>]*/?>")
1411                    .and_then(|re| re.find(xml))
1412                    .map(|m| m.as_str().to_string())
1413            });
1414            let format_tag = text_input_xml.as_deref().and_then(|xml| {
1415                re::get(r"(?i)<w:format\b[^>]*/?>")
1416                    .and_then(|re| re.find(xml))
1417                    .map(|m| m.as_str().to_string())
1418            });
1419            let input_type = type_tag
1420                .as_deref()
1421                .and_then(|tag| get_attribute(tag, "w:val"))
1422                .as_deref()
1423                .and_then(|value| decode_xml_attribute(Some(&value)));
1424            let default_text = default_tag
1425                .as_deref()
1426                .and_then(|tag| get_attribute(tag, "w:val"))
1427                .as_deref()
1428                .and_then(|value| decode_xml_attribute(Some(&value)));
1429            let max_length = max_length_tag
1430                .as_deref()
1431                .and_then(|tag| parse_integer_attribute(tag, "w:val"));
1432            let text_format = format_tag
1433                .as_deref()
1434                .and_then(|tag| get_attribute(tag, "w:val"))
1435                .as_deref()
1436                .and_then(|value| decode_xml_attribute(Some(&value)));
1437            let text_default = default_text.clone();
1438            if input_type.is_some()
1439                || text_default.is_some()
1440                || max_length.is_some()
1441                || text_format.is_some()
1442            {
1443                widget_settings.text = Some(FormFieldTextWidgetSettings {
1444                    input_type,
1445                    default_text: text_default,
1446                    max_length,
1447                    text_format,
1448                });
1449            }
1450            let value = fallback_result_text.or(default_text);
1451            return Some(FormFieldRunNode {
1452                r#type: FormFieldRunNodeType::FormField,
1453                field_type: FormFieldType::Text,
1454                source_kind: Some(FormFieldSourceKind::Legacy),
1455                id: None,
1456                tag: None,
1457                title: None,
1458                placeholder: None,
1459                checked: None,
1460                value,
1461                options: None,
1462                widget: Some(widget_settings),
1463                checked_symbol: None,
1464                unchecked_symbol: None,
1465                style,
1466                link,
1467                source_xml: field_xml,
1468                source_preview: None,
1469            });
1470        }
1471        FormFieldType::Date => None,
1472    }
1473}
1474
1475fn parse_legacy_paragraph_form_field_tokens(
1476    paragraph_xml: &str,
1477    runs: &[RunToken<'_>],
1478    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1479    paragraph_style_id: Option<&str>,
1480    table_run_style: Option<&TextStyle>,
1481) -> Vec<ParagraphFormFieldToken> {
1482    if runs.is_empty() {
1483        return Vec::new();
1484    }
1485    let mut tokens = Vec::new();
1486    let mut stack: Vec<(usize, Option<usize>, Vec<String>, Option<String>)> = Vec::new();
1487    let inline_token_pattern = re::get_unchecked(
1488        r#"(?is)<w:fldChar\b[^>]*\bw:fldCharType="(begin|separate|end)"[^>]*>([\s\S]*?)</w:fldChar>|<w:fldChar\b[^>]*\bw:fldCharType="(begin|separate|end)"[^>]*/>|<w:instrText\b(?:[^>]*[^/>])?>([\s\S]*?)</w:instrText>"#,
1489    );
1490
1491    for (run_index, run) in runs.iter().enumerate() {
1492        for caps in inline_token_pattern.captures_iter(&run.xml) {
1493            if let Some(chunk) = caps.get(4) {
1494                if let Some(current) = stack.last_mut() {
1495                    current.2.push(chunk.as_str().to_string());
1496                }
1497                continue;
1498            }
1499            let field_type = caps
1500                .get(1)
1501                .or_else(|| caps.get(3))
1502                .map(|m| m.as_str().to_ascii_lowercase())
1503                .unwrap_or_default();
1504            if field_type == "begin" {
1505                let field_xml = caps.get(0).map(|m| m.as_str()).unwrap_or_default();
1506                let ff_data_xml = extract_balanced_tag_blocks(field_xml, "w:ffData")
1507                    .into_iter()
1508                    .next()
1509                    .or_else(|| {
1510                        re::get(r"(?i)<w:ffData\b[^>]*/?>")
1511                            .and_then(|re| re.find(field_xml))
1512                            .map(|m| m.as_str().to_string())
1513                    });
1514                stack.push((run_index, None, Vec::new(), ff_data_xml));
1515                continue;
1516            }
1517            if field_type == "separate" {
1518                if let Some(current) = stack.last_mut() {
1519                    if current.1.is_none() {
1520                        current.1 = Some(run_index);
1521                    }
1522                }
1523                continue;
1524            }
1525            if field_type == "end" {
1526                let Some(current) = stack.pop() else { continue };
1527                let field = parse_legacy_form_field_from_range(
1528                    paragraph_xml,
1529                    runs,
1530                    context,
1531                    paragraph_style_id,
1532                    table_run_style,
1533                    current.0,
1534                    current.1,
1535                    run_index,
1536                    &current.2.join(" "),
1537                    current.3.as_deref(),
1538                );
1539                let Some(field) = field else { continue };
1540                let start = runs.get(current.0).map(|r| r.start);
1541                let end = runs.get(run_index).map(|r| r.end);
1542                if let (Some(start), Some(end)) = (start, end) {
1543                    if end > start {
1544                        tokens.push(ParagraphFormFieldToken { start, end, field });
1545                    }
1546                }
1547            }
1548        }
1549    }
1550    tokens
1551}
1552
1553fn trimmed_sdt_dropdown_style(
1554    runs: &[String],
1555    content_text: &str,
1556    fallback: Option<SharedTextStyle>,
1557    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1558    paragraph_style_id: Option<&str>,
1559    table_run_style: Option<&TextStyle>,
1560) -> Option<SharedTextStyle> {
1561    let Some(first_run) = runs.first() else {
1562        return fallback;
1563    };
1564    let first_text = parse_run_text(first_run);
1565    if !first_text.trim().is_empty() || content_text.trim().is_empty() {
1566        return fallback;
1567    }
1568
1569    let retained_start = content_text.len() - content_text.trim_start().len();
1570    let retained_end = content_text.trim_end().len();
1571    let mut cursor = 0;
1572    let mut retained_styles = Vec::new();
1573    for (index, run) in runs.iter().enumerate() {
1574        let text = if index == 0 {
1575            std::borrow::Cow::Borrowed(first_text.as_str())
1576        } else {
1577            std::borrow::Cow::Owned(parse_run_text(run))
1578        };
1579        if !content_text
1580            .get(cursor..)
1581            .is_some_and(|remaining| remaining.starts_with(text.as_ref()))
1582        {
1583            return fallback;
1584        }
1585        let end = cursor + text.len();
1586        if !text.is_empty() && cursor < retained_end && end > retained_start {
1587            retained_styles.push(parse_run_style_in_table(
1588                run,
1589                context,
1590                paragraph_style_id,
1591                table_run_style,
1592            ));
1593        }
1594        cursor = end;
1595    }
1596    if cursor != content_text.len() || retained_styles.is_empty() {
1597        return fallback;
1598    }
1599
1600    let Some(seed) = fallback
1601        .as_ref()
1602        .or_else(|| retained_styles.iter().flatten().next())
1603    else {
1604        return None;
1605    };
1606    let mut recovered = (**seed).clone();
1607    let mut changed = false;
1608    let mut has_any = false;
1609    macro_rules! recover_common_properties {
1610        ($($property:ident),+ $(,)?) => {
1611            $(
1612                let previous = fallback.as_ref().and_then(|style| style.$property.clone());
1613                let common = retained_styles[0].as_ref().and_then(|style| style.$property.as_ref());
1614                let value = if retained_styles.iter().skip(1).all(|style| {
1615                    style.as_ref().and_then(|style| style.$property.as_ref()) == common
1616                }) {
1617                    common.cloned()
1618                } else {
1619                    previous.clone()
1620                };
1621                changed |= value != previous;
1622                has_any |= value.is_some();
1623                recovered.$property = value;
1624            )+
1625        };
1626    }
1627    // A single field style can only adopt properties shared by all retained text.
1628    recover_common_properties!(
1629        bold,
1630        italic,
1631        underline,
1632        strike,
1633        all_caps,
1634        small_caps,
1635        color,
1636        highlight,
1637        background_color,
1638        font_size_pt,
1639        font_size_cs_pt,
1640        font_family,
1641        source_font_family,
1642        font_family_ascii,
1643        font_family_h_ansi,
1644        font_family_east_asia,
1645        font_family_cs,
1646        font_theme_ascii,
1647        font_theme_h_ansi,
1648        font_theme_east_asia,
1649        font_theme_cs,
1650        resolved_font_family_ascii,
1651        resolved_font_family_h_ansi,
1652        resolved_font_family_east_asia,
1653        resolved_font_family_cs,
1654        font_hint,
1655        language,
1656        language_east_asia,
1657        language_bidi,
1658        right_to_left,
1659        complex_script,
1660        character_spacing_twips,
1661        character_scale_percent,
1662        source_character_scale,
1663        kerning_min_font_size_pt,
1664        ligatures,
1665        contextual_alternates,
1666        vertical_align,
1667        run_border,
1668    );
1669    if !changed {
1670        fallback
1671    } else if has_any {
1672        Some(recovered.into())
1673    } else {
1674        None
1675    }
1676}
1677
1678fn parse_form_field_from_sdt_xml(
1679    sdt_xml: &str,
1680    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1681    paragraph_style_id: Option<&str>,
1682    table_run_style: Option<&TextStyle>,
1683    link: Option<String>,
1684) -> Option<FormFieldRunNode> {
1685    let sdt_properties_xml = extract_balanced_tag_blocks(sdt_xml, "w:sdtPr")
1686        .into_iter()
1687        .next()
1688        .or_else(|| {
1689            re::get(r"(?i)<w:sdtPr\b[^>]*/?>")
1690                .and_then(|re| re.find(sdt_xml))
1691                .map(|m| m.as_str().to_string())
1692        })
1693        .unwrap_or_default();
1694    let sdt_content_xml = extract_balanced_tag_blocks(sdt_xml, "w:sdtContent")
1695        .into_iter()
1696        .next()
1697        .unwrap_or_default();
1698    if sdt_properties_xml.is_empty() {
1699        return None;
1700    }
1701
1702    let alias_tag = re::get(r"(?i)<w:alias\b[^>]*/?>")
1703        .and_then(|re| re.find(&sdt_properties_xml))
1704        .map(|m| m.as_str().to_string());
1705    let tag_tag = re::get(r"(?i)<w:tag\b[^>]*/?>")
1706        .and_then(|re| re.find(&sdt_properties_xml))
1707        .map(|m| m.as_str().to_string());
1708    let id_tag = re::get(r"(?i)<w:id\b[^>]*/?>")
1709        .and_then(|re| re.find(&sdt_properties_xml))
1710        .map(|m| m.as_str().to_string());
1711    let placeholder_tag = re::get(r"(?is)<w:placeholder\b[\s\S]*?</w:placeholder>")
1712        .and_then(|re| re.find(&sdt_properties_xml))
1713        .map(|m| m.as_str().to_string());
1714    let placeholder_doc_part_tag = placeholder_tag.as_deref().and_then(|xml| {
1715        re::get(r"(?i)<w:docPart\b[^>]*/?>")
1716            .and_then(|re| re.find(xml))
1717            .map(|m| m.as_str().to_string())
1718    });
1719    let title = alias_tag
1720        .as_deref()
1721        .and_then(|tag| get_attribute(tag, "w:val"))
1722        .as_deref()
1723        .and_then(|value| decode_xml_attribute(Some(&value)));
1724    let tag = tag_tag
1725        .as_deref()
1726        .and_then(|t| get_attribute(t, "w:val"))
1727        .as_deref()
1728        .and_then(|value| decode_xml_attribute(Some(&value)));
1729    let id_value = id_tag
1730        .as_deref()
1731        .and_then(|tag| parse_integer_attribute(tag, "w:val"));
1732    let placeholder = placeholder_doc_part_tag
1733        .as_deref()
1734        .and_then(|tag| get_attribute(tag, "w:val"))
1735        .as_deref()
1736        .and_then(|value| decode_xml_attribute(Some(&value)));
1737    let content_runs = extract_balanced_tag_blocks(&sdt_content_xml, "w:r");
1738    let style = content_runs.first().and_then(|xml| {
1739        parse_run_style_in_table(xml, context, paragraph_style_id, table_run_style)
1740    });
1741    let content_text = parse_run_text(&sdt_content_xml);
1742    let trimmed_content_text = content_text.trim();
1743
1744    if re::get(r"(?i)<w14:checkbox\b").is_some_and(|re| re.is_match(&sdt_properties_xml)) {
1745        let checked_tag = re::get(r"(?i)<w14:checked\b[^>]*/?>")
1746            .and_then(|re| re.find(&sdt_properties_xml))
1747            .map(|m| m.as_str().to_string());
1748        let checked_value = checked_tag
1749            .as_deref()
1750            .and_then(|tag| get_attribute(tag, "w14:val").or_else(|| get_attribute(tag, "w:val")));
1751        let checked_state_tag = re::get(r"(?i)<w14:checkedState\b[^>]*/?>")
1752            .and_then(|re| re.find(&sdt_properties_xml))
1753            .map(|m| m.as_str().to_string());
1754        let unchecked_state_tag = re::get(r"(?i)<w14:uncheckedState\b[^>]*/?>")
1755            .and_then(|re| re.find(&sdt_properties_xml))
1756            .map(|m| m.as_str().to_string());
1757        let checked_symbol = checked_state_tag
1758            .as_deref()
1759            .and_then(|tag| {
1760                decode_hex_code_point(
1761                    get_attribute(tag, "w14:val")
1762                        .or_else(|| get_attribute(tag, "w:val"))
1763                        .as_deref(),
1764                )
1765            })
1766            .unwrap_or_else(|| "☒".to_string());
1767        let unchecked_symbol = unchecked_state_tag
1768            .as_deref()
1769            .and_then(|tag| {
1770                decode_hex_code_point(
1771                    get_attribute(tag, "w14:val")
1772                        .or_else(|| get_attribute(tag, "w:val"))
1773                        .as_deref(),
1774                )
1775            })
1776            .unwrap_or_else(|| "☐".to_string());
1777        let checked = on_off_value_to_boolean(checked_value.as_deref()).or_else(|| {
1778            if !trimmed_content_text.is_empty() {
1779                Some(trimmed_content_text.contains(&checked_symbol))
1780            } else {
1781                None
1782            }
1783        });
1784        return Some(FormFieldRunNode {
1785            r#type: FormFieldRunNodeType::FormField,
1786            field_type: FormFieldType::Checkbox,
1787            source_kind: Some(FormFieldSourceKind::Sdt),
1788            id: id_value,
1789            tag,
1790            title,
1791            placeholder,
1792            checked: Some(checked.unwrap_or(false)),
1793            value: None,
1794            options: None,
1795            widget: None,
1796            checked_symbol: Some(checked_symbol),
1797            unchecked_symbol: Some(unchecked_symbol),
1798            style,
1799            link,
1800            source_xml: Some(sdt_xml.to_string()),
1801            source_preview: None,
1802        });
1803    }
1804
1805    if re::get(r"(?i)<w:(?:dropDownList|comboBox)\b")
1806        .is_some_and(|re| re.is_match(&sdt_properties_xml))
1807    {
1808        let style = trimmed_sdt_dropdown_style(
1809            &content_runs,
1810            &content_text,
1811            style,
1812            context,
1813            paragraph_style_id,
1814            table_run_style,
1815        );
1816        let mut options = Vec::new();
1817        if let Some(re) = re::get(r"(?i)<w:listItem\b[^>]*/?>") {
1818            for mat in re.find_iter(&sdt_properties_xml) {
1819                let list_item_tag = mat.as_str();
1820                let display_text = get_attribute(list_item_tag, "w:displayText")
1821                    .and_then(|value| decode_xml_attribute(Some(&value)));
1822                let value = get_attribute(list_item_tag, "w:value")
1823                    .and_then(|value| decode_xml_attribute(Some(&value)));
1824                let Some(fallback_text) = display_text
1825                    .clone()
1826                    .or(value.clone())
1827                    .map(|t| t.trim().to_string())
1828                    .filter(|t| !t.is_empty())
1829                else {
1830                    continue;
1831                };
1832                options.push(FormFieldOption {
1833                    display_text: fallback_text,
1834                    value: value
1835                        .map(|v| v.trim().to_string())
1836                        .filter(|v| !v.is_empty()),
1837                });
1838            }
1839        }
1840        let last_value_tag = re::get(r"(?i)<w:lastValue\b[^>]*/?>")
1841            .and_then(|re| re.find(&sdt_properties_xml))
1842            .map(|m| m.as_str().to_string());
1843        let last_value = last_value_tag
1844            .as_deref()
1845            .and_then(|tag| get_attribute(tag, "w:val"))
1846            .and_then(|value| decode_xml_attribute(Some(&value)))
1847            .map(|v| v.trim().to_string())
1848            .filter(|v| !v.is_empty());
1849        let selected_value = if trimmed_content_text.is_empty() {
1850            last_value
1851        } else {
1852            Some(trimmed_content_text.to_string())
1853        };
1854        return Some(FormFieldRunNode {
1855            r#type: FormFieldRunNodeType::FormField,
1856            field_type: FormFieldType::Dropdown,
1857            source_kind: Some(FormFieldSourceKind::Sdt),
1858            id: id_value,
1859            tag,
1860            title,
1861            placeholder,
1862            checked: None,
1863            value: selected_value,
1864            options: if options.is_empty() {
1865                None
1866            } else {
1867                Some(options)
1868            },
1869            widget: None,
1870            checked_symbol: None,
1871            unchecked_symbol: None,
1872            style,
1873            link,
1874            source_xml: Some(sdt_xml.to_string()),
1875            source_preview: None,
1876        });
1877    }
1878
1879    if re::get(r"(?i)<w:date\b").is_some_and(|re| re.is_match(&sdt_properties_xml)) {
1880        let full_date_tag = re::get(r"(?i)<w:fullDate\b[^>]*/?>")
1881            .and_then(|re| re.find(&sdt_properties_xml))
1882            .map(|m| m.as_str().to_string());
1883        let full_date = full_date_tag
1884            .as_deref()
1885            .and_then(|tag| get_attribute(tag, "w:val"))
1886            .and_then(|value| decode_xml_attribute(Some(&value)))
1887            .map(|v| v.trim().to_string())
1888            .filter(|v| !v.is_empty());
1889        let value = if trimmed_content_text.is_empty() {
1890            full_date
1891        } else {
1892            Some(content_text)
1893        };
1894        return Some(FormFieldRunNode {
1895            r#type: FormFieldRunNodeType::FormField,
1896            field_type: FormFieldType::Date,
1897            source_kind: Some(FormFieldSourceKind::Sdt),
1898            id: id_value,
1899            tag,
1900            title,
1901            placeholder,
1902            checked: None,
1903            value,
1904            options: None,
1905            widget: None,
1906            checked_symbol: None,
1907            unchecked_symbol: None,
1908            style,
1909            link,
1910            source_xml: Some(sdt_xml.to_string()),
1911            source_preview: None,
1912        });
1913    }
1914
1915    if re::get(r"(?i)<w:(?:text|richText)\b").is_some_and(|re| re.is_match(&sdt_properties_xml)) {
1916        return Some(FormFieldRunNode {
1917            r#type: FormFieldRunNodeType::FormField,
1918            field_type: FormFieldType::Text,
1919            source_kind: Some(FormFieldSourceKind::Sdt),
1920            id: id_value,
1921            tag,
1922            title,
1923            placeholder,
1924            checked: None,
1925            value: Some(content_text),
1926            options: None,
1927            widget: None,
1928            checked_symbol: None,
1929            unchecked_symbol: None,
1930            style,
1931            link,
1932            source_xml: Some(sdt_xml.to_string()),
1933            source_preview: None,
1934        });
1935    }
1936
1937    None
1938}
1939
1940pub fn parse_paragraph_form_field_tokens(
1941    paragraph_xml: &str,
1942    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1943    paragraph_style_id: Option<&str>,
1944    runs: Option<&[ParagraphRunToken]>,
1945) -> Vec<ParagraphFormFieldToken> {
1946    let borrowed = runs.map(|runs| {
1947        runs.iter()
1948            .map(|run| RunToken {
1949                xml: std::borrow::Cow::Borrowed(run.xml.as_str()),
1950                start: run.start,
1951                end: run.end,
1952                link: run.link.clone(),
1953                text_links: run.text_links.clone(),
1954            })
1955            .collect::<Vec<_>>()
1956    });
1957    parse_paragraph_form_field_tokens_in_table(
1958        paragraph_xml,
1959        context,
1960        paragraph_style_id,
1961        borrowed.as_deref(),
1962        None,
1963    )
1964}
1965
1966fn parse_paragraph_form_field_tokens_in_table(
1967    paragraph_xml: &str,
1968    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1969    paragraph_style_id: Option<&str>,
1970    runs: Option<&[RunToken<'_>]>,
1971    table_run_style: Option<&TextStyle>,
1972) -> Vec<ParagraphFormFieldToken> {
1973    let has_legacy_fields = super::scan::contains_tag(paragraph_xml, "w:fldChar");
1974    if !has_legacy_fields && !super::scan::contains_tag(paragraph_xml, "w:sdt") {
1975        return Vec::new();
1976    }
1977    let legacy_tokens = if has_legacy_fields
1978        || crate::xml::find_ignore_ascii_case(paragraph_xml, 0, "<w:fldChar").is_some()
1979    {
1980        let paragraph_runs: Vec<RunToken<'_>> = runs
1981            .map(|r| r.to_vec())
1982            .unwrap_or_else(|| paragraph_runs(paragraph_xml, context));
1983        let story_runs: Vec<_> = paragraph_runs
1984            .iter()
1985            .map(|run| RunToken {
1986                xml: std::borrow::Cow::Owned(strip_text_box_content(&run.xml)),
1987                ..run.clone()
1988            })
1989            .collect();
1990        parse_legacy_paragraph_form_field_tokens(
1991            paragraph_xml,
1992            &story_runs,
1993            context,
1994            paragraph_style_id,
1995            table_run_style,
1996        )
1997    } else {
1998        Vec::new()
1999    };
2000    let text_box_ranges = extract_balanced_tag_ranges(paragraph_xml, "w:txbxContent");
2001    let sdt_ranges: Vec<_> = extract_balanced_tag_ranges(paragraph_xml, "w:sdt")
2002        .into_iter()
2003        .filter(|range| {
2004            !text_box_ranges
2005                .iter()
2006                .any(|story| range.start >= story.start && range.end <= story.end)
2007        })
2008        .collect();
2009    if sdt_ranges.is_empty() {
2010        return legacy_tokens;
2011    }
2012
2013    let hyperlink_ranges: Vec<_> = paragraph_nested_tag_ranges(paragraph_xml, "w:hyperlink")
2014        .into_iter()
2015        .map(|range| {
2016            let hyperlink_xml = &paragraph_xml[range.start..range.end];
2017            let hyperlink_tag = re::get(r"(?i)<w:hyperlink\b[^>]*>")
2018                .and_then(|re| re.find(hyperlink_xml))
2019                .map(|m| m.as_str().to_string())
2020                .unwrap_or_default();
2021            (range, hyperlink_href_from_tag(&hyperlink_tag, context))
2022        })
2023        .collect();
2024
2025    let mut sdt_tokens = Vec::new();
2026    for range in sdt_ranges {
2027        let sdt_xml = &paragraph_xml[range.start..range.end];
2028        let link = hyperlink_ranges
2029            .iter()
2030            .rev()
2031            .find(|(hyperlink_range, _)| {
2032                range.start >= hyperlink_range.start && range.end <= hyperlink_range.end
2033            })
2034            .and_then(|(_, href)| href.clone());
2035        if let Some(field) = parse_form_field_from_sdt_xml(
2036            sdt_xml,
2037            context,
2038            paragraph_style_id,
2039            table_run_style,
2040            link,
2041        ) {
2042            sdt_tokens.push(ParagraphFormFieldToken {
2043                start: range.start,
2044                end: range.end,
2045                field,
2046            });
2047        }
2048    }
2049
2050    let mut combined = legacy_tokens;
2051    combined.extend(sdt_tokens);
2052    combined.sort_by_key(|token| token.start);
2053    combined
2054}
2055
2056fn parse_paragraph_mark_formatting(
2057    current_mark: Option<&str>,
2058    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2059    table_run_style: Option<&TextStyle>,
2060) -> crate::model::ParagraphMarkFormattingSource {
2061    let current = current_mark.unwrap_or("<w:rPr/>");
2062    let character_style = current_mark.and_then(|xml| super::styles::direct_table_property(xml, "w:rStyle"))
2063        .and_then(|tag| get_attribute(tag, "w:val"))
2064        .and_then(|id| context.styles().run_style_by_id.get(&id).cloned());
2065    crate::model::ParagraphMarkFormattingSource {
2066        default_style: super::style::cached_mark_default_style(
2067            context.styles().default_run_style.as_ref(),
2068            table_run_style,
2069        ),
2070        character_style: character_style.map(Into::into),
2071        direct_style: super::style::parse_shared_text_style_from_xml(
2072            current,
2073            &context.styles().theme_fonts,
2074        )
2075        .map(Into::into),
2076    }
2077}
2078
2079pub fn parse_paragraph(
2080    paragraph_xml: &str,
2081    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2082) -> ParagraphNode {
2083    parse_paragraph_in_table(paragraph_xml, context, None)
2084}
2085
2086pub fn parse_paragraph_in_table(
2087    paragraph_xml: &str,
2088    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2089    table_paragraph_spacing: Option<&crate::model::ParagraphSpacing>,
2090) -> ParagraphNode {
2091    parse_paragraph_with_table_run_style(paragraph_xml, context, table_paragraph_spacing, None)
2092}
2093
2094pub(crate) fn parse_paragraph_with_table_run_style(
2095    paragraph_xml: &str,
2096    context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2097    table_paragraph_spacing: Option<&crate::model::ParagraphSpacing>,
2098    table_run_style: Option<&TextStyle>,
2099) -> ParagraphNode {
2100    let mut children: Vec<ParagraphChildNode> = Vec::new();
2101    let paragraph_properties_xml =
2102        super::styles::direct_table_property(paragraph_xml, "w:pPr").unwrap_or_default();
2103    let paragraph_style = super::style::parse_paragraph_style_from_properties(
2104        paragraph_properties_xml,
2105        context,
2106        table_paragraph_spacing,
2107    );
2108    let source_paragraph_mark_properties_xml =
2109        crate::xml::direct_child_tag(paragraph_properties_xml, "w:rPr");
2110    let effective_mark_properties = source_paragraph_mark_properties_xml
2111        .map(|xml| crate::xml::without_direct_child_tags(xml, "w:rPrChange"));
2112    let paragraph_mark_style = super::style::parse_mark_run_style(
2113        effective_mark_properties.as_deref().unwrap_or_default(),
2114        context,
2115        paragraph_style.as_ref().and_then(|style| style.style_id.as_deref()),
2116        table_run_style,
2117    );
2118    let source_paragraph_mark_formatting = parse_paragraph_mark_formatting(
2119        effective_mark_properties.as_deref(),
2120        context,
2121        table_run_style,
2122    );
2123    let paragraph_mark_deleted = source_paragraph_mark_properties_xml
2124        .as_deref()
2125        .is_some_and(|xml| super::styles::direct_table_property(xml, "w:del").is_some());
2126    if let Some(runs) = indexed_plain_paragraph(paragraph_xml) {
2127        children.reserve(runs.iter().map(|run| run.run.tokens.len()).sum());
2128        for IndexedPlainParagraphRun { xml, run } in runs {
2129            let mut style = super::style::parse_run_style_from_properties(
2130                xml,
2131                run.properties,
2132                run.scripts,
2133                context,
2134                paragraph_style
2135                    .as_ref()
2136                    .and_then(|style| style.style_id.as_deref()),
2137                table_run_style,
2138            );
2139            let mut tokens = run
2140                .tokens
2141                .into_iter()
2142                .filter(|token| !token.text.is_empty())
2143                .peekable();
2144            while let Some(token) = tokens.next() {
2145                children.push(ParagraphChildNode::Text(TextRunNode {
2146                    r#type: TextRunNodeType::Text,
2147                    text: token.text,
2148                    style: if tokens.peek().is_none() {
2149                        style.take()
2150                    } else {
2151                        style.clone()
2152                    },
2153                    link: None,
2154                    note_reference: None,
2155                }));
2156            }
2157        }
2158    } else {
2159        let runs = paragraph_runs(paragraph_xml, context);
2160        children.reserve(runs.len());
2161        let form_field_tokens = parse_paragraph_form_field_tokens_in_table(
2162            paragraph_xml,
2163            context,
2164            paragraph_style.as_ref().and_then(|s| s.style_id.as_deref()),
2165            Some(&runs),
2166            table_run_style,
2167        );
2168
2169        enum ContentToken<'a> {
2170            Run {
2171                start: usize,
2172                token: &'a RunToken<'a>,
2173            },
2174            FormField {
2175                start: usize,
2176                token: &'a ParagraphFormFieldToken,
2177            },
2178            Math {
2179                start: usize,
2180                xml: &'a str,
2181            },
2182        }
2183
2184        let mut content_tokens: Vec<ContentToken<'_>> =
2185            Vec::with_capacity(runs.len() + form_field_tokens.len());
2186        let mut form_field_token_cursor = 0usize;
2187        for run in &runs {
2188            while form_field_token_cursor < form_field_tokens.len()
2189                && form_field_tokens[form_field_token_cursor].end <= run.start
2190            {
2191                form_field_token_cursor += 1;
2192            }
2193            let current_form_field_token = form_field_tokens.get(form_field_token_cursor);
2194            let inside_form_field = current_form_field_token
2195                .is_some_and(|token| run.start >= token.start && run.end <= token.end);
2196            if inside_form_field {
2197                continue;
2198            }
2199            content_tokens.push(ContentToken::Run {
2200                start: run.start,
2201                token: run,
2202            });
2203        }
2204        for form_field_token in &form_field_tokens {
2205            content_tokens.push(ContentToken::FormField {
2206                start: form_field_token.start,
2207                token: form_field_token,
2208            });
2209        }
2210        let text_box_ranges = extract_balanced_tag_ranges(paragraph_xml, "w:txbxContent");
2211        for range in extract_balanced_tag_ranges(paragraph_xml, "m:oMath") {
2212            if !text_box_ranges
2213                .iter()
2214                .any(|story| range.start >= story.start && range.end <= story.end)
2215            {
2216                content_tokens.push(ContentToken::Math {
2217                    start: range.start,
2218                    xml: &paragraph_xml[range.start..range.end],
2219                });
2220            }
2221        }
2222        content_tokens.sort_by_key(|token| match token {
2223            ContentToken::Run { start, .. } => *start,
2224            ContentToken::FormField { start, .. } => *start,
2225            ContentToken::Math { start, .. } => *start,
2226        });
2227
2228        for content_token in content_tokens {
2229            match content_token {
2230                ContentToken::Math { xml, .. } => {
2231                    if let Some(math) = super::math::parse_math(xml) {
2232                        children.push(ParagraphChildNode::Image(Box::new(math)));
2233                    } else {
2234                        context.push_warning(
2235                            "Unable to interpret an equation; source markup is preserved",
2236                        );
2237                    }
2238                }
2239                ContentToken::FormField { token, .. } => {
2240                    children.push(ParagraphChildNode::FormField(Box::new(token.field.clone())));
2241                }
2242                ContentToken::Run { token: run, .. } => {
2243                    let plain = indexed_plain_run(&run.xml);
2244                    let style = if let Some(plain) = &plain {
2245                        super::style::parse_run_style_from_properties(
2246                            &run.xml,
2247                            plain.properties,
2248                            plain.scripts,
2249                            context,
2250                            paragraph_style.as_ref().and_then(|s| s.style_id.as_deref()),
2251                            table_run_style,
2252                        )
2253                    } else {
2254                        parse_run_style_in_table(
2255                            &run.xml,
2256                            context,
2257                            paragraph_style.as_ref().and_then(|s| s.style_id.as_deref()),
2258                            table_run_style,
2259                        )
2260                    };
2261                    if let Some(active_x_field) = (plain.is_none()
2262                        && super::scan::contains_tag(&run.xml, "w:object"))
2263                    .then(|| {
2264                        parse_run_active_x_checkbox_field_shared(
2265                            &run.xml,
2266                            context,
2267                            style.clone(),
2268                            run.link.clone(),
2269                        )
2270                    })
2271                    .flatten()
2272                    {
2273                        children.push(ParagraphChildNode::FormField(Box::new(active_x_field)));
2274                        continue;
2275                    }
2276                    let images = if plain.is_some() {
2277                        Vec::new()
2278                    } else {
2279                        parse_run_image_tokens(&run.xml, context)
2280                    };
2281                    // Text boxes own a separate story, including when their drawing
2282                    // renders as a group or cannot be represented as an image.
2283                    let parsed_tokens = if let Some(plain) = plain {
2284                        plain.tokens
2285                    } else {
2286                        let run_xml_for_text = run_text_xml(&run.xml);
2287                        match &run_xml_for_text {
2288                            std::borrow::Cow::Borrowed(xml) => parse_active_run_text_tokens(xml),
2289                            std::borrow::Cow::Owned(xml) => parse_run_text_tokens(xml),
2290                        }
2291                    };
2292                    let mut parsed_tokens = parsed_tokens
2293                        .into_iter()
2294                        .filter(|token| !token.text.is_empty() || token.note_reference.is_some())
2295                        .peekable();
2296                    let mut remaining_style = style;
2297                    let needs_child_ordering = !images.is_empty();
2298                    let mut ordered_children = Vec::new();
2299                    while let Some(token) = parsed_tokens.next() {
2300                        let child = ParagraphChildNode::Text(TextRunNode {
2301                            r#type: TextRunNodeType::Text,
2302                            text: token.text,
2303                            style: if parsed_tokens.peek().is_none() {
2304                                remaining_style.take()
2305                            } else {
2306                                remaining_style.clone()
2307                            },
2308                            link: run
2309                                .text_links
2310                                .iter()
2311                                .rev()
2312                                .find(|(offset, _)| *offset <= token.source_start)
2313                                .and_then(|(_, link)| link.clone()),
2314                            note_reference: token.note_reference,
2315                        });
2316                        if needs_child_ordering {
2317                            ordered_children.push((token.source_start, child));
2318                        } else {
2319                            children.push(child);
2320                        }
2321                    }
2322                    for (source_start, mut image) in images {
2323                        if let Some(xml) = image.source_xml.as_deref() {
2324                            let active_xml = prefer_alternate_content_choice(xml);
2325                            let text_boxes =
2326                                extract_balanced_tag_blocks(&active_xml, "w:txbxContent");
2327                            if !text_boxes.is_empty() {
2328                                image.content_nodes = Some(
2329                                    text_boxes
2330                                        .iter()
2331                                        .flat_map(|xml| {
2332                                            super::document::parse_document_xml(xml, context)
2333                                        })
2334                                        .collect(),
2335                                );
2336                                image.semantic_kind = Some("text_box".into());
2337                            }
2338                        }
2339                        ordered_children
2340                            .push((source_start, ParagraphChildNode::Image(Box::new(image))));
2341                    }
2342                    if needs_child_ordering {
2343                        ordered_children.sort_by_key(|(offset, _)| *offset);
2344                        children.extend(ordered_children.into_iter().map(|(_, child)| child));
2345                    }
2346                }
2347            }
2348        }
2349    }
2350
2351    if children.is_empty() {
2352        // An empty paragraph renders at the line height of its paragraph mark
2353        // (the pPr>rPr formatting), so the synthetic empty run must carry that
2354        // style — otherwise spacer paragraphs collapse to the default font size.
2355        children.push(ParagraphChildNode::Text(TextRunNode {
2356            r#type: TextRunNodeType::Text,
2357            text: String::new(),
2358            style: paragraph_mark_style.clone(),
2359            link: None,
2360            note_reference: None,
2361        }));
2362    }
2363
2364    let source_run_provenance = children
2365        .iter()
2366        .map(|child| match child {
2367            ParagraphChildNode::Text(run) => Some(ParagraphSourceTextPatchRun {
2368                style: run.style.clone(),
2369                link: run.link.clone(),
2370                note_reference: run.note_reference.clone(),
2371            }),
2372            _ => None,
2373        })
2374        .collect::<Option<Vec<_>>>()
2375        .map(|runs| ParagraphSourceTextPatch { runs });
2376
2377    ParagraphNode {
2378        r#type: ParagraphNodeType::Paragraph,
2379        block_id: None,
2380        style: paragraph_style,
2381        paragraph_mark_style: paragraph_mark_style.clone(),
2382        source_paragraph_mark_style: paragraph_mark_style,
2383        source_paragraph_mark_formatting: Some(source_paragraph_mark_formatting),
2384        source_paragraph_mark_properties_xml: source_paragraph_mark_properties_xml
2385            .map(str::to_owned),
2386        paragraph_mark_deleted: if paragraph_mark_deleted {
2387            Some(true)
2388        } else {
2389            None
2390        },
2391        children,
2392        source_xml: Some(paragraph_xml.to_string()),
2393        source_text_patch: None,
2394        source_run_provenance,
2395    }
2396}