1use bumpalo::Bump;
2use std::mem::take;
3
4use crate::{
5 InlineMacro, InlineNode, LineBreak, Location, ParseInlineResult, Pass, PassthroughKind, Plain,
6 ProcessedContent, Raw, Substitution,
7 model::substitution::{
8 SubstitutionPlan, character_reference_end, escape_attribute_characters,
9 is_restorable_character_reference, resolve_passthrough_substitutions,
10 },
11 parsed::OwnedInput,
12};
13
14use super::{
15 ParserState,
16 inlines::inline_parser,
17 location_mapping::clamp_inline_node_locations,
18 state::{InlineContext, InlineRules},
19 utf8_utils::{RoundDirection, step_char},
20};
21
22const QUOTES_THEN_MACROS: &[Substitution] = &[Substitution::Quotes, Substitution::Macros];
23
24pub(crate) fn process_attribute_placeholder<'a>(
27 index: usize,
28 start: usize,
29 end: usize,
30 state: &ParserState<'a>,
31) -> Vec<InlineNode<'a>> {
32 let Some(mut pass) = state.attribute_passthroughs.get(index).cloned() else {
33 return Vec::new();
34 };
35 pass.location = state.create_location(start, end);
36 process_passthrough(
37 pass.text.unwrap_or_default(),
38 &pass,
39 state,
40 state.inline_ctx.substitutions,
41 )
42}
43
44fn process_passthrough<'a>(
46 content: &'a str,
47 passthrough: &Pass<'a>,
48 state: &ParserState<'a>,
49 attribute_substitutions: SubstitutionPlan,
50) -> Vec<InlineNode<'a>> {
51 let raw = Raw {
52 content,
53 location: passthrough_content_location(passthrough, content, state),
54 subs: Vec::new(),
55 };
56 if !passthrough.attribute_fragments.is_empty() {
57 let mut nodes = Vec::new();
58 let remaining = attribute_substitutions.after(&Substitution::Attributes);
59 let mut cursor = 0;
60 for fragment in &passthrough.attribute_fragments {
61 if cursor < fragment.range.start {
62 nodes.extend(process_raw_substitutions(
63 Raw {
64 content: &content[cursor..fragment.range.start],
65 ..raw.clone()
66 },
67 &remaining,
68 state,
69 ));
70 }
71 nodes.extend(process_raw_substitutions(
72 Raw {
73 content: &content[fragment.range.clone()],
74 ..raw.clone()
75 },
76 &fragment.substitutions,
77 state,
78 ));
79 cursor = fragment.range.end;
80 }
81 if cursor < content.len() {
82 nodes.extend(process_raw_substitutions(
83 Raw {
84 content: &content[cursor..],
85 ..raw.clone()
86 },
87 &remaining,
88 state,
89 ));
90 }
91 for node in &mut nodes {
94 super::location_walk::walk_inline_locations_mut(node, &mut |location| {
95 *location = raw.location.clone();
96 });
97 }
98 return nodes;
99 }
100 let substitutions = resolve_passthrough_substitutions(&passthrough.substitutions);
101 if passthrough.kind != PassthroughKind::Macro || !content.contains(r"\]") {
102 return process_raw_substitutions(raw, &substitutions, state);
103 }
104
105 let mut fragments = Vec::new();
108 let mut cursor = 0;
109 for (start, _) in content.match_indices(r"\]") {
110 push_raw_segment(&mut fragments, &raw, cursor, start, Vec::new(), state);
111 fragments.push(InlineNode::RawText(Raw {
112 content: &content[start + 1..start + 2],
113 location: raw_segment_location(&raw, start, start + 2, state),
114 subs: Vec::new(),
115 }));
116 cursor = start + 2;
117 }
118 push_raw_segment(
119 &mut fragments,
120 &raw,
121 cursor,
122 content.len(),
123 Vec::new(),
124 state,
125 );
126 process_inline_nodes(fragments, &substitutions, state)
127}
128
129fn passthrough_content_location(
130 passthrough: &Pass<'_>,
131 content: &str,
132 state: &ParserState<'_>,
133) -> Location {
134 let delimiter_len = match passthrough.kind {
136 PassthroughKind::Macro | PassthroughKind::Single => 1,
137 PassthroughKind::Double => 2,
138 PassthroughKind::Triple => 3,
139 PassthroughKind::AttributeRef => {
140 return state.create_block_location(
141 passthrough.location.absolute_start,
142 passthrough.location.absolute_end,
143 0,
144 );
145 }
146 };
147 let total_len = passthrough.location.absolute_end - passthrough.location.absolute_start;
148 let prefix_len = total_len.saturating_sub(content.len() + delimiter_len);
149 let absolute_start = passthrough.location.absolute_start + prefix_len;
150 state.create_block_location(absolute_start, absolute_start + content.len(), 0)
151}
152
153fn process_raw_substitutions<'a>(
154 mut raw: Raw<'a>,
155 substitutions: &[Substitution],
156 state: &ParserState<'a>,
157) -> Vec<InlineNode<'a>> {
158 let Some((substitution, remaining)) = substitutions.split_first() else {
159 return vec![InlineNode::RawText(raw)];
160 };
161
162 if let [Substitution::Quotes, Substitution::Macros, remaining @ ..] = substitutions {
163 let parsed = parse_raw_stage(
164 raw,
165 SubstitutionPlan::from_substitutions(QUOTES_THEN_MACROS),
166 state,
167 );
168 return process_inline_nodes(parsed, remaining, state);
169 }
170
171 match substitution {
172 Substitution::SpecialChars => {
173 if raw.subs.last() == Some(&Substitution::SpecialChars) {
174 raw.content = state.intern_str(&escape_attribute_characters(raw.content));
177 } else {
178 raw.subs.push(substitution.clone());
179 }
180 process_raw_substitutions(raw, remaining, state)
181 }
182 Substitution::Replacements => {
183 if let Some(fragments) = restore_raw_character_references(&raw, state) {
184 return process_inline_nodes(fragments, remaining, state);
185 }
186 if !raw.subs.contains(&Substitution::Replacements) {
187 raw.subs.push(substitution.clone());
188 }
189 process_raw_substitutions(raw, remaining, state)
190 }
191 Substitution::Attributes => {
192 process_inline_nodes(expand_raw_attributes(&raw, state), remaining, state)
193 }
194 Substitution::Quotes | Substitution::Macros | Substitution::PostReplacements => {
195 process_inline_nodes(
196 parse_raw_substitution(raw, substitution, state),
197 remaining,
198 state,
199 )
200 }
201 Substitution::Callouts | Substitution::Normal | Substitution::Verbatim => {
204 process_raw_substitutions(raw, remaining, state)
205 }
206 }
207}
208
209fn restore_raw_character_references<'a>(
210 raw: &Raw<'a>,
211 state: &ParserState<'a>,
212) -> Option<Vec<InlineNode<'a>>> {
213 if !raw.content.contains('&') {
214 return None;
215 }
216 let mut fragments = Vec::new();
217 let mut cursor = 0;
218 let mut subs = raw.subs.clone();
219 if !subs.contains(&Substitution::Replacements) {
220 subs.push(Substitution::Replacements);
221 }
222 for (amp, _) in raw.content.match_indices('&') {
223 if amp < cursor {
224 continue;
225 }
226 let Some(length) = character_reference_end(&raw.content[amp..]) else {
227 continue;
228 };
229 let end = amp + length;
230 let encoded_arrow = match &raw.content[amp..end] {
233 ">" => amp > 0 && matches!(raw.content.as_bytes().get(amp - 1), Some(b'-' | b'=')),
234 "<" => {
235 matches!(raw.content.as_bytes().get(end), Some(b'-' | b'='))
236 || raw.content[end..].starts_with("\\->")
237 || raw.content[end..].starts_with("\\=>")
238 }
239 _ => false,
240 };
241 if encoded_arrow && !raw.subs.contains(&Substitution::SpecialChars) {
242 continue;
243 }
244 let mut start = if amp > cursor && raw.content.as_bytes().get(amp - 1) == Some(&b'\\') {
245 amp - 1
246 } else {
247 amp
248 };
249 let mut prepared = raw.content[start..end].to_owned();
250 for substitution in &raw.subs {
251 if *substitution == Substitution::SpecialChars {
252 prepared = escape_attribute_characters(&prepared);
253 }
254 }
255 let escaped = prepared.starts_with('\\');
256 let reference = prepared.strip_prefix('\\').unwrap_or(&prepared);
257 if let Some((body, _)) = reference
258 .strip_prefix("&")
259 .and_then(|rest| rest.split_once(';'))
260 && is_restorable_character_reference(body)
261 {
262 prepared = if escaped {
263 reference.to_owned()
264 } else {
265 format!("&{}", &reference[5..])
266 };
267 }
268
269 if prepared.starts_with('\\') {
270 start = amp;
271 prepared.remove(0);
272 }
273
274 let mut reference_subs = Vec::new();
277 for (encoded, decoded) in [("&", "&"), ("<", "<"), (">", ">")] {
278 if let Some(rest) = prepared.strip_prefix(encoded) {
279 prepared = format!("{decoded}{rest}");
280 reference_subs.push(Substitution::SpecialChars);
281 break;
282 }
283 }
284 push_raw_segment(&mut fragments, raw, cursor, start, subs.clone(), state);
285 fragments.push(InlineNode::RawText(Raw {
286 content: state.intern_str(&prepared),
287 location: raw_segment_location(raw, start, end, state),
288 subs: reference_subs,
289 }));
290 cursor = end;
291 }
292 if fragments.is_empty() {
293 return None;
294 }
295 push_raw_segment(&mut fragments, raw, cursor, raw.content.len(), subs, state);
296 Some(fragments)
297}
298
299fn process_inline_nodes<'a>(
300 nodes: Vec<InlineNode<'a>>,
301 substitutions: &[Substitution],
302 state: &ParserState<'a>,
303) -> Vec<InlineNode<'a>> {
304 let Some((substitution, remaining)) = substitutions.split_first() else {
305 return nodes;
306 };
307
308 if matches!(substitution, Substitution::PostReplacements) {
309 let nodes = process_post_replacements(nodes, state);
310 return process_inline_nodes(nodes, remaining, state);
311 }
312
313 if matches!(substitution, Substitution::Quotes | Substitution::Macros) {
314 let (stages, remaining) =
315 if let [Substitution::Quotes, Substitution::Macros, remaining @ ..] = substitutions {
316 (QUOTES_THEN_MACROS, remaining)
317 } else {
318 (std::slice::from_ref(substitution), remaining)
319 };
320 let plan = SubstitutionPlan::from_substitutions(stages);
321 let mut result = Vec::with_capacity(nodes.len());
322 let mut fragments = Vec::new();
323 for mut node in nodes {
324 if let InlineNode::RawText(raw) = node {
325 fragments.push(raw);
326 } else {
327 result.extend(parse_raw_fragments(take(&mut fragments), plan, state));
328 process_inline_children(&mut node, stages, state);
329 result.push(node);
330 }
331 }
332 result.extend(parse_raw_fragments(fragments, plan, state));
333 return process_inline_nodes(result, remaining, state);
334 }
335
336 let mut result = Vec::with_capacity(nodes.len());
338 for mut node in nodes {
339 if let InlineNode::RawText(raw) = node {
340 result.extend(process_raw_substitutions(
341 raw,
342 std::slice::from_ref(substitution),
343 state,
344 ));
345 continue;
346 }
347 process_inline_children(&mut node, std::slice::from_ref(substitution), state);
348 result.push(node);
349 }
350 process_inline_nodes(result, remaining, state)
351}
352
353fn process_post_replacements<'a>(
354 nodes: Vec<InlineNode<'a>>,
355 state: &ParserState<'a>,
356) -> Vec<InlineNode<'a>> {
357 let mut staged = Vec::with_capacity(nodes.len());
358 for mut node in nodes {
359 if let InlineNode::RawText(raw) = node {
360 staged.extend(parse_raw_substitution(
361 raw,
362 &Substitution::PostReplacements,
363 state,
364 ));
365 } else {
366 process_inline_children(&mut node, &[Substitution::PostReplacements], state);
367 staged.push(node);
368 }
369 }
370 replace_cross_boundary_hardbreaks(staged, state)
371}
372
373fn replace_cross_boundary_hardbreaks<'a>(
374 nodes: Vec<InlineNode<'a>>,
375 state: &ParserState<'a>,
376) -> Vec<InlineNode<'a>> {
377 let mut result = Vec::with_capacity(nodes.len());
378 let mut pending = None;
379
380 for node in nodes {
381 let InlineNode::RawText(right) = node else {
382 if let Some(left) = pending.take() {
383 result.push(InlineNode::RawText(left));
384 }
385 result.push(node);
386 continue;
387 };
388
389 let Some(left) = pending.take() else {
390 pending = Some(right);
391 continue;
392 };
393
394 if is_cross_boundary_hardbreak(&result, &left, &right) {
395 let location = cross_boundary_hardbreak_location(&result, &left, state);
396 remove_cross_boundary_hardbreak_marker(&mut result, left, state);
397 result.push(InlineNode::LineBreak(LineBreak { location }));
398 pending = raw_without_first_byte(right, state);
399 } else {
400 result.push(InlineNode::RawText(left));
401 pending = Some(right);
402 }
403 }
404
405 if let Some(raw) = pending {
406 result.push(InlineNode::RawText(raw));
407 }
408 result
409}
410
411fn is_cross_boundary_hardbreak(result: &[InlineNode<'_>], left: &Raw<'_>, right: &Raw<'_>) -> bool {
412 if !left.content.ends_with('+') || !right.content.starts_with('\n') {
413 return false;
414 }
415 left.content
416 .strip_suffix('+')
417 .and_then(|prefix| prefix.chars().next_back())
418 .or_else(|| match result.last() {
419 Some(InlineNode::RawText(raw)) => raw.content.chars().next_back(),
420 _ => None,
421 })
422 == Some(' ')
423}
424
425fn remove_cross_boundary_hardbreak_marker<'a>(
426 result: &mut Vec<InlineNode<'a>>,
427 left: Raw<'a>,
428 state: &ParserState<'_>,
429) {
430 if left.content.len() > 1 {
431 if let Some(prefix) = raw_without_suffix(left, 2, state) {
432 result.push(InlineNode::RawText(prefix));
433 }
434 return;
435 }
436
437 let Some(InlineNode::RawText(previous)) = result.pop() else {
438 return;
439 };
440 if let Some(prefix) = raw_without_suffix(previous, 1, state) {
441 result.push(InlineNode::RawText(prefix));
442 }
443}
444
445fn raw_without_suffix<'a>(
446 mut raw: Raw<'a>,
447 suffix_len: usize,
448 state: &ParserState<'_>,
449) -> Option<Raw<'a>> {
450 let end = raw.content.len().checked_sub(suffix_len)?;
451 if end == 0 {
452 return None;
453 }
454 raw.location = raw_segment_location(&raw, 0, end, state);
455 raw.content = &raw.content[..end];
456 Some(raw)
457}
458
459fn raw_without_first_byte<'a>(mut raw: Raw<'a>, state: &ParserState<'_>) -> Option<Raw<'a>> {
460 if raw.content.len() <= 1 {
461 return None;
462 }
463 raw.location = raw_segment_location(&raw, 1, raw.content.len(), state);
464 raw.content = &raw.content[1..];
465 Some(raw)
466}
467
468fn cross_boundary_hardbreak_location(
469 preceding: &[InlineNode<'_>],
470 left: &Raw<'_>,
471 state: &ParserState<'_>,
472) -> Location {
473 let start = if left.content.len() > 1 {
474 raw_segment_location(left, left.content.len() - 2, left.content.len(), state).absolute_start
475 } else if let Some(InlineNode::RawText(previous)) = preceding.last() {
476 raw_segment_location(
477 previous,
478 previous.content.len().saturating_sub(1),
479 previous.content.len(),
480 state,
481 )
482 .absolute_start
483 } else {
484 left.location.absolute_start
485 };
486 state.create_location(start, left.location.absolute_end)
487}
488
489fn process_inline_children<'a>(
490 node: &mut InlineNode<'a>,
491 substitutions: &[Substitution],
492 state: &ParserState<'a>,
493) {
494 macro_rules! process_content {
495 ($value:expr) => {
496 $value.content = process_inline_nodes(take(&mut $value.content), substitutions, state)
497 };
498 }
499
500 match node {
501 InlineNode::BoldText(value) => process_content!(value),
502 InlineNode::ItalicText(value) => process_content!(value),
503 InlineNode::MonospaceText(value) => process_content!(value),
504 InlineNode::HighlightText(value) => process_content!(value),
505 InlineNode::SubscriptText(value) => process_content!(value),
506 InlineNode::SuperscriptText(value) => process_content!(value),
507 InlineNode::CurvedQuotationText(value) => process_content!(value),
508 InlineNode::CurvedApostropheText(value) => process_content!(value),
509 InlineNode::Macro(macro_node) => match macro_node {
510 InlineMacro::IndexTerm(value) => {
511 if value.catalog.is_none() {
512 value.catalog = Some(Box::new((**value).clone()));
513 }
514 value.for_each_label_mut(|nodes| {
515 *nodes = process_inline_nodes(take(nodes), substitutions, state);
516 });
517 }
518 InlineMacro::Url(value) => {
519 value.text = process_inline_nodes(take(&mut value.text), substitutions, state);
520 }
521 InlineMacro::Link(value) => {
522 value.text = process_inline_nodes(take(&mut value.text), substitutions, state);
523 }
524 InlineMacro::Mailto(value) => {
525 value.text = process_inline_nodes(take(&mut value.text), substitutions, state);
526 }
527 InlineMacro::CrossReference(value) => {
528 value.text = process_inline_nodes(take(&mut value.text), substitutions, state);
529 }
530 InlineMacro::Footnote(_)
532 | InlineMacro::Icon(_)
533 | InlineMacro::Image(_)
534 | InlineMacro::Keyboard(_)
535 | InlineMacro::Button(_)
536 | InlineMacro::Menu(_)
537 | InlineMacro::Autolink(_)
538 | InlineMacro::Pass(_)
539 | InlineMacro::Stem(_) => {}
540 },
541 InlineNode::PlainText(_)
542 | InlineNode::RawText(_)
543 | InlineNode::VerbatimText(_)
544 | InlineNode::StandaloneCurvedApostrophe(_)
545 | InlineNode::LineBreak(_)
546 | InlineNode::InlineAnchor(_)
547 | InlineNode::CalloutRef(_) => {}
548 }
549}
550
551fn expand_raw_attributes<'a>(raw: &Raw<'a>, state: &ParserState<'a>) -> Vec<InlineNode<'a>> {
552 let mut result = Vec::new();
553 let mut cursor = 0;
554 let mut copied_until = 0;
555 while let Some(relative_start) = raw.content[cursor..].find('{') {
556 let start = cursor + relative_start;
557 let Some(relative_end) = raw.content[start + 1..].find('}') else {
558 break;
559 };
560 let end = start + 1 + relative_end + 1;
561 let name = &raw.content[start + 1..end - 1];
562 if let Some((source_start, literal)) =
563 escaped_raw_attribute_reference(raw, start, end, state)
564 {
565 push_raw_segment(
566 &mut result,
567 raw,
568 copied_until,
569 source_start,
570 raw.subs.clone(),
571 state,
572 );
573 result.push(literal);
574 cursor = end;
575 copied_until = end;
576 continue;
577 }
578 if name.is_empty()
579 || !name
580 .bytes()
581 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
582 {
583 cursor = end;
584 continue;
585 }
586
587 let Some(resolved) = state.document_attributes.get(name) else {
588 cursor = end;
589 continue;
590 };
591 push_raw_segment(
592 &mut result,
593 raw,
594 copied_until,
595 start,
596 raw.subs.clone(),
597 state,
598 );
599 let reference_location = raw_segment_location(raw, start, end, state);
600 let mut value = String::new();
601 let _ = resolved.write_text(&mut value);
602 cursor = end;
603 copied_until = end;
604 if !resolved.inline_fragments().is_empty() {
605 let passthrough = Pass {
606 attribute_fragments: resolved.inline_fragments().into(),
607 text: Some(state.intern_str(&value)),
608 substitutions: Vec::new(),
609 location: reference_location,
610 kind: PassthroughKind::AttributeRef,
611 };
612 result.extend(process_passthrough(
613 passthrough.text.unwrap_or_default(),
614 &passthrough,
615 state,
616 SubstitutionPlan::only(&Substitution::Attributes),
617 ));
618 continue;
619 }
620 let mut value_cursor = 0;
621 for range in resolved.passthrough_ranges() {
624 if value_cursor < range.start {
625 result.push(InlineNode::RawText(Raw {
626 content: state.intern_str(&value[value_cursor..range.start]),
627 location: reference_location.clone(),
628 subs: vec![Substitution::SpecialChars],
629 }));
630 }
631 result.push(InlineNode::RawText(Raw {
632 content: state.intern_str(&value[range.clone()]),
633 location: reference_location.clone(),
634 subs: Vec::new(),
635 }));
636 value_cursor = range.end;
637 }
638 if value_cursor < value.len() {
639 result.push(InlineNode::RawText(Raw {
640 content: state.intern_str(&value[value_cursor..]),
641 location: reference_location,
642 subs: vec![Substitution::SpecialChars],
643 }));
644 }
645 }
646 push_raw_segment(
647 &mut result,
648 raw,
649 copied_until,
650 raw.content.len(),
651 raw.subs.clone(),
652 state,
653 );
654 result
655}
656
657fn escaped_raw_attribute_reference<'a>(
658 raw: &Raw<'a>,
659 start: usize,
660 end: usize,
661 state: &ParserState<'a>,
662) -> Option<(usize, InlineNode<'a>)> {
663 let name = &raw.content[start + 1..end - 1];
664 let escaped_start = raw.content[..start].ends_with('\\');
665 if !escaped_start && !name.ends_with('\\') {
666 return None;
667 }
668 let name = name.strip_suffix('\\').unwrap_or(name);
669 if name.is_empty()
670 || !name
671 .bytes()
672 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
673 {
674 return None;
675 }
676 let source_start = start - usize::from(escaped_start);
677 let literal = format!("{{{name}}}");
678 Some((
679 source_start,
680 InlineNode::RawText(Raw {
681 content: state.intern_str(&literal),
682 location: raw_segment_location(raw, source_start, end, state),
683 subs: raw.subs.clone(),
684 }),
685 ))
686}
687
688fn push_raw_segment<'a>(
689 result: &mut Vec<InlineNode<'a>>,
690 raw: &Raw<'a>,
691 start: usize,
692 end: usize,
693 subs: Vec<Substitution>,
694 state: &ParserState<'_>,
695) {
696 if start < end {
697 result.push(InlineNode::RawText(Raw {
698 content: &raw.content[start..end],
699 location: raw_segment_location(raw, start, end, state),
700 subs,
701 }));
702 }
703}
704
705fn raw_segment_location(
706 raw: &Raw<'_>,
707 start: usize,
708 end: usize,
709 state: &ParserState<'_>,
710) -> Location {
711 let Some(source_start) = raw_source_start(raw, state) else {
712 return raw.location.clone();
714 };
715 let start = if start == 0 {
716 raw.location.absolute_start
717 } else {
718 source_start + start
719 };
720 state.create_block_location(start, source_start + end, 0)
721}
722
723fn raw_source_start(raw: &Raw<'_>, state: &ParserState<'_>) -> Option<usize> {
724 let end = step_char(
725 state.input,
726 raw.location.absolute_end,
727 RoundDirection::Forward,
728 );
729 let original = state.input.get(raw.location.absolute_start..end)?;
730 let escapes = original.strip_suffix(raw.content)?;
731 escapes
732 .bytes()
733 .all(|byte| byte == b'\\')
734 .then_some(raw.location.absolute_start + escapes.len())
735}
736
737fn parse_raw_substitution<'a>(
738 raw: Raw<'a>,
739 substitution: &Substitution,
740 state: &ParserState<'a>,
741) -> Vec<InlineNode<'a>> {
742 parse_raw_stage(raw, SubstitutionPlan::only(substitution), state)
743}
744
745fn parse_raw_stage<'a>(
746 raw: Raw<'a>,
747 plan: SubstitutionPlan,
748 state: &ParserState<'a>,
749) -> Vec<InlineNode<'a>> {
750 if raw.content.is_empty() {
751 return Vec::new();
752 }
753
754 let Some(mut parsed) = parse_raw_inlines(raw.content, plan, state) else {
755 return vec![InlineNode::RawText(raw)];
756 };
757 for node in &mut parsed {
758 convert_plain_to_raw_with_source(node, &raw.subs, Some(raw.content));
759 map_stage_locations(node, &raw, state);
760 }
761 parsed
762}
763
764fn parse_raw_inlines<'a>(
765 content: &'a str,
766 plan: SubstitutionPlan,
767 state: &ParserState<'a>,
768) -> Option<Vec<InlineNode<'a>>> {
769 let mut rules = state.inline_ctx.rules;
770 rules.set(InlineRules::AUTOLINKS, plan.enabled(&Substitution::Macros));
771 rules.remove(InlineRules::HARD_BREAKS);
772 rules.set(
773 InlineRules::EOI_HARD_BREAK,
774 plan.enabled(&Substitution::PostReplacements),
775 );
776 let inline_ctx = InlineContext {
777 offset: 0,
778 substitutions: plan,
779 rules,
780 };
781 let mut child = ParserState::for_inline_parsing(content, state, inline_ctx);
782
783 let parsed = if plan.enabled(&Substitution::Quotes) && !plan.enabled(&Substitution::Macros) {
784 inline_parser::quotes_only_inlines(content, &mut child)
785 } else {
786 inline_parser::inlines(content, &mut child)
787 };
788 parsed.ok()
789}
790
791fn map_stage_locations(node: &mut InlineNode<'_>, raw: &Raw<'_>, state: &ParserState<'_>) {
792 super::location_walk::walk_inline_locations_mut(node, &mut |location| {
793 let end = step_char(raw.content, location.absolute_end, RoundDirection::Forward);
794 *location = raw_segment_location(raw, location.absolute_start, end, state);
795 });
796}
797
798fn parse_raw_fragments<'a>(
801 fragments: Vec<Raw<'a>>,
802 plan: SubstitutionPlan,
803 state: &ParserState<'a>,
804) -> Vec<InlineNode<'a>> {
805 if fragments.len() <= 1 {
806 return fragments
807 .into_iter()
808 .flat_map(|raw| parse_raw_stage(raw, plan, state))
809 .collect();
810 }
811 let text: String = fragments.iter().map(|raw| raw.content).collect();
812 let text = state.intern_str(&text);
813 let Some(mut nodes) = parse_raw_inlines(text, plan, state) else {
814 return fragments.into_iter().map(InlineNode::RawText).collect();
815 };
816 for node in &mut nodes {
817 convert_plain_to_raw_with_source(node, &[], Some(text));
818 }
819 let mut offset = 0;
820 let spans: Vec<_> = fragments
821 .iter()
822 .map(|raw| {
823 let start = offset;
824 offset += raw.content.len();
825 (start..offset, raw)
826 })
827 .collect();
828 let mut nodes = restore_fragment_substitutions(nodes, &spans, text);
829 for node in &mut nodes {
830 super::location_walk::walk_inline_locations_mut(node, &mut |location| {
831 location.absolute_start =
832 fragment_offset(location.absolute_start, &spans, false, state);
833 location.absolute_end = fragment_offset(location.absolute_end, &spans, true, state);
834 location.start = state
835 .line_map
836 .offset_to_position(location.absolute_start, state.input);
837 location.end = state
838 .line_map
839 .offset_to_position(location.absolute_end, state.input);
840 });
841 }
842 nodes
843}
844
845fn fragment_offset(
846 offset: usize,
847 fragments: &[(std::ops::Range<usize>, &Raw<'_>)],
848 end: bool,
849 state: &ParserState<'_>,
850) -> usize {
851 let Some((span, raw)) = fragments
852 .iter()
853 .find(|(span, _)| offset < span.end)
854 .or_else(|| fragments.last())
855 else {
856 return offset;
857 };
858 let relative = offset.saturating_sub(span.start).min(span.len());
859 if let Some(start) = raw_source_start(raw, state) {
860 if relative == 0 && !end {
861 raw.location.absolute_start
862 } else {
863 start + relative
864 }
865 } else if end {
866 raw.location.absolute_end
867 } else {
868 raw.location.absolute_start
869 }
870}
871
872fn restore_fragment_substitutions<'a>(
873 nodes: Vec<InlineNode<'a>>,
874 fragments: &[(std::ops::Range<usize>, &Raw<'a>)],
875 text: &str,
876) -> Vec<InlineNode<'a>> {
877 let mut result = Vec::with_capacity(nodes.len());
878 for mut node in nodes {
879 if let InlineNode::RawText(raw) = &node
880 && raw.subs.is_empty()
881 {
882 let original_start = raw.location.absolute_start;
883 let start = text
885 .get(
886 original_start
887 ..step_char(text, raw.location.absolute_end, RoundDirection::Forward),
888 )
889 .and_then(|source| source.find(raw.content))
890 .map_or(original_start, |offset| original_start + offset);
891 let end = start + raw.content.len();
892 for (span, fragment) in fragments {
893 let left = start.max(span.start);
894 let right = end.min(span.end);
895 if left < right {
896 result.push(InlineNode::RawText(Raw {
897 content: &raw.content[left - start..right - start],
898 location: Location {
899 absolute_start: if left == start { original_start } else { left },
900 absolute_end: step_char(text, right, RoundDirection::Backward),
901 ..raw.location.clone()
902 },
903 subs: fragment.subs.clone(),
904 }));
905 }
906 }
907 continue;
908 }
909 for_each_inline_children(&mut node, &mut |children| {
910 *children = restore_fragment_substitutions(take(children), fragments, text);
911 });
912 result.push(node);
913 }
914 result
915}
916
917pub(crate) fn convert_plain_to_raw(node: &mut InlineNode<'_>, subs: &[Substitution]) {
918 convert_plain_to_raw_with_source(node, subs, None);
919}
920
921fn convert_plain_to_raw_with_source(
922 node: &mut InlineNode<'_>,
923 subs: &[Substitution],
924 source: Option<&str>,
925) {
926 if let InlineNode::PlainText(plain) = node {
927 if let Some(source) = source {
928 let start = plain.location.absolute_start;
929 let end = start + plain.content.len();
930 if source.get(start..end) == Some(plain.content) {
931 plain.location.absolute_end = step_char(source, end, RoundDirection::Backward);
932 }
933 }
934 *node = InlineNode::RawText(Raw {
935 content: plain.content,
936 location: plain.location.clone(),
937 subs: subs.to_vec(),
938 });
939 return;
940 }
941 for_each_inline_children(node, &mut |children| {
942 for child in children {
943 convert_plain_to_raw_with_source(child, subs, source);
944 }
945 });
946}
947
948fn for_each_inline_children<'a>(
949 node: &mut InlineNode<'a>,
950 visit: &mut impl FnMut(&mut Vec<InlineNode<'a>>),
951) {
952 match node {
953 InlineNode::BoldText(value) => visit(&mut value.content),
954 InlineNode::ItalicText(value) => visit(&mut value.content),
955 InlineNode::MonospaceText(value) => visit(&mut value.content),
956 InlineNode::HighlightText(value) => visit(&mut value.content),
957 InlineNode::SubscriptText(value) => visit(&mut value.content),
958 InlineNode::SuperscriptText(value) => visit(&mut value.content),
959 InlineNode::CurvedQuotationText(value) => visit(&mut value.content),
960 InlineNode::CurvedApostropheText(value) => visit(&mut value.content),
961 InlineNode::Macro(macro_node) => match macro_node {
962 InlineMacro::Footnote(value) => visit(&mut value.content),
963 InlineMacro::Url(value) => visit(&mut value.text),
964 InlineMacro::Link(value) => visit(&mut value.text),
965 InlineMacro::Mailto(value) => visit(&mut value.text),
966 InlineMacro::CrossReference(value) => visit(&mut value.text),
967 InlineMacro::IndexTerm(value) => {
968 value.for_each_label_mut(&mut *visit);
969 if let Some(catalog) = &mut value.catalog {
970 catalog.for_each_label_mut(&mut *visit);
971 }
972 }
973 InlineMacro::Icon(_)
974 | InlineMacro::Image(_)
975 | InlineMacro::Keyboard(_)
976 | InlineMacro::Button(_)
977 | InlineMacro::Menu(_)
978 | InlineMacro::Autolink(_)
979 | InlineMacro::Pass(_)
980 | InlineMacro::Stem(_) => {}
981 },
982 InlineNode::PlainText(_)
983 | InlineNode::RawText(_)
984 | InlineNode::VerbatimText(_)
985 | InlineNode::StandaloneCurvedApostrophe(_)
986 | InlineNode::LineBreak(_)
987 | InlineNode::InlineAnchor(_)
988 | InlineNode::CalloutRef(_) => {}
989 }
990}
991
992pub fn parse_text_for_quotes(content: &str) -> ParseInlineResult {
1019 let owner = OwnedInput::new(content.into());
1020 ParseInlineResult::from_infallible(owner, |owner| {
1021 parse_text_for_quotes_in(&owner.arena, &owner.source)
1022 })
1023}
1024
1025pub(crate) fn parse_text_for_quotes_in<'a>(
1029 arena: &'a Bump,
1030 content: &'a str,
1031) -> Vec<InlineNode<'a>> {
1032 if content.is_empty() {
1033 return Vec::new();
1034 }
1035
1036 if !content
1040 .bytes()
1041 .any(|b| matches!(b, b'*' | b'_' | b'`' | b'#' | b'^' | b'~' | b'"' | b'\''))
1042 {
1043 return vec![InlineNode::PlainText(Plain {
1044 content,
1045 location: Location::default(),
1046 escaped: false,
1047 })];
1048 }
1049
1050 let mut state = ParserState::new_quotes_only(content, arena);
1051 if let Ok(nodes) = inline_parser::quotes_only_inlines(content, &mut state) {
1052 nodes
1053 } else {
1054 tracing::warn!(
1055 input_len = content.len(),
1056 "quotes-only PEG parse failed, falling back to plain text"
1057 );
1058 vec![InlineNode::PlainText(Plain {
1059 content,
1060 location: Location::default(),
1061 escaped: false,
1062 })]
1063 }
1064}
1065
1066fn plain_text_at<'a>(
1068 text: &'a str,
1069 base_location: &Location,
1070 offset: usize,
1071 state: &ParserState<'_>,
1072) -> InlineNode<'a> {
1073 let start = base_location.absolute_start + offset;
1074 InlineNode::PlainText(Plain {
1075 content: text,
1076 location: state.create_block_location(start, start + text.len(), 0),
1077 escaped: false,
1078 })
1079}
1080
1081pub(crate) fn process_passthrough_placeholders<'a>(
1087 content: &'a str,
1088 processed: &ProcessedContent<'a>,
1089 state: &ParserState<'a>,
1090 base_location: &Location,
1091) -> Vec<InlineNode<'a>> {
1092 let mut result = Vec::with_capacity(processed.passthroughs.len() * 2 + 1);
1096 let mut remaining = content;
1097 let mut processed_offset = 0; for (index, passthrough) in processed.passthroughs.iter().enumerate() {
1101 let placeholder = format!("���{index}���");
1102
1103 if let Some(placeholder_pos) = remaining.find(&placeholder) {
1104 let before_content = if placeholder_pos > 0 {
1105 Some(&remaining[..placeholder_pos])
1106 } else {
1107 None
1108 };
1109
1110 if let Some(before) = before_content
1112 && !before.is_empty()
1113 {
1114 result.push(plain_text_at(
1115 before,
1116 base_location,
1117 processed_offset,
1118 state,
1119 ));
1120 processed_offset += before.len();
1121 }
1122
1123 if let Some(passthrough_content) = &passthrough.text {
1125 let processed_nodes = process_passthrough(
1126 passthrough_content,
1127 passthrough,
1128 state,
1129 processed.attribute_substitutions,
1130 );
1131 for node in processed_nodes {
1132 result.push(node);
1133 }
1134 }
1135
1136 let skip_len = placeholder_pos + placeholder.len();
1138 remaining = &remaining[skip_len..];
1139 processed_offset +=
1141 passthrough.location.absolute_end - passthrough.location.absolute_start;
1142 }
1143 }
1144
1145 if !remaining.is_empty() {
1147 if let Some(InlineNode::PlainText(last_plain)) = result.last_mut() {
1149 last_plain.content =
1151 state.intern_fmt(format_args!("{}{remaining}", last_plain.content));
1152 last_plain.location.absolute_end = base_location.absolute_end;
1154 last_plain.location.end = base_location.end.clone();
1155 } else {
1156 let mut node = plain_text_at(remaining, base_location, processed_offset, state);
1160 if let InlineNode::PlainText(ref mut p) = node {
1161 p.location.absolute_end = base_location.absolute_end;
1162 p.location.end = base_location.end.clone();
1163 }
1164 result.push(node);
1165 }
1166 }
1167
1168 for node in &mut result {
1170 clamp_inline_node_locations(node, state.input);
1171 }
1172
1173 merge_adjacent_plain_text_nodes(state, result)
1175}
1176
1177pub(crate) fn merge_adjacent_plain_text_nodes<'a>(
1180 state: &ParserState<'a>,
1181 nodes: Vec<InlineNode<'a>>,
1182) -> Vec<InlineNode<'a>> {
1183 let mut result: Vec<InlineNode<'a>> = Vec::with_capacity(nodes.len());
1185
1186 for node in nodes {
1187 match (result.last_mut(), node) {
1188 (Some(InlineNode::PlainText(last_plain)), InlineNode::PlainText(current_plain)) => {
1189 last_plain.content = state.intern_fmt(format_args!(
1191 "{}{}",
1192 last_plain.content, current_plain.content
1193 ));
1194 last_plain.location.absolute_end = current_plain.location.absolute_end;
1196 last_plain.location.end = current_plain.location.end;
1197 }
1198 (_, node) => {
1199 result.push(node);
1201 }
1202 }
1203 }
1204
1205 result
1206}
1207
1208pub(crate) fn replace_passthrough_placeholders(
1209 content: &str,
1210 processed: &ProcessedContent,
1211) -> String {
1212 let mut result: String = content.into();
1213
1214 for (index, passthrough) in processed.passthroughs.iter().enumerate() {
1216 let placeholder = format!("���{index}���");
1217 if let Some(text) = &passthrough.text {
1218 result = result.replace(&placeholder, text);
1219 }
1220 }
1221
1222 result
1223}
1224
1225#[cfg(test)]
1226#[allow(clippy::indexing_slicing)] mod tests {
1228 use super::*;
1229
1230 #[test]
1237 fn test_constrained_bold_pattern() {
1238 let parsed = parse_text_for_quotes("This is *bold* text.");
1239 let nodes = parsed.inlines();
1240 assert_eq!(nodes.len(), 3);
1241 assert!(matches!(nodes[0], InlineNode::PlainText(_)));
1242 assert!(
1243 matches!(&nodes[1], InlineNode::BoldText(b) if matches!(b.content.first(), Some(InlineNode::PlainText(p)) if p.content == "bold"))
1244 );
1245 assert!(matches!(nodes[2], InlineNode::PlainText(_)));
1246 }
1247
1248 #[test]
1249 fn test_unconstrained_bold_pattern() {
1250 let parsed = parse_text_for_quotes("This**bold**word");
1251 let nodes = parsed.inlines();
1252 assert_eq!(nodes.len(), 3);
1253 assert!(
1254 matches!(&nodes[1], InlineNode::BoldText(b) if matches!(b.content.first(), Some(InlineNode::PlainText(p)) if p.content == "bold"))
1255 );
1256 }
1257
1258 #[test]
1259 fn test_constrained_italic_pattern() {
1260 let parsed = parse_text_for_quotes("This is _italic_ text.");
1261 let nodes = parsed.inlines();
1262 assert_eq!(nodes.len(), 3);
1263 assert!(
1264 matches!(&nodes[1], InlineNode::ItalicText(i) if matches!(i.content.first(), Some(InlineNode::PlainText(p)) if p.content == "italic"))
1265 );
1266 }
1267
1268 #[test]
1269 fn test_unconstrained_italic_pattern() {
1270 let parsed = parse_text_for_quotes("This__italic__word");
1271 let nodes = parsed.inlines();
1272 assert_eq!(nodes.len(), 3);
1273 assert!(
1274 matches!(&nodes[1], InlineNode::ItalicText(i) if matches!(i.content.first(), Some(InlineNode::PlainText(p)) if p.content == "italic"))
1275 );
1276 }
1277
1278 #[test]
1279 fn test_constrained_monospace_pattern() {
1280 let parsed = parse_text_for_quotes("Use `code` here.");
1281 let nodes = parsed.inlines();
1282 assert_eq!(nodes.len(), 3);
1283 assert!(
1284 matches!(&nodes[1], InlineNode::MonospaceText(m) if matches!(m.content.first(), Some(InlineNode::PlainText(p)) if p.content == "code"))
1285 );
1286 }
1287
1288 #[test]
1289 fn test_superscript_pattern() {
1290 let parsed = parse_text_for_quotes("E=mc^2^");
1291 let nodes = parsed.inlines();
1292 assert_eq!(nodes.len(), 2);
1293 assert!(
1294 matches!(&nodes[1], InlineNode::SuperscriptText(s) if matches!(s.content.first(), Some(InlineNode::PlainText(p)) if p.content == "2"))
1295 );
1296 }
1297
1298 #[test]
1299 fn test_subscript_pattern() {
1300 let parsed = parse_text_for_quotes("H~2~O");
1301 let nodes = parsed.inlines();
1302 assert_eq!(nodes.len(), 3);
1303 assert!(
1304 matches!(&nodes[1], InlineNode::SubscriptText(s) if matches!(s.content.first(), Some(InlineNode::PlainText(p)) if p.content == "2"))
1305 );
1306 }
1307
1308 #[test]
1309 fn test_highlight_pattern() {
1310 let parsed = parse_text_for_quotes("This is #highlighted# text.");
1311 let nodes = parsed.inlines();
1312 assert_eq!(nodes.len(), 3);
1313 assert!(
1314 matches!(&nodes[1], InlineNode::HighlightText(h) if matches!(h.content.first(), Some(InlineNode::PlainText(p)) if p.content == "highlighted"))
1315 );
1316 }
1317
1318 #[test]
1319 fn test_escaped_superscript_not_parsed() {
1320 let parsed = parse_text_for_quotes(r"E=mc\^2^");
1322 let nodes = parsed.inlines();
1323 assert!(
1325 nodes.iter().all(|n| matches!(n, InlineNode::PlainText(_))),
1326 "Escaped superscript should not be parsed"
1327 );
1328 }
1329
1330 #[test]
1331 fn test_escaped_subscript_not_parsed() {
1332 let parsed = parse_text_for_quotes(r"H\~2~O");
1333 let nodes = parsed.inlines();
1334 assert!(
1335 nodes.iter().all(|n| matches!(n, InlineNode::PlainText(_))),
1336 "Escaped subscript should not be parsed"
1337 );
1338 }
1339
1340 #[test]
1341 fn test_multiple_formats_in_sequence() {
1342 let parsed = parse_text_for_quotes("*bold* and _italic_ and `code`");
1343 let nodes = parsed.inlines();
1344 assert!(nodes.iter().any(|n| matches!(n, InlineNode::BoldText(_))));
1345 assert!(nodes.iter().any(|n| matches!(n, InlineNode::ItalicText(_))));
1346 assert!(
1347 nodes
1348 .iter()
1349 .any(|n| matches!(n, InlineNode::MonospaceText(_)))
1350 );
1351 }
1352
1353 #[test]
1354 fn test_plain_text_only() {
1355 let parsed = parse_text_for_quotes("Just plain text here.");
1356 let nodes = parsed.inlines();
1357 assert_eq!(nodes.len(), 1);
1358 assert!(matches!(nodes[0], InlineNode::PlainText(_)));
1359 }
1360
1361 #[test]
1362 fn test_empty_input() {
1363 let parsed = parse_text_for_quotes("");
1364 let nodes = parsed.inlines();
1365 assert_eq!(nodes, []);
1366 }
1367}