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