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