1use std::borrow::Cow;
9use std::collections::VecDeque;
10use std::fmt::{self, Display, Write};
11use std::iter;
12
13use rustc_data_structures::fx::FxIndexMap;
14use rustc_lexer::{Cursor, FrontmatterAllowed, LiteralKind, TokenKind};
15use rustc_span::edition::Edition;
16use rustc_span::symbol::Symbol;
17use rustc_span::{BytePos, DUMMY_SP, Span};
18
19use super::format;
20use crate::clean::PrimitiveType;
21use crate::display::Joined as _;
22use crate::html::escape::EscapeBodyText;
23use crate::html::macro_expansion::ExpandedCode;
24use crate::html::render::{Context, LinkFromSrc};
25
26pub(crate) struct HrefContext<'a, 'tcx> {
28 pub(crate) context: &'a Context<'tcx>,
29 pub(crate) file_span: Span,
31 pub(crate) root_path: &'a str,
34 pub(crate) current_href: String,
36}
37
38#[derive(Default)]
41pub(crate) struct DecorationInfo(pub(crate) FxIndexMap<&'static str, Vec<(u32, u32)>>);
42
43#[derive(Eq, PartialEq, Clone)]
44pub(crate) enum Tooltip {
45 IgnoreAll,
46 IgnoreSome(Vec<String>),
47 CompileFail,
48 ShouldPanic,
49 Edition(Edition),
50}
51
52pub(crate) fn render_example_with_highlighting(
54 src: &str,
55 tooltip: Option<&Tooltip>,
56 playground_button: Option<&str>,
57 extra_classes: &[String],
58) -> impl Display {
59 fmt::from_fn(move |f| {
60 write_header("rust-example-rendered", tooltip, extra_classes).fmt(f)?;
61 write_code(f, src, None, None, None);
62 write_footer(playground_button).fmt(f)
63 })
64}
65
66fn write_header(class: &str, tooltip: Option<&Tooltip>, extra_classes: &[String]) -> impl Display {
67 fmt::from_fn(move |f| {
68 write!(
69 f,
70 "<div class=\"example-wrap{}\">",
71 tooltip
72 .map(|tooltip| match tooltip {
73 Tooltip::IgnoreAll | Tooltip::IgnoreSome(_) => " ignore",
74 Tooltip::CompileFail => " compile_fail",
75 Tooltip::ShouldPanic => " should_panic",
76 Tooltip::Edition(_) => " edition",
77 })
78 .unwrap_or_default()
79 )?;
80
81 if let Some(tooltip) = tooltip {
82 let tooltip = fmt::from_fn(|f| match tooltip {
83 Tooltip::IgnoreAll => f.write_str("This example is not tested"),
84 Tooltip::IgnoreSome(platforms) => {
85 f.write_str("This example is not tested on ")?;
86 match &platforms[..] {
87 [] => unreachable!(),
88 [platform] => f.write_str(platform)?,
89 [first, second] => write!(f, "{first} or {second}")?,
90 [platforms @ .., last] => {
91 for platform in platforms {
92 write!(f, "{platform}, ")?;
93 }
94 write!(f, "or {last}")?;
95 }
96 }
97 Ok(())
98 }
99 Tooltip::CompileFail => f.write_str("This example deliberately fails to compile"),
100 Tooltip::ShouldPanic => f.write_str("This example panics"),
101 Tooltip::Edition(edition) => write!(f, "This example runs with edition {edition}"),
102 });
103
104 write!(f, "<a href=\"#\" class=\"tooltip\" title=\"{tooltip}\">ⓘ</a>")?;
105 }
106
107 let classes = fmt::from_fn(|f| {
108 iter::once("rust")
109 .chain(Some(class).filter(|class| !class.is_empty()))
110 .chain(extra_classes.iter().map(String::as_str))
111 .joined(" ", f)
112 });
113
114 write!(f, "<pre class=\"{classes}\"><code>")
115 })
116}
117
118fn can_merge(class1: Option<Class>, class2: Option<Class>, text: &str) -> bool {
127 match (class1, class2) {
128 (Some(c1), Some(c2)) => c1.is_equal_to(c2),
129 (Some(Class::Ident(_)), None) | (None, Some(Class::Ident(_))) => true,
130 (Some(Class::Macro(_)), _) => false,
131 (Some(_), None) | (None, Some(_)) => text.trim().is_empty(),
132 (None, None) => true,
133 }
134}
135
136struct TokenHandler<'a, 'tcx, F: Write> {
139 out: &'a mut F,
140 closing_tags: Vec<(&'static str, Class)>,
142 pending_exit_span: Option<Class>,
145 current_class: Option<Class>,
148 pending_elems: Vec<(Cow<'a, str>, Option<Class>)>,
151 href_context: Option<HrefContext<'a, 'tcx>>,
152 write_line_number: fn(&mut F, u32, &'static str),
153}
154
155impl<F: Write> std::fmt::Debug for TokenHandler<'_, '_, F> {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 f.debug_struct("TokenHandler")
158 .field("closing_tags", &self.closing_tags)
159 .field("pending_exit_span", &self.pending_exit_span)
160 .field("current_class", &self.current_class)
161 .field("pending_elems", &self.pending_elems)
162 .finish()
163 }
164}
165
166impl<F: Write> TokenHandler<'_, '_, F> {
167 fn handle_exit_span(&mut self) {
168 let class = self.closing_tags.last().expect("ExitSpan without EnterSpan").1;
171 self.write_pending_elems(Some(class));
173
174 exit_span(self.out, self.closing_tags.pop().expect("ExitSpan without EnterSpan").0);
175 self.pending_exit_span = None;
176 }
177
178 fn write_pending_elems(&mut self, current_class: Option<Class>) -> bool {
189 if self.pending_elems.is_empty() {
190 return false;
191 }
192 if let Some((_, parent_class)) = self.closing_tags.last()
193 && can_merge(current_class, Some(*parent_class), "")
194 {
195 for (text, class) in self.pending_elems.iter() {
196 string(
197 self.out,
198 EscapeBodyText(text),
199 *class,
200 &self.href_context,
201 false,
202 self.write_line_number,
203 );
204 }
205 } else {
206 let close_tag = if self.pending_elems.len() > 1
209 && let Some(current_class) = current_class
210 && !matches!(current_class, Class::PreludeTy(_))
213 {
214 Some(enter_span(self.out, current_class, &self.href_context))
215 } else {
216 None
217 };
218 let last_pending =
221 self.pending_elems.pop_if(|(_, class)| *class == Some(Class::Expansion));
222 for (text, class) in self.pending_elems.iter() {
223 string(
224 self.out,
225 EscapeBodyText(text),
226 *class,
227 &self.href_context,
228 close_tag.is_none(),
229 self.write_line_number,
230 );
231 }
232 if let Some(close_tag) = close_tag {
233 exit_span(self.out, close_tag);
234 }
235 if let Some((text, class)) = last_pending {
236 string(
237 self.out,
238 EscapeBodyText(&text),
239 class,
240 &self.href_context,
241 close_tag.is_none(),
242 self.write_line_number,
243 );
244 }
245 }
246 self.pending_elems.clear();
247 true
248 }
249
250 #[inline]
251 fn write_line_number(&mut self, line: u32, extra: &'static str) {
252 (self.write_line_number)(self.out, line, extra);
253 }
254}
255
256impl<F: Write> Drop for TokenHandler<'_, '_, F> {
257 fn drop(&mut self) {
259 if self.pending_exit_span.is_some() {
260 self.handle_exit_span();
261 } else {
262 self.write_pending_elems(self.current_class);
263 }
264 }
265}
266
267fn write_scraped_line_number(out: &mut impl Write, line: u32, extra: &'static str) {
268 write!(out, "{extra}<span data-nosnippet>{line}</span>",).unwrap();
271}
272
273fn write_line_number(out: &mut impl Write, line: u32, extra: &'static str) {
274 write!(out, "{extra}<a href=#{line} id={line} data-nosnippet>{line}</a>",).unwrap();
277}
278
279fn empty_line_number(out: &mut impl Write, _: u32, extra: &'static str) {
280 out.write_str(extra).unwrap();
281}
282
283fn get_next_expansion(
284 expanded_codes: &[ExpandedCode],
285 line: u32,
286 span: Span,
287) -> Option<&ExpandedCode> {
288 expanded_codes.iter().find(|code| code.start_line == line && code.span.lo() > span.lo())
289}
290
291fn get_expansion<'a, W: Write>(
292 token_handler: &mut TokenHandler<'_, '_, W>,
293 expanded_codes: &'a [ExpandedCode],
294 line: u32,
295 span: Span,
296) -> Option<&'a ExpandedCode> {
297 if let Some(expanded_code) = get_next_expansion(expanded_codes, line, span) {
298 let (closing, reopening) = if let Some(current_class) = token_handler.current_class
299 && let class = current_class.as_html()
300 && !class.is_empty()
301 {
302 ("</span>", format!("<span class=\"{class}\">"))
303 } else {
304 ("", String::new())
305 };
306 let id = format!("expand-{line}");
307 token_handler.pending_elems.push((
308 Cow::Owned(format!(
309 "{closing}\
310<span class=expansion>\
311 <input id={id} \
312 tabindex=0 \
313 type=checkbox \
314 aria-label=\"Collapse/expand macro\" \
315 title=\"\"Collapse/expand macro\">{reopening}",
316 )),
317 Some(Class::Expansion),
318 ));
319 Some(expanded_code)
320 } else {
321 None
322 }
323}
324
325fn start_expansion(out: &mut Vec<(Cow<'_, str>, Option<Class>)>, expanded_code: &ExpandedCode) {
326 out.push((
327 Cow::Owned(format!(
328 "<span class=expanded>{}</span><span class=original>",
329 expanded_code.code,
330 )),
331 Some(Class::Expansion),
332 ));
333}
334
335fn end_expansion<'a, W: Write>(
336 token_handler: &mut TokenHandler<'_, '_, W>,
337 expanded_codes: &'a [ExpandedCode],
338 expansion_start_tags: &[(&'static str, Class)],
339 line: u32,
340 span: Span,
341) -> Option<&'a ExpandedCode> {
342 if let Some(expanded_code) = get_next_expansion(expanded_codes, line, span) {
343 token_handler.pending_elems.push((Cow::Borrowed("</span>"), Some(Class::Expansion)));
345 return Some(expanded_code);
346 }
347 if expansion_start_tags.is_empty() && token_handler.closing_tags.is_empty() {
348 token_handler.pending_elems.push((Cow::Borrowed("</span></span>"), Some(Class::Expansion)));
350 return None;
351 }
352
353 let mut out = String::new();
356 let mut end = String::new();
357
358 let mut closing_tags = token_handler.closing_tags.iter().peekable();
359 let mut start_closing_tags = expansion_start_tags.iter().peekable();
360
361 while let (Some(tag), Some(start_tag)) = (closing_tags.peek(), start_closing_tags.peek())
362 && tag == start_tag
363 {
364 closing_tags.next();
365 start_closing_tags.next();
366 }
367 for (tag, class) in start_closing_tags.chain(closing_tags) {
368 out.push_str(tag);
369 end.push_str(&format!("<span class=\"{}\">", class.as_html()));
370 }
371 token_handler
372 .pending_elems
373 .push((Cow::Owned(format!("</span></span>{out}{end}")), Some(Class::Expansion)));
374 None
375}
376
377#[derive(Clone, Copy)]
378pub(super) struct LineInfo {
379 pub(super) start_line: u32,
380 max_lines: u32,
381 pub(super) is_scraped_example: bool,
382}
383
384impl LineInfo {
385 pub(super) fn new(max_lines: u32) -> Self {
386 Self { start_line: 1, max_lines: max_lines + 1, is_scraped_example: false }
387 }
388
389 pub(super) fn new_scraped(max_lines: u32, start_line: u32) -> Self {
390 Self {
391 start_line: start_line + 1,
392 max_lines: max_lines + start_line + 1,
393 is_scraped_example: true,
394 }
395 }
396}
397
398pub(super) fn write_code(
410 out: &mut impl Write,
411 src: &str,
412 href_context: Option<HrefContext<'_, '_>>,
413 decoration_info: Option<&DecorationInfo>,
414 line_info: Option<LineInfo>,
415) {
416 let src = src.replace("\r\n", "\n");
418 let mut token_handler = TokenHandler {
419 out,
420 closing_tags: Vec::new(),
421 pending_exit_span: None,
422 current_class: None,
423 pending_elems: Vec::with_capacity(20),
424 href_context,
425 write_line_number: match line_info {
426 Some(line_info) => {
427 if line_info.is_scraped_example {
428 write_scraped_line_number
429 } else {
430 write_line_number
431 }
432 }
433 None => empty_line_number,
434 },
435 };
436
437 let (mut line, max_lines) = if let Some(line_info) = line_info {
438 token_handler.write_line_number(line_info.start_line, "");
439 (line_info.start_line, line_info.max_lines)
440 } else {
441 (0, u32::MAX)
442 };
443
444 let (expanded_codes, file_span) = match token_handler.href_context.as_ref().and_then(|c| {
445 let expanded_codes = c.context.shared.expanded_codes.get(&c.file_span.lo())?;
446 Some((expanded_codes, c.file_span))
447 }) {
448 Some((expanded_codes, file_span)) => (expanded_codes.as_slice(), file_span),
449 None => (&[] as &[ExpandedCode], DUMMY_SP),
450 };
451 let mut current_expansion = get_expansion(&mut token_handler, expanded_codes, line, file_span);
452 token_handler.write_pending_elems(None);
453 let mut expansion_start_tags = Vec::new();
454
455 Classifier::new(
456 &src,
457 token_handler.href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
458 decoration_info,
459 )
460 .highlight(&mut |span, highlight| {
461 match highlight {
462 Highlight::Token { text, class } => {
463 let need_current_class_update = if let Some(pending) =
466 token_handler.pending_exit_span
467 && !can_merge(Some(pending), class, text)
468 {
469 token_handler.handle_exit_span();
470 true
471 } else if !can_merge(token_handler.current_class, class, text) {
474 token_handler.write_pending_elems(token_handler.current_class);
475 true
476 } else {
477 token_handler.current_class.is_none()
478 };
479
480 if need_current_class_update {
481 token_handler.current_class = class.map(Class::dummy);
482 }
483 if text == "\n" {
484 line += 1;
485 if line < max_lines {
486 token_handler
487 .pending_elems
488 .push((Cow::Borrowed(text), Some(Class::Backline(line))));
489 }
490 if current_expansion.is_none() {
491 current_expansion =
492 get_expansion(&mut token_handler, expanded_codes, line, span);
493 expansion_start_tags = token_handler.closing_tags.clone();
494 }
495 if let Some(ref current_expansion) = current_expansion
496 && current_expansion.span.lo() == span.hi()
497 {
498 start_expansion(&mut token_handler.pending_elems, current_expansion);
499 }
500 } else {
501 token_handler.pending_elems.push((Cow::Borrowed(text), class));
502
503 let mut need_end = false;
504 if let Some(ref current_expansion) = current_expansion {
505 if current_expansion.span.lo() == span.hi() {
506 start_expansion(&mut token_handler.pending_elems, current_expansion);
507 } else if current_expansion.end_line == line
508 && span.hi() >= current_expansion.span.hi()
509 {
510 need_end = true;
511 }
512 }
513 if need_end {
514 current_expansion = end_expansion(
515 &mut token_handler,
516 expanded_codes,
517 &expansion_start_tags,
518 line,
519 span,
520 );
521 }
522 }
523 }
524 Highlight::EnterSpan { class } => {
525 let mut should_add = true;
526 if let Some(pending_exit_span) = token_handler.pending_exit_span {
527 if class.is_equal_to(pending_exit_span) {
528 should_add = false;
529 } else {
530 token_handler.handle_exit_span();
531 }
532 } else {
533 if token_handler.write_pending_elems(token_handler.current_class) {
535 token_handler.current_class = None;
536 }
537 }
538 if should_add {
539 let closing_tag =
540 enter_span(token_handler.out, class, &token_handler.href_context);
541 token_handler.closing_tags.push((closing_tag, class));
542 }
543
544 token_handler.current_class = None;
545 token_handler.pending_exit_span = None;
546 }
547 Highlight::ExitSpan => {
548 token_handler.current_class = None;
549 token_handler.pending_exit_span = Some(
550 token_handler
551 .closing_tags
552 .last()
553 .as_ref()
554 .expect("ExitSpan without EnterSpan")
555 .1,
556 );
557 }
558 };
559 });
560}
561
562fn write_footer(playground_button: Option<&str>) -> impl Display {
563 fmt::from_fn(move |f| write!(f, "</code></pre>{}</div>", playground_button.unwrap_or_default()))
564}
565
566#[derive(Clone, Copy, Debug, Eq, PartialEq)]
568enum Class {
569 Comment,
570 DocComment,
571 Attribute,
572 KeyWord,
573 RefKeyWord,
575 Self_(Span),
576 Macro(Span),
577 MacroNonTerminal,
578 String,
579 Number,
580 Bool,
581 Ident(Span),
583 Lifetime,
584 PreludeTy(Span),
585 PreludeVal(Span),
586 QuestionMark,
587 Decoration(&'static str),
588 Backline(u32),
589 Expansion,
591}
592
593impl Class {
594 fn is_equal_to(self, other: Self) -> bool {
599 match (self, other) {
600 (Self::Self_(_), Self::Self_(_))
601 | (Self::Macro(_), Self::Macro(_))
602 | (Self::Ident(_), Self::Ident(_)) => true,
603 (Self::Decoration(c1), Self::Decoration(c2)) => c1 == c2,
604 (x, y) => x == y,
605 }
606 }
607
608 fn dummy(self) -> Self {
611 match self {
612 Self::Self_(_) => Self::Self_(DUMMY_SP),
613 Self::Macro(_) => Self::Macro(DUMMY_SP),
614 Self::Ident(_) => Self::Ident(DUMMY_SP),
615 s => s,
616 }
617 }
618
619 fn as_html(self) -> &'static str {
621 match self {
622 Class::Comment => "comment",
623 Class::DocComment => "doccomment",
624 Class::Attribute => "attr",
625 Class::KeyWord => "kw",
626 Class::RefKeyWord => "kw-2",
627 Class::Self_(_) => "self",
628 Class::Macro(_) => "macro",
629 Class::MacroNonTerminal => "macro-nonterminal",
630 Class::String => "string",
631 Class::Number => "number",
632 Class::Bool => "bool-val",
633 Class::Ident(_) => "",
634 Class::Lifetime => "lifetime",
635 Class::PreludeTy(_) => "prelude-ty",
636 Class::PreludeVal(_) => "prelude-val",
637 Class::QuestionMark => "question-mark",
638 Class::Decoration(kind) => kind,
639 Class::Backline(_) => "",
640 Class::Expansion => "",
641 }
642 }
643
644 fn get_span(self) -> Option<Span> {
647 match self {
648 Self::Ident(sp)
649 | Self::Self_(sp)
650 | Self::Macro(sp)
651 | Self::PreludeTy(sp)
652 | Self::PreludeVal(sp) => Some(sp),
653 Self::Comment
654 | Self::DocComment
655 | Self::Attribute
656 | Self::KeyWord
657 | Self::RefKeyWord
658 | Self::MacroNonTerminal
659 | Self::String
660 | Self::Number
661 | Self::Bool
662 | Self::Lifetime
663 | Self::QuestionMark
664 | Self::Decoration(_)
665 | Self::Backline(_)
666 | Self::Expansion => None,
667 }
668 }
669}
670
671#[derive(Debug)]
672enum Highlight<'a> {
673 Token { text: &'a str, class: Option<Class> },
674 EnterSpan { class: Class },
675 ExitSpan,
676}
677
678struct TokenIter<'a> {
679 src: &'a str,
680 cursor: Cursor<'a>,
681}
682
683impl<'a> Iterator for TokenIter<'a> {
684 type Item = (TokenKind, &'a str);
685 fn next(&mut self) -> Option<(TokenKind, &'a str)> {
686 let token = self.cursor.advance_token();
687 if token.kind == TokenKind::Eof {
688 return None;
689 }
690 let (text, rest) = self.src.split_at(token.len as usize);
691 self.src = rest;
692 Some((token.kind, text))
693 }
694}
695
696fn get_real_ident_class(text: &str, allow_path_keywords: bool) -> Option<Class> {
698 let ignore: &[&str] =
699 if allow_path_keywords { &["self", "Self", "super", "crate"] } else { &["self", "Self"] };
700 if ignore.contains(&text) {
701 return None;
702 }
703 Some(match text {
704 "ref" | "mut" => Class::RefKeyWord,
705 "false" | "true" => Class::Bool,
706 _ if Symbol::intern(text).is_reserved(|| Edition::Edition2021) => Class::KeyWord,
707 _ => return None,
708 })
709}
710
711struct PeekIter<'a> {
717 stored: VecDeque<(TokenKind, &'a str)>,
718 peek_pos: usize,
720 iter: TokenIter<'a>,
721}
722
723impl<'a> PeekIter<'a> {
724 fn new(iter: TokenIter<'a>) -> Self {
725 Self { stored: VecDeque::new(), peek_pos: 0, iter }
726 }
727 fn peek(&mut self) -> Option<&(TokenKind, &'a str)> {
729 if self.stored.is_empty()
730 && let Some(next) = self.iter.next()
731 {
732 self.stored.push_back(next);
733 }
734 self.stored.front()
735 }
736 fn peek_next(&mut self) -> Option<&(TokenKind, &'a str)> {
738 self.peek_pos += 1;
739 if self.peek_pos - 1 < self.stored.len() {
740 self.stored.get(self.peek_pos - 1)
741 } else if let Some(next) = self.iter.next() {
742 self.stored.push_back(next);
743 self.stored.back()
744 } else {
745 None
746 }
747 }
748}
749
750impl<'a> Iterator for PeekIter<'a> {
751 type Item = (TokenKind, &'a str);
752 fn next(&mut self) -> Option<Self::Item> {
753 self.peek_pos = 0;
754 if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() }
755 }
756}
757
758struct Decorations {
760 starts: Vec<(u32, &'static str)>,
761 ends: Vec<u32>,
762}
763
764impl Decorations {
765 fn new(info: &DecorationInfo) -> Self {
766 let (mut starts, mut ends): (Vec<_>, Vec<_>) = info
768 .0
769 .iter()
770 .flat_map(|(&kind, ranges)| ranges.iter().map(move |&(lo, hi)| ((lo, kind), hi)))
771 .unzip();
772
773 starts.sort_by_key(|(lo, _)| *lo);
775 ends.sort();
776
777 Decorations { starts, ends }
778 }
779}
780
781fn new_span(lo: u32, text: &str, file_span: Span) -> Span {
783 let hi = lo + text.len() as u32;
784 let file_lo = file_span.lo();
785 file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi))
786}
787
788struct Classifier<'src> {
791 tokens: PeekIter<'src>,
792 in_attribute: bool,
793 in_macro: bool,
794 in_macro_nonterminal: bool,
795 byte_pos: u32,
796 file_span: Span,
797 src: &'src str,
798 decorations: Option<Decorations>,
799}
800
801impl<'src> Classifier<'src> {
802 fn new(src: &'src str, file_span: Span, decoration_info: Option<&DecorationInfo>) -> Self {
805 let tokens =
806 PeekIter::new(TokenIter { src, cursor: Cursor::new(src, FrontmatterAllowed::Yes) });
807 let decorations = decoration_info.map(Decorations::new);
808 Classifier {
809 tokens,
810 in_attribute: false,
811 in_macro: false,
812 in_macro_nonterminal: false,
813 byte_pos: 0,
814 file_span,
815 src,
816 decorations,
817 }
818 }
819
820 fn get_full_ident_path(&mut self) -> Vec<(TokenKind, usize, usize)> {
822 let start = self.byte_pos as usize;
823 let mut pos = start;
824 let mut has_ident = false;
825
826 loop {
827 let mut nb = 0;
828 while let Some((TokenKind::Colon, _)) = self.tokens.peek() {
829 self.tokens.next();
830 nb += 1;
831 }
832 if has_ident && nb == 0 {
835 return vec![(TokenKind::Ident, start, pos)];
836 } else if nb != 0 && nb != 2 {
837 if has_ident {
838 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
839 } else {
840 return vec![(TokenKind::Colon, start, pos + nb)];
841 }
842 }
843
844 if let Some((None, text)) = self.tokens.peek().map(|(token, text)| {
845 if *token == TokenKind::Ident {
846 let class = get_real_ident_class(text, true);
847 (class, text)
848 } else {
849 (Some(Class::Comment), text)
851 }
852 }) {
853 pos += text.len() + nb;
855 has_ident = true;
856 self.tokens.next();
857 } else if nb > 0 && has_ident {
858 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
859 } else if nb > 0 {
860 return vec![(TokenKind::Colon, start, start + nb)];
861 } else if has_ident {
862 return vec![(TokenKind::Ident, start, pos)];
863 } else {
864 return Vec::new();
865 }
866 }
867 }
868
869 fn next(&mut self) -> Option<(TokenKind, &'src str, u32)> {
874 if let Some((kind, text)) = self.tokens.next() {
875 let before = self.byte_pos;
876 self.byte_pos += text.len() as u32;
877 Some((kind, text, before))
878 } else {
879 None
880 }
881 }
882
883 fn highlight(mut self, sink: &mut dyn FnMut(Span, Highlight<'src>)) {
889 loop {
890 if let Some(decs) = self.decorations.as_mut() {
891 let byte_pos = self.byte_pos;
892 let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count();
893 for (_, kind) in decs.starts.drain(0..n_starts) {
894 sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Decoration(kind) });
895 }
896
897 let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count();
898 for _ in decs.ends.drain(0..n_ends) {
899 sink(DUMMY_SP, Highlight::ExitSpan);
900 }
901 }
902
903 if self
904 .tokens
905 .peek()
906 .map(|t| matches!(t.0, TokenKind::Colon | TokenKind::Ident))
907 .unwrap_or(false)
908 {
909 let tokens = self.get_full_ident_path();
910 for (token, start, end) in &tokens {
911 let text = &self.src[*start..*end];
912 self.advance(*token, text, sink, *start as u32);
913 self.byte_pos += text.len() as u32;
914 }
915 if !tokens.is_empty() {
916 continue;
917 }
918 }
919 if let Some((token, text, before)) = self.next() {
920 self.advance(token, text, sink, before);
921 } else {
922 break;
923 }
924 }
925 }
926
927 fn advance(
934 &mut self,
935 token: TokenKind,
936 text: &'src str,
937 sink: &mut dyn FnMut(Span, Highlight<'src>),
938 before: u32,
939 ) {
940 let lookahead = self.peek();
941 let file_span = self.file_span;
942 let no_highlight = |sink: &mut dyn FnMut(_, _)| {
943 sink(new_span(before, text, file_span), Highlight::Token { text, class: None })
944 };
945 let whitespace = |sink: &mut dyn FnMut(_, _)| {
946 let mut start = 0u32;
947 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
948 sink(
949 new_span(before + start, part, file_span),
950 Highlight::Token { text: part, class: None },
951 );
952 start += part.len() as u32;
953 }
954 };
955 let class = match token {
956 TokenKind::Whitespace => return whitespace(sink),
957 TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => {
958 if doc_style.is_some() {
959 Class::DocComment
960 } else {
961 Class::Comment
962 }
963 }
964 TokenKind::Bang if self.in_macro => {
967 self.in_macro = false;
968 sink(new_span(before, text, file_span), Highlight::Token { text, class: None });
969 sink(DUMMY_SP, Highlight::ExitSpan);
970 return;
971 }
972
973 TokenKind::Star => match self.tokens.peek() {
977 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
978 Some((TokenKind::Ident, "mut")) => {
979 self.next();
980 sink(
981 DUMMY_SP,
982 Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) },
983 );
984 return;
985 }
986 Some((TokenKind::Ident, "const")) => {
987 self.next();
988 sink(
989 DUMMY_SP,
990 Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) },
991 );
992 return;
993 }
994 _ => Class::RefKeyWord,
995 },
996 TokenKind::And => match self.tokens.peek() {
997 Some((TokenKind::And, _)) => {
998 self.next();
999 sink(DUMMY_SP, Highlight::Token { text: "&&", class: None });
1000 return;
1001 }
1002 Some((TokenKind::Eq, _)) => {
1003 self.next();
1004 sink(DUMMY_SP, Highlight::Token { text: "&=", class: None });
1005 return;
1006 }
1007 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
1008 Some((TokenKind::Ident, "mut")) => {
1009 self.next();
1010 sink(
1011 DUMMY_SP,
1012 Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) },
1013 );
1014 return;
1015 }
1016 _ => Class::RefKeyWord,
1017 },
1018
1019 TokenKind::Eq => match lookahead {
1021 Some(TokenKind::Eq) => {
1022 self.next();
1023 sink(DUMMY_SP, Highlight::Token { text: "==", class: None });
1024 return;
1025 }
1026 Some(TokenKind::Gt) => {
1027 self.next();
1028 sink(DUMMY_SP, Highlight::Token { text: "=>", class: None });
1029 return;
1030 }
1031 _ => return no_highlight(sink),
1032 },
1033 TokenKind::Minus if lookahead == Some(TokenKind::Gt) => {
1034 self.next();
1035 sink(DUMMY_SP, Highlight::Token { text: "->", class: None });
1036 return;
1037 }
1038
1039 TokenKind::Minus
1041 | TokenKind::Plus
1042 | TokenKind::Or
1043 | TokenKind::Slash
1044 | TokenKind::Caret
1045 | TokenKind::Percent
1046 | TokenKind::Bang
1047 | TokenKind::Lt
1048 | TokenKind::Gt => return no_highlight(sink),
1049
1050 TokenKind::Dot
1052 | TokenKind::Semi
1053 | TokenKind::Comma
1054 | TokenKind::OpenParen
1055 | TokenKind::CloseParen
1056 | TokenKind::OpenBrace
1057 | TokenKind::CloseBrace
1058 | TokenKind::OpenBracket
1059 | TokenKind::At
1060 | TokenKind::Tilde
1061 | TokenKind::Colon
1062 | TokenKind::Frontmatter { .. }
1063 | TokenKind::Unknown => return no_highlight(sink),
1064
1065 TokenKind::Question => Class::QuestionMark,
1066
1067 TokenKind::Dollar => match lookahead {
1068 Some(TokenKind::Ident) => {
1069 self.in_macro_nonterminal = true;
1070 Class::MacroNonTerminal
1071 }
1072 _ => return no_highlight(sink),
1073 },
1074
1075 TokenKind::Pound => {
1080 match lookahead {
1081 Some(TokenKind::Bang) => {
1083 self.next();
1084 if let Some(TokenKind::OpenBracket) = self.peek() {
1085 self.in_attribute = true;
1086 sink(
1087 new_span(before, text, file_span),
1088 Highlight::EnterSpan { class: Class::Attribute },
1089 );
1090 }
1091 sink(DUMMY_SP, Highlight::Token { text: "#", class: None });
1092 sink(DUMMY_SP, Highlight::Token { text: "!", class: None });
1093 return;
1094 }
1095 Some(TokenKind::OpenBracket) => {
1097 self.in_attribute = true;
1098 sink(
1099 new_span(before, text, file_span),
1100 Highlight::EnterSpan { class: Class::Attribute },
1101 );
1102 }
1103 _ => (),
1104 }
1105 return no_highlight(sink);
1106 }
1107 TokenKind::CloseBracket => {
1108 if self.in_attribute {
1109 self.in_attribute = false;
1110 sink(
1111 new_span(before, text, file_span),
1112 Highlight::Token { text: "]", class: None },
1113 );
1114 sink(DUMMY_SP, Highlight::ExitSpan);
1115 return;
1116 }
1117 return no_highlight(sink);
1118 }
1119 TokenKind::Literal { kind, .. } => match kind {
1120 LiteralKind::Byte { .. }
1122 | LiteralKind::Char { .. }
1123 | LiteralKind::Str { .. }
1124 | LiteralKind::ByteStr { .. }
1125 | LiteralKind::RawStr { .. }
1126 | LiteralKind::RawByteStr { .. }
1127 | LiteralKind::CStr { .. }
1128 | LiteralKind::RawCStr { .. } => Class::String,
1129 LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number,
1131 },
1132 TokenKind::GuardedStrPrefix => return no_highlight(sink),
1133 TokenKind::Ident | TokenKind::RawIdent if lookahead == Some(TokenKind::Bang) => {
1134 self.in_macro = true;
1135 let span = new_span(before, text, file_span);
1136 sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Macro(span) });
1137 sink(span, Highlight::Token { text, class: None });
1138 return;
1139 }
1140 TokenKind::Ident => match get_real_ident_class(text, false) {
1141 None => match text {
1142 "Option" | "Result" => Class::PreludeTy(new_span(before, text, file_span)),
1143 "Some" | "None" | "Ok" | "Err" => {
1144 Class::PreludeVal(new_span(before, text, file_span))
1145 }
1146 "union" if self.check_if_is_union_keyword() => Class::KeyWord,
1149 _ if self.in_macro_nonterminal => {
1150 self.in_macro_nonterminal = false;
1151 Class::MacroNonTerminal
1152 }
1153 "self" | "Self" => Class::Self_(new_span(before, text, file_span)),
1154 _ => Class::Ident(new_span(before, text, file_span)),
1155 },
1156 Some(c) => c,
1157 },
1158 TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => {
1159 Class::Ident(new_span(before, text, file_span))
1160 }
1161 TokenKind::Lifetime { .. }
1162 | TokenKind::RawLifetime
1163 | TokenKind::UnknownPrefixLifetime => Class::Lifetime,
1164 TokenKind::Eof => panic!("Eof in advance"),
1165 };
1166 let mut start = 0u32;
1169 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
1170 sink(
1171 new_span(before + start, part, file_span),
1172 Highlight::Token { text: part, class: Some(class) },
1173 );
1174 start += part.len() as u32;
1175 }
1176 }
1177
1178 fn peek(&mut self) -> Option<TokenKind> {
1179 self.tokens.peek().map(|(token_kind, _text)| *token_kind)
1180 }
1181
1182 fn check_if_is_union_keyword(&mut self) -> bool {
1183 while let Some(kind) = self.tokens.peek_next().map(|(token_kind, _text)| token_kind) {
1184 if *kind == TokenKind::Whitespace {
1185 continue;
1186 }
1187 return *kind == TokenKind::Ident;
1188 }
1189 false
1190 }
1191}
1192
1193fn enter_span(
1196 out: &mut impl Write,
1197 klass: Class,
1198 href_context: &Option<HrefContext<'_, '_>>,
1199) -> &'static str {
1200 string_without_closing_tag(out, "", Some(klass), href_context, true).expect(
1201 "internal error: enter_span was called with Some(klass) but did not return a \
1202 closing HTML tag",
1203 )
1204}
1205
1206fn exit_span(out: &mut impl Write, closing_tag: &str) {
1208 out.write_str(closing_tag).unwrap();
1209}
1210
1211fn string<W: Write>(
1228 out: &mut W,
1229 text: EscapeBodyText<'_>,
1230 klass: Option<Class>,
1231 href_context: &Option<HrefContext<'_, '_>>,
1232 open_tag: bool,
1233 write_line_number_callback: fn(&mut W, u32, &'static str),
1234) {
1235 if let Some(Class::Backline(line)) = klass {
1236 write_line_number_callback(out, line, "\n");
1237 } else if let Some(Class::Expansion) = klass {
1238 out.write_str(text.0).unwrap();
1240 } else if let Some(closing_tag) =
1241 string_without_closing_tag(out, text, klass, href_context, open_tag)
1242 {
1243 out.write_str(closing_tag).unwrap();
1244 }
1245}
1246
1247fn generate_link_to_def(
1248 out: &mut impl Write,
1249 text_s: &str,
1250 klass: Class,
1251 href_context: &Option<HrefContext<'_, '_>>,
1252 def_span: Span,
1253 open_tag: bool,
1254) -> bool {
1255 if let Some(href_context) = href_context
1256 && let Some(href) =
1257 href_context.context.shared.span_correspondence_map.get(&def_span).and_then(|href| {
1258 let context = href_context.context;
1259 match href {
1265 LinkFromSrc::Local(span) => {
1266 context.href_from_span_relative(*span, &href_context.current_href)
1267 }
1268 LinkFromSrc::External(def_id) => {
1269 format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1270 .ok()
1271 .map(|(url, _, _)| url)
1272 }
1273 LinkFromSrc::Primitive(prim) => format::href_with_root_path(
1274 PrimitiveType::primitive_locations(context.tcx())[prim],
1275 context,
1276 Some(href_context.root_path),
1277 )
1278 .ok()
1279 .map(|(url, _, _)| url),
1280 LinkFromSrc::Doc(def_id) => {
1281 format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1282 .ok()
1283 .map(|(doc_link, _, _)| doc_link)
1284 }
1285 }
1286 })
1287 {
1288 if !open_tag {
1289 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1292 } else {
1293 let klass_s = klass.as_html();
1294 if klass_s.is_empty() {
1295 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1296 } else {
1297 write!(out, "<a class=\"{klass_s}\" href=\"{href}\">{text_s}").unwrap();
1298 }
1299 }
1300 return true;
1301 }
1302 false
1303}
1304
1305fn string_without_closing_tag<T: Display>(
1315 out: &mut impl Write,
1316 text: T,
1317 klass: Option<Class>,
1318 href_context: &Option<HrefContext<'_, '_>>,
1319 open_tag: bool,
1320) -> Option<&'static str> {
1321 let Some(klass) = klass else {
1322 write!(out, "{text}").unwrap();
1323 return None;
1324 };
1325 let Some(def_span) = klass.get_span() else {
1326 if !open_tag {
1327 write!(out, "{text}").unwrap();
1328 return None;
1329 }
1330 write!(out, "<span class=\"{klass}\">{text}", klass = klass.as_html()).unwrap();
1331 return Some("</span>");
1332 };
1333
1334 let mut added_links = false;
1335 let mut text_s = text.to_string();
1336 if text_s.contains("::") {
1337 let mut span = def_span.with_hi(def_span.lo());
1338 text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| {
1339 span = span.with_hi(span.hi() + BytePos(t.len() as _));
1340 match t {
1341 "::" => write!(&mut path, "::"),
1342 "self" | "Self" => write!(
1343 &mut path,
1344 "<span class=\"{klass}\">{t}</span>",
1345 klass = Class::Self_(DUMMY_SP).as_html(),
1346 ),
1347 "crate" | "super" => {
1348 write!(
1349 &mut path,
1350 "<span class=\"{klass}\">{t}</span>",
1351 klass = Class::KeyWord.as_html(),
1352 )
1353 }
1354 t => {
1355 if !t.is_empty()
1356 && generate_link_to_def(&mut path, t, klass, href_context, span, open_tag)
1357 {
1358 added_links = true;
1359 write!(&mut path, "</a>")
1360 } else {
1361 write!(&mut path, "{t}")
1362 }
1363 }
1364 }
1365 .expect("Failed to build source HTML path");
1366 span = span.with_lo(span.lo() + BytePos(t.len() as _));
1367 path
1368 });
1369 }
1370
1371 if !added_links && generate_link_to_def(out, &text_s, klass, href_context, def_span, open_tag) {
1372 return Some("</a>");
1373 }
1374 if !open_tag {
1375 out.write_str(&text_s).unwrap();
1376 return None;
1377 }
1378 let klass_s = klass.as_html();
1379 if klass_s.is_empty() {
1380 out.write_str(&text_s).unwrap();
1381 Some("")
1382 } else {
1383 write!(out, "<span class=\"{klass_s}\">{text_s}").unwrap();
1384 Some("</span>")
1385 }
1386}
1387
1388#[cfg(test)]
1389mod tests;