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 instruction_position: Option<(usize, usize)>,
872 result_position: Option<(usize, usize)>,
873 empty_result_position: Option<(usize, usize)>,
874 has_result: bool,
875 has_unseparated_content: bool,
876 nested: bool,
877 }
878 let mut fields: Vec<FieldState> = Vec::new();
879 let mut complex_field_ranges = Vec::new();
880 let mut empty_fields_by_run: std::collections::HashMap<usize, Vec<(usize, String)>> =
881 std::collections::HashMap::new();
882 let field_tokens = re::get_unchecked(
883 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[^>]*/>"#,
884 );
885
886 for (run_index, range) in run_ranges.iter().enumerate() {
887 if !has_complex_fields
888 || (fields.is_empty()
889 && !paragraph_xml[range.start..range.end].contains("<mc:AlternateContent")
890 && crate::xml::find_ignore_ascii_case(
891 ¶graph_xml[range.start..range.end],
892 0,
893 "<w:fldChar",
894 )
895 .is_none())
896 {
897 field_text_links_by_run.insert(run_index, vec![(0, None)]);
898 continue;
899 }
900 let run_xml = run_text_xml(¶graph_xml[range.start..range.end]);
901 let mut text_links = vec![(0, fields.iter().rev().find_map(|field| field.link.clone()))];
902 for token in field_tokens.captures_iter(&run_xml) {
903 if let Some(instruction) = token.get(1) {
904 if let Some(field) = fields.last_mut().filter(|field| !field.separated) {
905 field.instruction.push_str(instruction.as_str());
906 field
907 .instruction_position
908 .get_or_insert((run_index, token.get(0).unwrap().end()));
909 }
910 continue;
911 }
912 let tag = token.get(0).unwrap().as_str();
913 if tag.to_ascii_lowercase().starts_with("<w:fldchar") {
914 match get_attribute(tag, "w:fldCharType").as_deref() {
915 Some("begin") => {
916 let nested = !fields.is_empty();
917 for field in &mut fields {
918 field.nested = true;
919 }
920 fields.push(FieldState {
921 start: range.start + token.get(0).unwrap().start(),
922 nested,
923 ..FieldState::default()
924 });
925 }
926 Some("separate") => {
927 if let Some(field) = fields.last_mut() {
928 if field.separated {
929 field.nested = true;
930 }
931 field.link = hyperlink_href_from_field_instruction(&field.instruction);
932 field.separated = true;
933 field.result_position = Some((run_index, token.get(0).unwrap().end()));
934 }
935 }
936 Some("end") => {
937 if let Some(field) = fields.pop() {
938 let end = range.start + token.get(0).unwrap().end();
939 complex_field_ranges.push((field.start, end));
940 let overlaps_simple = simple_fields
941 .iter()
942 .any(|simple| simple.start < end && simple.end > field.start);
943 let has_other_content =
944 paragraph_xml.get(field.start..end).is_none_or(|xml| {
945 ["w:sdt", "m:oMath", "w:object"]
946 .iter()
947 .any(|tag| super::scan::contains_tag(xml, tag))
948 });
949 if !field.nested
950 && !overlaps_simple
951 && !field.has_result
952 && !has_other_content
953 {
954 let instruction = decode_xml_entities(&field.instruction);
955 if is_page_field_instruction(&instruction) {
956 let result_position = field
957 .empty_result_position
958 .or(field.result_position)
959 .or_else(|| {
960 if field.separated || field.has_unseparated_content {
961 None
962 } else {
963 field.instruction_position
964 }
965 });
966 if let Some((result_run, result_offset)) = result_position {
967 empty_fields_by_run
968 .entry(result_run)
969 .or_default()
970 .push((result_offset, instruction));
971 }
972 }
973 }
974 }
975 }
976 _ => {}
977 }
978 text_links.push((
979 token.get(0).unwrap().end(),
980 fields.iter().rev().find_map(|field| field.link.clone()),
981 ));
982 } else {
983 let is_text = tag.starts_with("<w:t") || tag.starts_with("<a:t");
984 let has_result = !is_text
985 || tag
986 .find('>')
987 .zip(tag.rfind('<'))
988 .is_some_and(|(start, end)| end > start + 1);
989 if has_result {
990 for field in &mut fields {
991 if field.separated {
992 field.has_result = true;
993 } else {
994 field.has_unseparated_content = true;
995 }
996 }
997 } else if let Some(field) = fields.last_mut().filter(|field| field.separated) {
998 field
999 .empty_result_position
1000 .get_or_insert((run_index, token.get(0).unwrap().start()));
1001 }
1002 let link = fields.iter().rev().find_map(|field| field.link.clone());
1003 if let Some(link) = link.as_ref() {
1004 field_links_by_run
1005 .entry(run_index)
1006 .or_insert_with(|| link.clone());
1007 }
1008 }
1009 }
1010 field_text_links_by_run.insert(run_index, text_links);
1011 }
1012
1013 complex_field_ranges.extend(
1014 fields
1015 .iter()
1016 .map(|field| (field.start, paragraph_xml.len())),
1017 );
1018 let mut synthetic_fields = Vec::new();
1019 for simple in &simple_fields {
1020 if simple_fields.iter().any(|other| {
1021 other.start != simple.start && other.start < simple.end && other.end > simple.start
1022 }) || complex_field_ranges
1023 .iter()
1024 .any(|(start, end)| *start < simple.end && *end > simple.start)
1025 {
1026 continue;
1027 }
1028 let xml = ¶graph_xml[simple.start..simple.end];
1029 let Some(instruction) = super::scan::find_tag_token_ref(xml, "w:fldSimple")
1030 .and_then(|tag| get_attribute(tag, "w:instr"))
1031 .map(|instruction| decode_xml_entities(&instruction))
1032 .filter(|instruction| is_page_field_instruction(instruction))
1033 else {
1034 continue;
1035 };
1036 if parse_run_text_tokens(xml)
1037 .iter()
1038 .any(|token| !token.text.is_empty() || token.note_reference.is_some())
1039 || ["w:drawing", "w:pict", "w:object", "w:sdt", "m:oMath"]
1040 .iter()
1041 .any(|tag| super::scan::contains_tag(xml, tag))
1042 {
1043 continue;
1044 }
1045 if let Some((run_index, _)) = run_ranges
1046 .iter()
1047 .enumerate()
1048 .find(|(_, run)| run.start >= simple.start && run.end <= simple.end)
1049 {
1050 empty_fields_by_run
1051 .entry(run_index)
1052 .or_default()
1053 .push((0, instruction));
1054 } else {
1055 synthetic_fields.push(RunToken {
1056 xml: std::borrow::Cow::Borrowed(xml),
1057 start: simple.start,
1058 end: simple.end,
1059 link: None,
1060 text_links: Vec::new(),
1061 empty_fields: vec![(0, instruction)],
1062 });
1063 }
1064 }
1065
1066 let mut hyperlink_range_cursor = 0usize;
1067 let mut active_hyperlinks: Vec<&(crate::xml::TagRange, Option<String>)> = Vec::new();
1068 let mut runs: Vec<_> = run_ranges
1069 .into_iter()
1070 .enumerate()
1071 .map(|(run_index, range)| {
1072 active_hyperlinks.retain(|(hyperlink, _)| hyperlink.end > range.start);
1073 while hyperlink_range_cursor < hyperlink_ranges.len()
1074 && hyperlink_ranges[hyperlink_range_cursor].0.start < range.end
1075 {
1076 let hyperlink = &hyperlink_ranges[hyperlink_range_cursor];
1077 if hyperlink.0.end > range.start {
1078 active_hyperlinks.push(hyperlink);
1079 }
1080 hyperlink_range_cursor += 1;
1081 }
1082 let field_links = field_text_links_by_run
1083 .remove(&run_index)
1084 .unwrap_or_default();
1085 let overlapping = &active_hyperlinks;
1086 let hyperlink_href = overlapping
1087 .iter()
1088 .rev()
1089 .find(|(hyperlink_range, _)| {
1090 range.start >= hyperlink_range.start && range.end <= hyperlink_range.end
1091 })
1092 .and_then(|(_, href)| href.clone());
1093 let mut offsets: Vec<_> = field_links.iter().map(|(offset, _)| *offset).collect();
1094 for (hyperlink, _) in overlapping {
1095 offsets.push(hyperlink.start.saturating_sub(range.start));
1096 if hyperlink.end < range.end {
1097 offsets.push(hyperlink.end - range.start);
1098 }
1099 }
1100 offsets.sort_unstable();
1101 offsets.dedup();
1102 let text_links = offsets
1103 .into_iter()
1104 .map(|offset| {
1105 let position = range.start + offset;
1106 let href = overlapping
1107 .iter()
1108 .rev()
1109 .find(|(hyperlink, _)| {
1110 position >= hyperlink.start && position < hyperlink.end
1111 })
1112 .and_then(|(_, href)| href.clone())
1113 .or_else(|| {
1114 field_links
1115 .iter()
1116 .rev()
1117 .find(|(start, _)| *start <= offset)
1118 .and_then(|(_, href)| href.clone())
1119 });
1120 (offset, href)
1121 })
1122 .collect();
1123 let link = hyperlink_href.or_else(|| field_links_by_run.get(&run_index).cloned());
1124 RunToken {
1125 xml: std::borrow::Cow::Borrowed(¶graph_xml[range.start..range.end]),
1126 start: range.start,
1127 end: range.end,
1128 link,
1129 text_links,
1130 empty_fields: empty_fields_by_run.remove(&run_index).unwrap_or_default(),
1131 }
1132 })
1133 .collect();
1134 runs.extend(synthetic_fields);
1135 runs.sort_by_key(|run| run.start);
1136 runs
1137}
1138
1139#[derive(Clone, Debug)]
1140pub struct ParagraphFormFieldToken {
1141 pub start: usize,
1142 pub end: usize,
1143 pub field: FormFieldRunNode,
1144}
1145
1146fn decode_active_x_binary_markup(binary: &[u8]) -> String {
1147 let utf16: String = binary
1148 .chunks_exact(2)
1149 .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
1150 .filter_map(|code_unit| char::from_u32(code_unit as u32))
1151 .collect();
1152 if re::get(r"(?i)input\s+type=").is_some_and(|re| re.is_match(&utf16)) {
1153 return utf16;
1154 }
1155 binary.iter().map(|&b| b as char).collect()
1156}
1157
1158pub fn parse_run_active_x_checkbox_field(
1159 run_xml: &str,
1160 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1161 style: Option<TextStyle>,
1162 link: Option<String>,
1163) -> Option<FormFieldRunNode> {
1164 parse_run_active_x_checkbox_field_shared(run_xml, context, style.map(Into::into), link)
1165}
1166
1167fn parse_active_x_source_preview(
1168 run_xml: &str,
1169 control_start: usize,
1170 control_tag: &str,
1171 relationship_id: &str,
1172 checked: bool,
1173 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1174) -> Option<Box<FormFieldSourcePreview>> {
1175 let shape_id = decode_xml_entities(&get_attribute(control_tag, "w:shapeid")?);
1176 if shape_id.is_empty() {
1177 return None;
1178 }
1179 let object = extract_balanced_tag_ranges(run_xml, "w:object")
1180 .into_iter()
1181 .find(|range| range.start < control_start && control_start < range.end)?;
1182 let object_xml = &run_xml[object.start..object.end];
1183 let mut matches = extract_direct_child_tag_ranges(object_xml)
1184 .into_iter()
1185 .filter(|range| range.tag_name.eq_ignore_ascii_case("v:shape"))
1186 .filter(|range| {
1187 let shape_xml = &object_xml[range.start..range.end];
1188 let Some(tag_end) = crate::xml::tag_token_end(shape_xml, 1) else {
1189 return false;
1190 };
1191 get_attribute(&shape_xml[..tag_end], "id")
1192 .is_some_and(|id| decode_xml_entities(&id) == shape_id)
1193 });
1194 let matched = matches.next()?;
1195 if matches.next().is_some() {
1196 return None;
1197 }
1198 let shape_xml = &object_xml[matched.start..matched.end];
1199 let children = extract_direct_child_tag_ranges(shape_xml);
1200 let mut image_data = children
1201 .iter()
1202 .filter(|range| range.tag_name.eq_ignore_ascii_case("v:imagedata"));
1203 let data_range = image_data.next()?;
1204 if image_data.next().is_some()
1205 || children.iter().any(|range| {
1206 range.tag_name.eq_ignore_ascii_case("v:group")
1207 || range.tag_name.eq_ignore_ascii_case("v:shape")
1208 || range.tag_name.eq_ignore_ascii_case("v:textbox")
1209 })
1210 {
1211 return None;
1212 }
1213 let image_relationship =
1214 get_attribute(&shape_xml[data_range.start..data_range.end], "r:id")?;
1215 let part_name = context.relationships.get(&image_relationship)?;
1216 if context.binary_assets.get(part_name)?.is_empty() {
1217 return None;
1218 }
1219 let mut image = parse_run_image_block(shape_xml, context)?;
1220 if is_windows_metafile_content_type(image.content_type.as_deref(), image.part_name.as_deref())
1221 && !matches!(image.content_type.as_deref(), Some("image/svg+xml" | "image/png"))
1222 {
1223 let svg = crate::wmf::wmf_bitmap_preview_to_svg(image.data.as_deref()?)?;
1224 image.src = Some(svg_data_uri(&svg));
1225 image.content_type = Some("image/svg+xml".to_owned());
1226 }
1227 if image.part_name.as_deref() != Some(part_name.as_str())
1228 || image.data.as_ref().is_none_or(Vec::is_empty)
1229 || image.src.as_deref().is_none_or(str::is_empty)
1230 || image
1231 .content_type
1232 .as_deref()
1233 .is_none_or(|content_type| !content_type.starts_with("image/"))
1234 {
1235 return None;
1236 }
1237 Some(Box::new(FormFieldSourcePreview {
1238 relationship_id: relationship_id.to_owned(),
1239 shape_id,
1240 checked,
1241 image: Box::new(image),
1242 }))
1243}
1244
1245fn parse_run_active_x_checkbox_field_shared(
1246 run_xml: &str,
1247 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1248 style: Option<SharedTextStyle>,
1249 link: Option<String>,
1250) -> Option<FormFieldRunNode> {
1251 if !re::get(r"(?i)<w:object\b").is_some_and(|re| re.is_match(run_xml))
1252 || !re::get(r"(?i)<w:control\b").is_some_and(|re| re.is_match(run_xml))
1253 {
1254 return None;
1255 }
1256 let control = re::get(r"(?i)<w:control\b[^>]*/?>")
1257 .and_then(|re| re.find(run_xml))?;
1258 let control_tag = control.as_str();
1259 let control_relationship_id = get_attribute(&control_tag, "r:id")?;
1260 let active_x_part_name = context.relationships.get(&control_relationship_id)?.clone();
1261 let active_x_xml = context.parts.get(&active_x_part_name)?.content.clone();
1262 let ocx_tag = re::get(r"(?i)<ax:ocx\b[^>]*/?>")
1263 .and_then(|re| re.find(&active_x_xml))
1264 .map(|m| m.as_str().to_string())
1265 .unwrap_or_default();
1266 let binary_relationship_id = get_attribute(&ocx_tag, "r:id")?;
1267 let active_x_relationships = parse_relationships_from_parts(context.parts, &active_x_part_name);
1268 let active_x_binary_part_name = active_x_relationships.get(&binary_relationship_id)?.clone();
1269 let active_x_binary = context.binary_assets.get(&active_x_binary_part_name)?;
1270 if active_x_binary.is_empty() {
1271 return None;
1272 }
1273 let markup = decode_active_x_binary_markup(active_x_binary);
1274 if !re::get(r#"(?i)input\s+type\s*=\s*"checkbox""#).is_some_and(|re| re.is_match(&markup)) {
1275 return None;
1276 }
1277 let name = re::get(r#"(?i)\bname\s*=\s*"([^"]+)""#)
1278 .and_then(|re| re.captures(&markup))
1279 .and_then(|c| c.get(1))
1280 .map(|m| m.as_str().trim().to_string());
1281 let checked = re::get(r"(?i)\bchecked\b").is_some_and(|re| re.is_match(&markup));
1282 let source_preview = parse_active_x_source_preview(
1283 run_xml,
1284 control.start(),
1285 control_tag,
1286 &control_relationship_id,
1287 checked,
1288 context,
1289 );
1290 Some(FormFieldRunNode {
1291 r#type: FormFieldRunNodeType::FormField,
1292 field_type: FormFieldType::Checkbox,
1293 source_kind: Some(FormFieldSourceKind::Legacy),
1294 id: None,
1295 tag: None,
1296 title: None,
1297 placeholder: None,
1298 checked: Some(checked),
1299 value: None,
1300 options: None,
1301 widget: Some(FormFieldWidgetSettings {
1302 name,
1303 enabled: None,
1304 calc_on_exit: None,
1305 text: None,
1306 checkbox: None,
1307 dropdown: None,
1308 }),
1309 checked_symbol: Some("☒".to_string()),
1310 unchecked_symbol: Some("☐".to_string()),
1311 style,
1312 link,
1313 source_xml: Some(run_xml.to_string()),
1314 source_preview,
1315 })
1316}
1317
1318fn parse_legacy_form_field_from_range(
1319 paragraph_xml: &str,
1320 runs: &[RunToken<'_>],
1321 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1322 paragraph_style_id: Option<&str>,
1323 table_run_style: Option<&TextStyle>,
1324 start_run_index: usize,
1325 separate_run_index: Option<usize>,
1326 end_run_index: usize,
1327 raw_instruction: &str,
1328 ff_data_xml: Option<&str>,
1329) -> Option<FormFieldRunNode> {
1330 let instruction = decode_xml_entities(raw_instruction)
1331 .split_whitespace()
1332 .collect::<Vec<_>>()
1333 .join(" ");
1334 let normalized_instruction = instruction.to_ascii_uppercase();
1335 let field_type = if normalized_instruction.contains("FORMCHECKBOX") {
1336 FormFieldType::Checkbox
1337 } else if normalized_instruction.contains("FORMDROPDOWN") {
1338 FormFieldType::Dropdown
1339 } else if normalized_instruction.contains("FORMTEXT") {
1340 FormFieldType::Text
1341 } else {
1342 return None;
1343 };
1344
1345 let result_start_run_index = separate_run_index
1346 .map(|i| i + 1)
1347 .unwrap_or((end_run_index).min(start_run_index + 1));
1348 let result_runs = if result_start_run_index < end_run_index {
1349 &runs[result_start_run_index..end_run_index]
1350 } else {
1351 &[]
1352 };
1353 let fallback_result_text = normalize_legacy_form_display_value(Some(
1354 &result_runs
1355 .iter()
1356 .map(|r| parse_run_text(&r.xml))
1357 .collect::<String>(),
1358 ));
1359 let style_run_xml = result_runs
1360 .iter()
1361 .find(|run| {
1362 !parse_run_text(&run.xml).trim().is_empty()
1363 || re::get(r"(?i)<w:sym\b").is_some_and(|re| re.is_match(&run.xml))
1364 })
1365 .or_else(|| runs.get(start_run_index))
1366 .map(|r| r.xml.as_ref());
1367 let style = style_run_xml.and_then(|xml| {
1368 parse_run_style_in_table(xml, context, paragraph_style_id, table_run_style)
1369 });
1370 let link = runs
1371 .get(start_run_index..=end_run_index.min(runs.len().saturating_sub(1)))
1372 .and_then(|slice| slice.iter().find_map(|r| r.link.clone()));
1373 let field_xml = runs
1374 .get(start_run_index)
1375 .zip(runs.get(end_run_index))
1376 .map(|(start, end)| paragraph_xml[start.start..end.end].to_string());
1377
1378 let name_tag = ff_data_xml.and_then(|xml| {
1379 re::get(r"(?i)<w:name\b[^>]*/?>")
1380 .and_then(|re| re.find(xml))
1381 .map(|m| m.as_str().to_string())
1382 });
1383 let enabled = ff_data_xml.and_then(|xml| parse_on_off_attribute(xml, "enabled"));
1384 let calc_on_exit = ff_data_xml.and_then(|xml| parse_on_off_attribute(xml, "calcOnExit"));
1385 let mut widget_settings = FormFieldWidgetSettings {
1386 name: name_tag
1387 .as_deref()
1388 .and_then(|tag| get_attribute(tag, "w:val"))
1389 .and_then(|value| decode_xml_attribute(Some(&value))),
1390 enabled,
1391 calc_on_exit,
1392 text: None,
1393 checkbox: None,
1394 dropdown: None,
1395 };
1396
1397 match field_type {
1398 FormFieldType::Checkbox => {
1399 let checkbox_xml = ff_data_xml
1400 .and_then(|xml| {
1401 extract_balanced_tag_blocks(xml, "w:checkBox")
1402 .into_iter()
1403 .next()
1404 })
1405 .or_else(|| {
1406 ff_data_xml.and_then(|xml| {
1407 re::get(r"(?i)<w:checkBox\b[^>]*/?>")
1408 .and_then(|re| re.find(xml))
1409 .map(|m| m.as_str().to_string())
1410 })
1411 });
1412 let default_tag = checkbox_xml.as_deref().and_then(|xml| {
1413 re::get(r"(?i)<w:default\b[^>]*/?>")
1414 .and_then(|re| re.find(xml))
1415 .map(|m| m.as_str().to_string())
1416 });
1417 let checked_tag = checkbox_xml.as_deref().and_then(|xml| {
1418 re::get(r"(?i)<w:checked\b[^>]*/?>")
1419 .and_then(|re| re.find(xml))
1420 .map(|m| m.as_str().to_string())
1421 });
1422 let size_auto_tag = checkbox_xml.as_deref().and_then(|xml| {
1423 re::get(r"(?i)<w:sizeAuto\b[^>]*/?>")
1424 .and_then(|re| re.find(xml))
1425 .map(|m| m.as_str().to_string())
1426 });
1427 let size_tag = checkbox_xml.as_deref().and_then(|xml| {
1428 re::get(r"(?i)<w:size\b[^>]*/?>")
1429 .and_then(|re| re.find(xml))
1430 .map(|m| m.as_str().to_string())
1431 });
1432 let default_checked = parse_on_off_tag_value(default_tag.as_deref(), &["w:val"]);
1433 let checked =
1434 parse_on_off_tag_value(checked_tag.as_deref(), &["w:val"]).or(default_checked);
1435 let size_value = size_tag
1436 .as_deref()
1437 .and_then(|t| parse_integer_attribute(t, "w:val"));
1438 let size_pt = size_value
1439 .filter(|v| *v > 0)
1440 .map(|v| ((v as f64) / 2.0 * 100.0).round() / 100.0);
1441 let size_mode = if size_auto_tag.is_some() {
1442 Some(FormFieldCheckboxSizeMode::Auto)
1443 } else if size_pt.is_some() {
1444 Some(FormFieldCheckboxSizeMode::Exact)
1445 } else {
1446 None
1447 };
1448 if default_checked.is_some() || size_mode.is_some() || size_pt.is_some() {
1449 widget_settings.checkbox = Some(FormFieldCheckboxWidgetSettings {
1450 default_checked,
1451 size_mode,
1452 size_pt,
1453 });
1454 }
1455 return Some(FormFieldRunNode {
1456 r#type: FormFieldRunNodeType::FormField,
1457 field_type: FormFieldType::Checkbox,
1458 source_kind: Some(FormFieldSourceKind::Legacy),
1459 id: None,
1460 tag: None,
1461 title: None,
1462 placeholder: None,
1463 checked: Some(checked.unwrap_or(false)),
1464 value: None,
1465 options: None,
1466 widget: Some(widget_settings),
1467 checked_symbol: Some("☒".to_string()),
1468 unchecked_symbol: Some("☐".to_string()),
1469 style,
1470 link,
1471 source_xml: field_xml,
1472 source_preview: None,
1473 });
1474 }
1475 FormFieldType::Dropdown => {
1476 let dropdown_xml = ff_data_xml
1477 .and_then(|xml| {
1478 extract_balanced_tag_blocks(xml, "w:ddList")
1479 .into_iter()
1480 .next()
1481 })
1482 .or_else(|| {
1483 ff_data_xml.and_then(|xml| {
1484 re::get(r"(?i)<w:ddList\b[^>]*/?>")
1485 .and_then(|re| re.find(xml))
1486 .map(|m| m.as_str().to_string())
1487 })
1488 });
1489 let options: Vec<FormFieldOption> = dropdown_xml
1490 .as_deref()
1491 .map(|xml| {
1492 re::get(r"(?i)<w:listEntry\b[^>]*/?>")
1493 .map(|re| {
1494 re.find_iter(xml)
1495 .filter_map(|m| {
1496 let entry = get_attribute(m.as_str(), "w:val")
1497 .and_then(|value| decode_xml_attribute(Some(&value)))?
1498 .trim()
1499 .to_string();
1500 if entry.is_empty() {
1501 None
1502 } else {
1503 Some(FormFieldOption {
1504 display_text: entry.clone(),
1505 value: Some(entry),
1506 })
1507 }
1508 })
1509 .collect()
1510 })
1511 .unwrap_or_default()
1512 })
1513 .unwrap_or_default();
1514 let default_tag = dropdown_xml.as_deref().and_then(|xml| {
1515 re::get(r"(?i)<w:default\b[^>]*/?>")
1516 .and_then(|re| re.find(xml))
1517 .map(|m| m.as_str().to_string())
1518 });
1519 let default_value = default_tag
1520 .as_deref()
1521 .and_then(|tag| get_attribute(tag, "w:val"))
1522 .as_deref()
1523 .and_then(|value| decode_xml_attribute(Some(&value)))
1524 .map(|v| v.trim().to_string())
1525 .filter(|v| !v.is_empty());
1526 let dropdown_default_value = default_value.clone();
1527 if dropdown_default_value.is_some() {
1528 widget_settings.dropdown = Some(FormFieldDropdownWidgetSettings {
1529 default_value: dropdown_default_value,
1530 });
1531 }
1532 let value = fallback_result_text
1533 .or(default_value)
1534 .or_else(|| options.first().map(|o| o.display_text.clone()));
1535 return Some(FormFieldRunNode {
1536 r#type: FormFieldRunNodeType::FormField,
1537 field_type: FormFieldType::Dropdown,
1538 source_kind: Some(FormFieldSourceKind::Legacy),
1539 id: None,
1540 tag: None,
1541 title: None,
1542 placeholder: None,
1543 checked: None,
1544 value,
1545 options: if options.is_empty() {
1546 None
1547 } else {
1548 Some(options)
1549 },
1550 widget: Some(widget_settings),
1551 checked_symbol: None,
1552 unchecked_symbol: None,
1553 style,
1554 link,
1555 source_xml: field_xml,
1556 source_preview: None,
1557 });
1558 }
1559 FormFieldType::Text => {
1560 let text_input_xml = ff_data_xml
1561 .and_then(|xml| {
1562 extract_balanced_tag_blocks(xml, "w:textInput")
1563 .into_iter()
1564 .next()
1565 })
1566 .or_else(|| {
1567 ff_data_xml.and_then(|xml| {
1568 re::get(r"(?i)<w:textInput\b[^>]*/?>")
1569 .and_then(|re| re.find(xml))
1570 .map(|m| m.as_str().to_string())
1571 })
1572 });
1573 let type_tag = text_input_xml.as_deref().and_then(|xml| {
1574 re::get(r"(?i)<w:type\b[^>]*/?>")
1575 .and_then(|re| re.find(xml))
1576 .map(|m| m.as_str().to_string())
1577 });
1578 let default_tag = text_input_xml.as_deref().and_then(|xml| {
1579 re::get(r"(?i)<w:default\b[^>]*/?>")
1580 .and_then(|re| re.find(xml))
1581 .map(|m| m.as_str().to_string())
1582 });
1583 let max_length_tag = text_input_xml.as_deref().and_then(|xml| {
1584 re::get(r"(?i)<w:maxLength\b[^>]*/?>")
1585 .and_then(|re| re.find(xml))
1586 .map(|m| m.as_str().to_string())
1587 });
1588 let format_tag = text_input_xml.as_deref().and_then(|xml| {
1589 re::get(r"(?i)<w:format\b[^>]*/?>")
1590 .and_then(|re| re.find(xml))
1591 .map(|m| m.as_str().to_string())
1592 });
1593 let input_type = type_tag
1594 .as_deref()
1595 .and_then(|tag| get_attribute(tag, "w:val"))
1596 .as_deref()
1597 .and_then(|value| decode_xml_attribute(Some(&value)));
1598 let default_text = default_tag
1599 .as_deref()
1600 .and_then(|tag| get_attribute(tag, "w:val"))
1601 .as_deref()
1602 .and_then(|value| decode_xml_attribute(Some(&value)));
1603 let max_length = max_length_tag
1604 .as_deref()
1605 .and_then(|tag| parse_integer_attribute(tag, "w:val"));
1606 let text_format = format_tag
1607 .as_deref()
1608 .and_then(|tag| get_attribute(tag, "w:val"))
1609 .as_deref()
1610 .and_then(|value| decode_xml_attribute(Some(&value)));
1611 let text_default = default_text.clone();
1612 if input_type.is_some()
1613 || text_default.is_some()
1614 || max_length.is_some()
1615 || text_format.is_some()
1616 {
1617 widget_settings.text = Some(FormFieldTextWidgetSettings {
1618 input_type,
1619 default_text: text_default,
1620 max_length,
1621 text_format,
1622 });
1623 }
1624 let value = fallback_result_text.or(default_text);
1625 return Some(FormFieldRunNode {
1626 r#type: FormFieldRunNodeType::FormField,
1627 field_type: FormFieldType::Text,
1628 source_kind: Some(FormFieldSourceKind::Legacy),
1629 id: None,
1630 tag: None,
1631 title: None,
1632 placeholder: None,
1633 checked: None,
1634 value,
1635 options: None,
1636 widget: Some(widget_settings),
1637 checked_symbol: None,
1638 unchecked_symbol: None,
1639 style,
1640 link,
1641 source_xml: field_xml,
1642 source_preview: None,
1643 });
1644 }
1645 FormFieldType::Date => None,
1646 }
1647}
1648
1649fn parse_legacy_paragraph_form_field_tokens(
1650 paragraph_xml: &str,
1651 runs: &[RunToken<'_>],
1652 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1653 paragraph_style_id: Option<&str>,
1654 table_run_style: Option<&TextStyle>,
1655) -> Vec<ParagraphFormFieldToken> {
1656 if runs.is_empty() {
1657 return Vec::new();
1658 }
1659 let mut tokens = Vec::new();
1660 let mut stack: Vec<(usize, Option<usize>, Vec<String>, Option<String>)> = Vec::new();
1661 let inline_token_pattern = re::get_unchecked(
1662 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>"#,
1663 );
1664
1665 for (run_index, run) in runs.iter().enumerate() {
1666 for caps in inline_token_pattern.captures_iter(&run.xml) {
1667 if let Some(chunk) = caps.get(4) {
1668 if let Some(current) = stack.last_mut() {
1669 current.2.push(chunk.as_str().to_string());
1670 }
1671 continue;
1672 }
1673 let field_type = caps
1674 .get(1)
1675 .or_else(|| caps.get(3))
1676 .map(|m| m.as_str().to_ascii_lowercase())
1677 .unwrap_or_default();
1678 if field_type == "begin" {
1679 let field_xml = caps.get(0).map(|m| m.as_str()).unwrap_or_default();
1680 let ff_data_xml = extract_balanced_tag_blocks(field_xml, "w:ffData")
1681 .into_iter()
1682 .next()
1683 .or_else(|| {
1684 re::get(r"(?i)<w:ffData\b[^>]*/?>")
1685 .and_then(|re| re.find(field_xml))
1686 .map(|m| m.as_str().to_string())
1687 });
1688 stack.push((run_index, None, Vec::new(), ff_data_xml));
1689 continue;
1690 }
1691 if field_type == "separate" {
1692 if let Some(current) = stack.last_mut() {
1693 if current.1.is_none() {
1694 current.1 = Some(run_index);
1695 }
1696 }
1697 continue;
1698 }
1699 if field_type == "end" {
1700 let Some(current) = stack.pop() else { continue };
1701 let field = parse_legacy_form_field_from_range(
1702 paragraph_xml,
1703 runs,
1704 context,
1705 paragraph_style_id,
1706 table_run_style,
1707 current.0,
1708 current.1,
1709 run_index,
1710 ¤t.2.join(" "),
1711 current.3.as_deref(),
1712 );
1713 let Some(field) = field else { continue };
1714 let start = runs.get(current.0).map(|r| r.start);
1715 let end = runs.get(run_index).map(|r| r.end);
1716 if let (Some(start), Some(end)) = (start, end) {
1717 if end > start {
1718 tokens.push(ParagraphFormFieldToken { start, end, field });
1719 }
1720 }
1721 }
1722 }
1723 }
1724 tokens
1725}
1726
1727fn trimmed_sdt_dropdown_style(
1728 runs: &[String],
1729 content_text: &str,
1730 fallback: Option<SharedTextStyle>,
1731 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1732 paragraph_style_id: Option<&str>,
1733 table_run_style: Option<&TextStyle>,
1734) -> Option<SharedTextStyle> {
1735 let Some(first_run) = runs.first() else {
1736 return fallback;
1737 };
1738 let first_text = parse_run_text(first_run);
1739 if !first_text.trim().is_empty() || content_text.trim().is_empty() {
1740 return fallback;
1741 }
1742
1743 let retained_start = content_text.len() - content_text.trim_start().len();
1744 let retained_end = content_text.trim_end().len();
1745 let mut cursor = 0;
1746 let mut retained_styles = Vec::new();
1747 for (index, run) in runs.iter().enumerate() {
1748 let text = if index == 0 {
1749 std::borrow::Cow::Borrowed(first_text.as_str())
1750 } else {
1751 std::borrow::Cow::Owned(parse_run_text(run))
1752 };
1753 if !content_text
1754 .get(cursor..)
1755 .is_some_and(|remaining| remaining.starts_with(text.as_ref()))
1756 {
1757 return fallback;
1758 }
1759 let end = cursor + text.len();
1760 if !text.is_empty() && cursor < retained_end && end > retained_start {
1761 retained_styles.push(parse_run_style_in_table(
1762 run,
1763 context,
1764 paragraph_style_id,
1765 table_run_style,
1766 ));
1767 }
1768 cursor = end;
1769 }
1770 if cursor != content_text.len() || retained_styles.is_empty() {
1771 return fallback;
1772 }
1773
1774 let Some(seed) = fallback
1775 .as_ref()
1776 .or_else(|| retained_styles.iter().flatten().next())
1777 else {
1778 return None;
1779 };
1780 let mut recovered = (**seed).clone();
1781 let mut changed = false;
1782 let mut has_any = false;
1783 macro_rules! recover_common_properties {
1784 ($($property:ident),+ $(,)?) => {
1785 $(
1786 let previous = fallback.as_ref().and_then(|style| style.$property.clone());
1787 let common = retained_styles[0].as_ref().and_then(|style| style.$property.as_ref());
1788 let value = if retained_styles.iter().skip(1).all(|style| {
1789 style.as_ref().and_then(|style| style.$property.as_ref()) == common
1790 }) {
1791 common.cloned()
1792 } else {
1793 previous.clone()
1794 };
1795 changed |= value != previous;
1796 has_any |= value.is_some();
1797 recovered.$property = value;
1798 )+
1799 };
1800 }
1801 recover_common_properties!(
1803 bold,
1804 italic,
1805 underline,
1806 strike,
1807 all_caps,
1808 small_caps,
1809 color,
1810 highlight,
1811 background_color,
1812 font_size_pt,
1813 font_size_cs_pt,
1814 font_family,
1815 source_font_family,
1816 font_family_ascii,
1817 font_family_h_ansi,
1818 font_family_east_asia,
1819 font_family_cs,
1820 font_theme_ascii,
1821 font_theme_h_ansi,
1822 font_theme_east_asia,
1823 font_theme_cs,
1824 resolved_font_family_ascii,
1825 resolved_font_family_h_ansi,
1826 resolved_font_family_east_asia,
1827 resolved_font_family_cs,
1828 font_hint,
1829 language,
1830 language_east_asia,
1831 language_bidi,
1832 right_to_left,
1833 complex_script,
1834 character_spacing_twips,
1835 character_scale_percent,
1836 source_character_scale,
1837 kerning_min_font_size_pt,
1838 ligatures,
1839 contextual_alternates,
1840 vertical_align,
1841 run_border,
1842 );
1843 if !changed {
1844 fallback
1845 } else if has_any {
1846 Some(recovered.into())
1847 } else {
1848 None
1849 }
1850}
1851
1852fn parse_form_field_from_sdt_xml(
1853 sdt_xml: &str,
1854 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
1855 paragraph_style_id: Option<&str>,
1856 table_run_style: Option<&TextStyle>,
1857 link: Option<String>,
1858) -> Option<FormFieldRunNode> {
1859 let sdt_properties_xml = extract_balanced_tag_blocks(sdt_xml, "w:sdtPr")
1860 .into_iter()
1861 .next()
1862 .or_else(|| {
1863 re::get(r"(?i)<w:sdtPr\b[^>]*/?>")
1864 .and_then(|re| re.find(sdt_xml))
1865 .map(|m| m.as_str().to_string())
1866 })
1867 .unwrap_or_default();
1868 let sdt_content_xml = extract_balanced_tag_blocks(sdt_xml, "w:sdtContent")
1869 .into_iter()
1870 .next()
1871 .unwrap_or_default();
1872 if sdt_properties_xml.is_empty() {
1873 return None;
1874 }
1875
1876 let alias_tag = re::get(r"(?i)<w:alias\b[^>]*/?>")
1877 .and_then(|re| re.find(&sdt_properties_xml))
1878 .map(|m| m.as_str().to_string());
1879 let tag_tag = re::get(r"(?i)<w:tag\b[^>]*/?>")
1880 .and_then(|re| re.find(&sdt_properties_xml))
1881 .map(|m| m.as_str().to_string());
1882 let id_tag = re::get(r"(?i)<w:id\b[^>]*/?>")
1883 .and_then(|re| re.find(&sdt_properties_xml))
1884 .map(|m| m.as_str().to_string());
1885 let placeholder_tag = re::get(r"(?is)<w:placeholder\b[\s\S]*?</w:placeholder>")
1886 .and_then(|re| re.find(&sdt_properties_xml))
1887 .map(|m| m.as_str().to_string());
1888 let placeholder_doc_part_tag = placeholder_tag.as_deref().and_then(|xml| {
1889 re::get(r"(?i)<w:docPart\b[^>]*/?>")
1890 .and_then(|re| re.find(xml))
1891 .map(|m| m.as_str().to_string())
1892 });
1893 let title = alias_tag
1894 .as_deref()
1895 .and_then(|tag| get_attribute(tag, "w:val"))
1896 .as_deref()
1897 .and_then(|value| decode_xml_attribute(Some(&value)));
1898 let tag = tag_tag
1899 .as_deref()
1900 .and_then(|t| get_attribute(t, "w:val"))
1901 .as_deref()
1902 .and_then(|value| decode_xml_attribute(Some(&value)));
1903 let id_value = id_tag
1904 .as_deref()
1905 .and_then(|tag| parse_integer_attribute(tag, "w:val"));
1906 let placeholder = placeholder_doc_part_tag
1907 .as_deref()
1908 .and_then(|tag| get_attribute(tag, "w:val"))
1909 .as_deref()
1910 .and_then(|value| decode_xml_attribute(Some(&value)));
1911 let content_runs = extract_balanced_tag_blocks(&sdt_content_xml, "w:r");
1912 let style = content_runs.first().and_then(|xml| {
1913 parse_run_style_in_table(xml, context, paragraph_style_id, table_run_style)
1914 });
1915 let content_text = parse_run_text(&sdt_content_xml);
1916 let trimmed_content_text = content_text.trim();
1917
1918 if re::get(r"(?i)<w14:checkbox\b").is_some_and(|re| re.is_match(&sdt_properties_xml)) {
1919 let checked_tag = re::get(r"(?i)<w14:checked\b[^>]*/?>")
1920 .and_then(|re| re.find(&sdt_properties_xml))
1921 .map(|m| m.as_str().to_string());
1922 let checked_value = checked_tag
1923 .as_deref()
1924 .and_then(|tag| get_attribute(tag, "w14:val").or_else(|| get_attribute(tag, "w:val")));
1925 let checked_state_tag = re::get(r"(?i)<w14:checkedState\b[^>]*/?>")
1926 .and_then(|re| re.find(&sdt_properties_xml))
1927 .map(|m| m.as_str().to_string());
1928 let unchecked_state_tag = re::get(r"(?i)<w14:uncheckedState\b[^>]*/?>")
1929 .and_then(|re| re.find(&sdt_properties_xml))
1930 .map(|m| m.as_str().to_string());
1931 let checked_symbol = checked_state_tag
1932 .as_deref()
1933 .and_then(|tag| {
1934 decode_hex_code_point(
1935 get_attribute(tag, "w14:val")
1936 .or_else(|| get_attribute(tag, "w:val"))
1937 .as_deref(),
1938 )
1939 })
1940 .unwrap_or_else(|| "☒".to_string());
1941 let unchecked_symbol = unchecked_state_tag
1942 .as_deref()
1943 .and_then(|tag| {
1944 decode_hex_code_point(
1945 get_attribute(tag, "w14:val")
1946 .or_else(|| get_attribute(tag, "w:val"))
1947 .as_deref(),
1948 )
1949 })
1950 .unwrap_or_else(|| "☐".to_string());
1951 let checked = on_off_value_to_boolean(checked_value.as_deref()).or_else(|| {
1952 if !trimmed_content_text.is_empty() {
1953 Some(trimmed_content_text.contains(&checked_symbol))
1954 } else {
1955 None
1956 }
1957 });
1958 return Some(FormFieldRunNode {
1959 r#type: FormFieldRunNodeType::FormField,
1960 field_type: FormFieldType::Checkbox,
1961 source_kind: Some(FormFieldSourceKind::Sdt),
1962 id: id_value,
1963 tag,
1964 title,
1965 placeholder,
1966 checked: Some(checked.unwrap_or(false)),
1967 value: None,
1968 options: None,
1969 widget: None,
1970 checked_symbol: Some(checked_symbol),
1971 unchecked_symbol: Some(unchecked_symbol),
1972 style,
1973 link,
1974 source_xml: Some(sdt_xml.to_string()),
1975 source_preview: None,
1976 });
1977 }
1978
1979 if re::get(r"(?i)<w:(?:dropDownList|comboBox)\b")
1980 .is_some_and(|re| re.is_match(&sdt_properties_xml))
1981 {
1982 let style = trimmed_sdt_dropdown_style(
1983 &content_runs,
1984 &content_text,
1985 style,
1986 context,
1987 paragraph_style_id,
1988 table_run_style,
1989 );
1990 let mut options = Vec::new();
1991 if let Some(re) = re::get(r"(?i)<w:listItem\b[^>]*/?>") {
1992 for mat in re.find_iter(&sdt_properties_xml) {
1993 let list_item_tag = mat.as_str();
1994 let display_text = get_attribute(list_item_tag, "w:displayText")
1995 .and_then(|value| decode_xml_attribute(Some(&value)));
1996 let value = get_attribute(list_item_tag, "w:value")
1997 .and_then(|value| decode_xml_attribute(Some(&value)));
1998 let Some(fallback_text) = display_text
1999 .clone()
2000 .or(value.clone())
2001 .map(|t| t.trim().to_string())
2002 .filter(|t| !t.is_empty())
2003 else {
2004 continue;
2005 };
2006 options.push(FormFieldOption {
2007 display_text: fallback_text,
2008 value: value
2009 .map(|v| v.trim().to_string())
2010 .filter(|v| !v.is_empty()),
2011 });
2012 }
2013 }
2014 let last_value_tag = re::get(r"(?i)<w:lastValue\b[^>]*/?>")
2015 .and_then(|re| re.find(&sdt_properties_xml))
2016 .map(|m| m.as_str().to_string());
2017 let last_value = last_value_tag
2018 .as_deref()
2019 .and_then(|tag| get_attribute(tag, "w:val"))
2020 .and_then(|value| decode_xml_attribute(Some(&value)))
2021 .map(|v| v.trim().to_string())
2022 .filter(|v| !v.is_empty());
2023 let selected_value = if trimmed_content_text.is_empty() {
2024 last_value
2025 } else {
2026 Some(trimmed_content_text.to_string())
2027 };
2028 return Some(FormFieldRunNode {
2029 r#type: FormFieldRunNodeType::FormField,
2030 field_type: FormFieldType::Dropdown,
2031 source_kind: Some(FormFieldSourceKind::Sdt),
2032 id: id_value,
2033 tag,
2034 title,
2035 placeholder,
2036 checked: None,
2037 value: selected_value,
2038 options: if options.is_empty() {
2039 None
2040 } else {
2041 Some(options)
2042 },
2043 widget: None,
2044 checked_symbol: None,
2045 unchecked_symbol: None,
2046 style,
2047 link,
2048 source_xml: Some(sdt_xml.to_string()),
2049 source_preview: None,
2050 });
2051 }
2052
2053 if re::get(r"(?i)<w:date\b").is_some_and(|re| re.is_match(&sdt_properties_xml)) {
2054 let full_date_tag = re::get(r"(?i)<w:fullDate\b[^>]*/?>")
2055 .and_then(|re| re.find(&sdt_properties_xml))
2056 .map(|m| m.as_str().to_string());
2057 let full_date = full_date_tag
2058 .as_deref()
2059 .and_then(|tag| get_attribute(tag, "w:val"))
2060 .and_then(|value| decode_xml_attribute(Some(&value)))
2061 .map(|v| v.trim().to_string())
2062 .filter(|v| !v.is_empty());
2063 let value = if trimmed_content_text.is_empty() {
2064 full_date
2065 } else {
2066 Some(content_text)
2067 };
2068 return Some(FormFieldRunNode {
2069 r#type: FormFieldRunNodeType::FormField,
2070 field_type: FormFieldType::Date,
2071 source_kind: Some(FormFieldSourceKind::Sdt),
2072 id: id_value,
2073 tag,
2074 title,
2075 placeholder,
2076 checked: None,
2077 value,
2078 options: None,
2079 widget: None,
2080 checked_symbol: None,
2081 unchecked_symbol: None,
2082 style,
2083 link,
2084 source_xml: Some(sdt_xml.to_string()),
2085 source_preview: None,
2086 });
2087 }
2088
2089 if re::get(r"(?i)<w:(?:text|richText)\b").is_some_and(|re| re.is_match(&sdt_properties_xml)) {
2090 return Some(FormFieldRunNode {
2091 r#type: FormFieldRunNodeType::FormField,
2092 field_type: FormFieldType::Text,
2093 source_kind: Some(FormFieldSourceKind::Sdt),
2094 id: id_value,
2095 tag,
2096 title,
2097 placeholder,
2098 checked: None,
2099 value: Some(content_text),
2100 options: None,
2101 widget: None,
2102 checked_symbol: None,
2103 unchecked_symbol: None,
2104 style,
2105 link,
2106 source_xml: Some(sdt_xml.to_string()),
2107 source_preview: None,
2108 });
2109 }
2110
2111 None
2112}
2113
2114pub fn parse_paragraph_form_field_tokens(
2115 paragraph_xml: &str,
2116 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2117 paragraph_style_id: Option<&str>,
2118 runs: Option<&[ParagraphRunToken]>,
2119) -> Vec<ParagraphFormFieldToken> {
2120 let borrowed = runs.map(|runs| {
2121 runs.iter()
2122 .map(|run| RunToken {
2123 xml: std::borrow::Cow::Borrowed(run.xml.as_str()),
2124 start: run.start,
2125 end: run.end,
2126 link: run.link.clone(),
2127 text_links: run.text_links.clone(),
2128 empty_fields: Vec::new(),
2129 })
2130 .collect::<Vec<_>>()
2131 });
2132 parse_paragraph_form_field_tokens_in_table(
2133 paragraph_xml,
2134 context,
2135 paragraph_style_id,
2136 borrowed.as_deref(),
2137 None,
2138 )
2139}
2140
2141fn parse_paragraph_form_field_tokens_in_table(
2142 paragraph_xml: &str,
2143 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2144 paragraph_style_id: Option<&str>,
2145 runs: Option<&[RunToken<'_>]>,
2146 table_run_style: Option<&TextStyle>,
2147) -> Vec<ParagraphFormFieldToken> {
2148 let has_legacy_fields = super::scan::contains_tag(paragraph_xml, "w:fldChar");
2149 if !has_legacy_fields && !super::scan::contains_tag(paragraph_xml, "w:sdt") {
2150 return Vec::new();
2151 }
2152 let legacy_tokens = if has_legacy_fields
2153 || crate::xml::find_ignore_ascii_case(paragraph_xml, 0, "<w:fldChar").is_some()
2154 {
2155 let paragraph_runs: Vec<RunToken<'_>> = runs
2156 .map(|r| r.to_vec())
2157 .unwrap_or_else(|| paragraph_runs(paragraph_xml, context));
2158 let story_runs: Vec<_> = paragraph_runs
2159 .iter()
2160 .map(|run| RunToken {
2161 xml: std::borrow::Cow::Owned(strip_text_box_content(&run.xml)),
2162 ..run.clone()
2163 })
2164 .collect();
2165 parse_legacy_paragraph_form_field_tokens(
2166 paragraph_xml,
2167 &story_runs,
2168 context,
2169 paragraph_style_id,
2170 table_run_style,
2171 )
2172 } else {
2173 Vec::new()
2174 };
2175 let text_box_ranges = extract_balanced_tag_ranges(paragraph_xml, "w:txbxContent");
2176 let sdt_ranges: Vec<_> = extract_balanced_tag_ranges(paragraph_xml, "w:sdt")
2177 .into_iter()
2178 .filter(|range| {
2179 !text_box_ranges
2180 .iter()
2181 .any(|story| range.start >= story.start && range.end <= story.end)
2182 })
2183 .collect();
2184 if sdt_ranges.is_empty() {
2185 return legacy_tokens;
2186 }
2187
2188 let hyperlink_ranges: Vec<_> = paragraph_nested_tag_ranges(paragraph_xml, "w:hyperlink")
2189 .into_iter()
2190 .map(|range| {
2191 let hyperlink_xml = ¶graph_xml[range.start..range.end];
2192 let hyperlink_tag = re::get(r"(?i)<w:hyperlink\b[^>]*>")
2193 .and_then(|re| re.find(hyperlink_xml))
2194 .map(|m| m.as_str().to_string())
2195 .unwrap_or_default();
2196 (range, hyperlink_href_from_tag(&hyperlink_tag, context))
2197 })
2198 .collect();
2199
2200 let mut sdt_tokens = Vec::new();
2201 for range in sdt_ranges {
2202 let sdt_xml = ¶graph_xml[range.start..range.end];
2203 let link = hyperlink_ranges
2204 .iter()
2205 .rev()
2206 .find(|(hyperlink_range, _)| {
2207 range.start >= hyperlink_range.start && range.end <= hyperlink_range.end
2208 })
2209 .and_then(|(_, href)| href.clone());
2210 if let Some(field) = parse_form_field_from_sdt_xml(
2211 sdt_xml,
2212 context,
2213 paragraph_style_id,
2214 table_run_style,
2215 link,
2216 ) {
2217 sdt_tokens.push(ParagraphFormFieldToken {
2218 start: range.start,
2219 end: range.end,
2220 field,
2221 });
2222 }
2223 }
2224
2225 let mut combined = legacy_tokens;
2226 combined.extend(sdt_tokens);
2227 combined.sort_by_key(|token| token.start);
2228 combined
2229}
2230
2231fn parse_paragraph_mark_formatting(
2232 current_mark: Option<&str>,
2233 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2234 table_run_style: Option<&TextStyle>,
2235) -> crate::model::ParagraphMarkFormattingSource {
2236 let current = current_mark.unwrap_or("<w:rPr/>");
2237 let character_style = current_mark.and_then(|xml| super::styles::direct_table_property(xml, "w:rStyle"))
2238 .and_then(|tag| get_attribute(tag, "w:val"))
2239 .and_then(|id| context.styles().run_style_by_id.get(&id).cloned());
2240 crate::model::ParagraphMarkFormattingSource {
2241 default_style: super::style::cached_mark_default_style(
2242 context.styles().default_run_style.as_ref(),
2243 table_run_style,
2244 ),
2245 character_style: character_style.map(Into::into),
2246 direct_style: super::style::parse_shared_text_style_from_xml(
2247 current,
2248 &context.styles().theme_fonts,
2249 )
2250 .map(Into::into),
2251 }
2252}
2253
2254pub fn parse_paragraph(
2255 paragraph_xml: &str,
2256 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2257) -> ParagraphNode {
2258 parse_paragraph_in_table(paragraph_xml, context, None)
2259}
2260
2261pub fn parse_paragraph_in_table(
2262 paragraph_xml: &str,
2263 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2264 table_paragraph_spacing: Option<&crate::model::ParagraphSpacing>,
2265) -> ParagraphNode {
2266 parse_paragraph_with_table_run_style(paragraph_xml, context, table_paragraph_spacing, None)
2267}
2268
2269pub(crate) fn parse_paragraph_with_table_run_style(
2270 paragraph_xml: &str,
2271 context: &ParseContext<'_, impl std::borrow::Borrow<super::context::ParsedStyleSheet>>,
2272 table_paragraph_spacing: Option<&crate::model::ParagraphSpacing>,
2273 table_run_style: Option<&TextStyle>,
2274) -> ParagraphNode {
2275 let mut children: Vec<ParagraphChildNode> = Vec::new();
2276 let paragraph_properties_xml =
2277 super::styles::direct_table_property(paragraph_xml, "w:pPr").unwrap_or_default();
2278 let paragraph_style = super::style::parse_paragraph_style_from_properties(
2279 paragraph_properties_xml,
2280 context,
2281 table_paragraph_spacing,
2282 );
2283 let source_paragraph_mark_properties_xml =
2284 crate::xml::direct_child_tag(paragraph_properties_xml, "w:rPr");
2285 let effective_mark_properties = source_paragraph_mark_properties_xml
2286 .map(|xml| crate::xml::without_direct_child_tags(xml, "w:rPrChange"));
2287 let paragraph_mark_style = super::style::parse_mark_run_style(
2288 effective_mark_properties.as_deref().unwrap_or_default(),
2289 context,
2290 paragraph_style.as_ref().and_then(|style| style.style_id.as_deref()),
2291 table_run_style,
2292 );
2293 let source_paragraph_mark_formatting = parse_paragraph_mark_formatting(
2294 effective_mark_properties.as_deref(),
2295 context,
2296 table_run_style,
2297 );
2298 let paragraph_mark_deleted = source_paragraph_mark_properties_xml
2299 .as_deref()
2300 .is_some_and(|xml| super::styles::direct_table_property(xml, "w:del").is_some());
2301 if let Some(runs) = indexed_plain_paragraph(paragraph_xml) {
2302 children.reserve(runs.iter().map(|run| run.run.tokens.len()).sum());
2303 for IndexedPlainParagraphRun { xml, run } in runs {
2304 let mut style = super::style::parse_run_style_from_properties(
2305 xml,
2306 run.properties,
2307 run.scripts,
2308 context,
2309 paragraph_style
2310 .as_ref()
2311 .and_then(|style| style.style_id.as_deref()),
2312 table_run_style,
2313 );
2314 let mut tokens = run
2315 .tokens
2316 .into_iter()
2317 .filter(|token| !token.text.is_empty())
2318 .peekable();
2319 while let Some(token) = tokens.next() {
2320 children.push(ParagraphChildNode::Text(TextRunNode {
2321 r#type: TextRunNodeType::Text,
2322 text: token.text,
2323 style: if tokens.peek().is_none() {
2324 style.take()
2325 } else {
2326 style.clone()
2327 },
2328 link: None,
2329 note_reference: None,
2330 field_instruction: None,
2331 }));
2332 }
2333 }
2334 } else {
2335 let runs = paragraph_runs(paragraph_xml, context);
2336 children.reserve(runs.len());
2337 let form_field_tokens = parse_paragraph_form_field_tokens_in_table(
2338 paragraph_xml,
2339 context,
2340 paragraph_style.as_ref().and_then(|s| s.style_id.as_deref()),
2341 Some(&runs),
2342 table_run_style,
2343 );
2344
2345 enum ContentToken<'a> {
2346 Run {
2347 start: usize,
2348 token: &'a RunToken<'a>,
2349 },
2350 FormField {
2351 start: usize,
2352 token: &'a ParagraphFormFieldToken,
2353 },
2354 Math {
2355 start: usize,
2356 xml: &'a str,
2357 },
2358 }
2359
2360 let mut content_tokens: Vec<ContentToken<'_>> =
2361 Vec::with_capacity(runs.len() + form_field_tokens.len());
2362 let mut form_field_token_cursor = 0usize;
2363 for run in &runs {
2364 while form_field_token_cursor < form_field_tokens.len()
2365 && form_field_tokens[form_field_token_cursor].end <= run.start
2366 {
2367 form_field_token_cursor += 1;
2368 }
2369 let current_form_field_token = form_field_tokens.get(form_field_token_cursor);
2370 let inside_form_field = current_form_field_token
2371 .is_some_and(|token| run.start >= token.start && run.end <= token.end);
2372 if inside_form_field {
2373 continue;
2374 }
2375 content_tokens.push(ContentToken::Run {
2376 start: run.start,
2377 token: run,
2378 });
2379 }
2380 for form_field_token in &form_field_tokens {
2381 content_tokens.push(ContentToken::FormField {
2382 start: form_field_token.start,
2383 token: form_field_token,
2384 });
2385 }
2386 let text_box_ranges = extract_balanced_tag_ranges(paragraph_xml, "w:txbxContent");
2387 for range in extract_balanced_tag_ranges(paragraph_xml, "m:oMath") {
2388 if !text_box_ranges
2389 .iter()
2390 .any(|story| range.start >= story.start && range.end <= story.end)
2391 {
2392 content_tokens.push(ContentToken::Math {
2393 start: range.start,
2394 xml: ¶graph_xml[range.start..range.end],
2395 });
2396 }
2397 }
2398 content_tokens.sort_by_key(|token| match token {
2399 ContentToken::Run { start, .. } => *start,
2400 ContentToken::FormField { start, .. } => *start,
2401 ContentToken::Math { start, .. } => *start,
2402 });
2403
2404 for content_token in content_tokens {
2405 match content_token {
2406 ContentToken::Math { xml, .. } => {
2407 if let Some(math) = super::math::parse_math(xml) {
2408 children.push(ParagraphChildNode::Image(Box::new(math)));
2409 } else {
2410 context.push_warning(
2411 "Unable to interpret an equation; source markup is preserved",
2412 );
2413 }
2414 }
2415 ContentToken::FormField { token, .. } => {
2416 children.push(ParagraphChildNode::FormField(Box::new(token.field.clone())));
2417 }
2418 ContentToken::Run { token: run, .. } => {
2419 let plain = indexed_plain_run(&run.xml);
2420 let style = if let Some(plain) = &plain {
2421 super::style::parse_run_style_from_properties(
2422 &run.xml,
2423 plain.properties,
2424 plain.scripts,
2425 context,
2426 paragraph_style.as_ref().and_then(|s| s.style_id.as_deref()),
2427 table_run_style,
2428 )
2429 } else {
2430 parse_run_style_in_table(
2431 &run.xml,
2432 context,
2433 paragraph_style.as_ref().and_then(|s| s.style_id.as_deref()),
2434 table_run_style,
2435 )
2436 };
2437 if let Some(active_x_field) = (plain.is_none()
2438 && super::scan::contains_tag(&run.xml, "w:object"))
2439 .then(|| {
2440 parse_run_active_x_checkbox_field_shared(
2441 &run.xml,
2442 context,
2443 style.clone(),
2444 run.link.clone(),
2445 )
2446 })
2447 .flatten()
2448 {
2449 children.push(ParagraphChildNode::FormField(Box::new(active_x_field)));
2450 continue;
2451 }
2452 let images = if plain.is_some() {
2453 Vec::new()
2454 } else {
2455 parse_run_image_tokens(&run.xml, context)
2456 };
2457 let parsed_tokens = if let Some(plain) = plain {
2460 plain.tokens
2461 } else {
2462 let run_xml_for_text = run_text_xml(&run.xml);
2463 match &run_xml_for_text {
2464 std::borrow::Cow::Borrowed(xml) => parse_active_run_text_tokens(xml),
2465 std::borrow::Cow::Owned(xml) => parse_run_text_tokens(xml),
2466 }
2467 };
2468 let mut parsed_tokens: Vec<_> = parsed_tokens
2469 .into_iter()
2470 .filter(|token| !token.text.is_empty() || token.note_reference.is_some())
2471 .map(|token| (token, None))
2472 .collect();
2473 parsed_tokens.extend(run.empty_fields.iter().map(
2474 |(source_start, instruction)| {
2475 (
2476 ParsedRunTextToken {
2477 text: String::new(),
2478 source_start: *source_start,
2479 note_reference: None,
2480 },
2481 Some(instruction.clone()),
2482 )
2483 },
2484 ));
2485 parsed_tokens.sort_by_key(|(token, _)| token.source_start);
2486 let mut parsed_tokens = parsed_tokens.into_iter().peekable();
2487 let mut remaining_style = style;
2488 let needs_child_ordering = !images.is_empty();
2489 let mut ordered_children = Vec::new();
2490 while let Some((token, field_instruction)) = parsed_tokens.next() {
2491 let child = ParagraphChildNode::Text(TextRunNode {
2492 r#type: TextRunNodeType::Text,
2493 text: token.text,
2494 style: if parsed_tokens.peek().is_none() {
2495 remaining_style.take()
2496 } else {
2497 remaining_style.clone()
2498 },
2499 link: run
2500 .text_links
2501 .iter()
2502 .rev()
2503 .find(|(offset, _)| *offset <= token.source_start)
2504 .and_then(|(_, link)| link.clone()),
2505 note_reference: token.note_reference,
2506 field_instruction,
2507 });
2508 if needs_child_ordering {
2509 ordered_children.push((token.source_start, child));
2510 } else {
2511 children.push(child);
2512 }
2513 }
2514 for (source_start, mut image) in images {
2515 if let Some(xml) = image.source_xml.as_deref() {
2516 let active_xml = prefer_alternate_content_choice(xml);
2517 let text_boxes =
2518 extract_balanced_tag_blocks(&active_xml, "w:txbxContent");
2519 if !text_boxes.is_empty() {
2520 image.content_nodes = Some(
2521 text_boxes
2522 .iter()
2523 .flat_map(|xml| {
2524 super::document::parse_document_xml(xml, context)
2525 })
2526 .collect(),
2527 );
2528 image.semantic_kind = Some("text_box".into());
2529 }
2530 }
2531 ordered_children
2532 .push((source_start, ParagraphChildNode::Image(Box::new(image))));
2533 }
2534 if needs_child_ordering {
2535 ordered_children.sort_by_key(|(offset, _)| *offset);
2536 children.extend(ordered_children.into_iter().map(|(_, child)| child));
2537 }
2538 }
2539 }
2540 }
2541 }
2542
2543 if children.is_empty() {
2544 children.push(ParagraphChildNode::Text(TextRunNode {
2548 r#type: TextRunNodeType::Text,
2549 text: String::new(),
2550 style: paragraph_mark_style.clone(),
2551 link: None,
2552 note_reference: None,
2553 field_instruction: None,
2554 }));
2555 }
2556
2557 let source_run_provenance = children
2558 .iter()
2559 .map(|child| match child {
2560 ParagraphChildNode::Text(run) => Some(ParagraphSourceTextPatchRun {
2561 style: run.style.clone(),
2562 link: run.link.clone(),
2563 note_reference: run.note_reference.clone(),
2564 }),
2565 _ => None,
2566 })
2567 .collect::<Option<Vec<_>>>()
2568 .map(|runs| ParagraphSourceTextPatch { runs });
2569
2570 let review_content = crate::revisions::removed_fragments(paragraph_xml)
2571 .into_iter()
2572 .map(|source| {
2573 let visible = crate::revisions::visible_removed_xml(source);
2574 let xml = format!("<w:p>{paragraph_properties_xml}{visible}</w:p>");
2575 let content = parse_paragraph_with_table_run_style(
2576 &xml, context, table_paragraph_spacing, table_run_style,
2577 );
2578 crate::revisions::ReviewContent { source_xml: source.to_owned(), children: content.children }
2579 })
2580 .collect::<Vec<_>>();
2581 ParagraphNode {
2582 review_content: (!review_content.is_empty()).then_some(review_content),
2583 r#type: ParagraphNodeType::Paragraph,
2584 block_id: None,
2585 style: paragraph_style,
2586 paragraph_mark_style: paragraph_mark_style.clone(),
2587 source_paragraph_mark_style: paragraph_mark_style,
2588 source_paragraph_mark_formatting: Some(source_paragraph_mark_formatting),
2589 source_paragraph_mark_properties_xml: source_paragraph_mark_properties_xml
2590 .map(str::to_owned),
2591 paragraph_mark_deleted: if paragraph_mark_deleted {
2592 Some(true)
2593 } else {
2594 None
2595 },
2596 children,
2597 source_xml: Some(paragraph_xml.to_string()),
2598 source_text_patch: None,
2599 source_run_provenance,
2600 }
2601}