1use std::borrow::Cow;
9use std::collections::VecDeque;
10use std::fmt::{self, Display, Write};
11use std::iter;
12
13use itertools::Either;
14use rustc_data_structures::fx::FxIndexMap;
15use rustc_lexer::{Cursor, FrontmatterAllowed, LiteralKind, TokenKind};
16use rustc_span::BytePos;
17use rustc_span::edition::Edition;
18use rustc_span::symbol::Symbol;
19
20use super::format;
21use crate::clean::PrimitiveType;
22use crate::display::Joined as _;
23use crate::html::escape::EscapeBodyText;
24use crate::html::format::HrefInfo;
25use crate::html::macro_expansion::ExpandedCode;
26use crate::html::render::span_map::{DUMMY_SP, Span};
27use crate::html::render::{Context, LinkFromSrc};
28
29pub(crate) struct HrefContext<'a, 'tcx> {
31 pub(crate) context: &'a Context<'tcx>,
32 pub(crate) file_span: Span,
34 pub(crate) root_path: &'a str,
37 pub(crate) current_href: String,
39}
40
41#[derive(Default)]
44pub(crate) struct DecorationInfo(pub(crate) FxIndexMap<&'static str, Vec<(u32, u32)>>);
45
46#[derive(Eq, PartialEq, Clone)]
47pub(crate) enum Tooltip {
48 IgnoreAll,
49 IgnoreSome(Vec<String>),
50 CompileFail,
51 ShouldPanic,
52 Edition(Edition),
53}
54
55pub(crate) fn render_example_with_highlighting(
57 src: &str,
58 tooltip: Option<&Tooltip>,
59 playground_button: Option<&str>,
60 extra_classes: &[String],
61) -> impl Display {
62 fmt::from_fn(move |f| {
63 write_header("rust-example-rendered", tooltip, extra_classes).fmt(f)?;
64 write_code(f, src, None, None, None);
65 write_footer(playground_button).fmt(f)
66 })
67}
68
69fn write_header(class: &str, tooltip: Option<&Tooltip>, extra_classes: &[String]) -> impl Display {
70 fmt::from_fn(move |f| {
71 write!(
72 f,
73 "<div class=\"example-wrap{}\">",
74 tooltip
75 .map(|tooltip| match tooltip {
76 Tooltip::IgnoreAll | Tooltip::IgnoreSome(_) => " ignore",
77 Tooltip::CompileFail => " compile_fail",
78 Tooltip::ShouldPanic => " should_panic",
79 Tooltip::Edition(_) => " edition",
80 })
81 .unwrap_or_default()
82 )?;
83
84 if let Some(tooltip) = tooltip {
85 let tooltip = fmt::from_fn(|f| match tooltip {
86 Tooltip::IgnoreAll => f.write_str("This example is not tested"),
87 Tooltip::IgnoreSome(platforms) => {
88 f.write_str("This example is not tested on ")?;
89 match &platforms[..] {
90 [] => unreachable!(),
91 [platform] => f.write_str(platform)?,
92 [first, second] => write!(f, "{first} or {second}")?,
93 [platforms @ .., last] => {
94 for platform in platforms {
95 write!(f, "{platform}, ")?;
96 }
97 write!(f, "or {last}")?;
98 }
99 }
100 Ok(())
101 }
102 Tooltip::CompileFail => f.write_str("This example deliberately fails to compile"),
103 Tooltip::ShouldPanic => f.write_str("This example panics"),
104 Tooltip::Edition(edition) => write!(f, "This example runs with edition {edition}"),
105 });
106
107 write!(f, "<a href=\"#\" class=\"tooltip\" title=\"{tooltip}\">ⓘ</a>")?;
108 }
109
110 let classes = fmt::from_fn(|f| {
111 iter::once("rust")
112 .chain(Some(class).filter(|class| !class.is_empty()))
113 .chain(extra_classes.iter().map(String::as_str))
114 .joined(" ", f)
115 });
116
117 write!(f, "<pre class=\"{classes}\"><code>")
118 })
119}
120
121fn can_merge(class1: Option<Class>, class2: Option<Class>, text: &str) -> bool {
130 match (class1, class2) {
131 (Some(c1), Some(c2)) => c1.is_equal_to(c2),
132 (Some(Class::Ident(_)), None) | (None, Some(Class::Ident(_))) => true,
133 (Some(Class::Macro(_)), _) => false,
134 (Some(_), None) | (None, Some(_)) => text.trim().is_empty(),
135 (None, None) => true,
136 }
137}
138
139#[derive(Debug)]
140struct ClassInfo {
141 class: Class,
142 closing_tag: Option<&'static str>,
144 pending_exit: bool,
150}
151
152impl ClassInfo {
153 fn new(class: Class, closing_tag: Option<&'static str>) -> Self {
154 Self { class, closing_tag, pending_exit: closing_tag.is_some() }
155 }
156
157 fn close_tag<W: Write>(&self, out: &mut W) {
158 if let Some(closing_tag) = self.closing_tag {
159 out.write_str(closing_tag).unwrap();
160 }
161 }
162
163 fn is_open(&self) -> bool {
164 self.closing_tag.is_some()
165 }
166}
167
168#[derive(Debug)]
175struct ClassStack {
176 open_classes: Vec<ClassInfo>,
177}
178
179impl ClassStack {
180 fn new() -> Self {
181 Self { open_classes: Vec::new() }
182 }
183
184 fn enter_elem<W: Write>(
185 &mut self,
186 out: &mut W,
187 href_context: &Option<HrefContext<'_, '_>>,
188 new_class: Class,
189 closing_tag: Option<&'static str>,
190 ) {
191 if let Some(current_class) = self.open_classes.last_mut() {
192 if can_merge(Some(current_class.class), Some(new_class), "") {
193 current_class.pending_exit = false;
194 return;
195 } else if current_class.pending_exit {
196 current_class.close_tag(out);
197 self.open_classes.pop();
198 }
199 }
200 let mut class_info = ClassInfo::new(new_class, closing_tag);
201 if closing_tag.is_none() {
202 if matches!(new_class, Class::Decoration(_) | Class::Original) {
203 write!(out, "<span class=\"{}\">", new_class.as_html()).unwrap();
208 class_info.closing_tag = Some("</span>");
209 } else if new_class.get_span().is_some()
210 && let Some(closing_tag) =
211 string_without_closing_tag(out, "", Some(class_info.class), href_context, false)
212 && !closing_tag.is_empty()
213 {
214 class_info.closing_tag = Some(closing_tag);
215 }
216 }
217
218 self.open_classes.push(class_info);
219 }
220
221 fn exit_elem(&mut self) {
225 let current_class =
226 self.open_classes.last_mut().expect("`exit_elem` called on empty class stack");
227 if !current_class.pending_exit {
228 current_class.pending_exit = true;
229 return;
230 }
231 self.open_classes.pop();
233 let current_class =
234 self.open_classes.last_mut().expect("`exit_elem` called on empty class stack parent");
235 current_class.pending_exit = true;
236 }
237
238 fn last_class(&self) -> Option<Class> {
239 self.open_classes.last().map(|c| c.class)
240 }
241
242 fn last_class_is_open(&self) -> bool {
243 if let Some(last) = self.open_classes.last() {
244 last.is_open()
245 } else {
246 true
248 }
249 }
250
251 fn close_last_if_needed<W: Write>(&mut self, out: &mut W) {
252 if let Some(last) = self.open_classes.pop_if(|class| class.pending_exit && class.is_open())
253 {
254 last.close_tag(out);
255 }
256 }
257
258 fn push<W: Write>(
259 &mut self,
260 out: &mut W,
261 href_context: &Option<HrefContext<'_, '_>>,
262 class: Option<Class>,
263 text: Cow<'_, str>,
264 needs_escape: bool,
265 ) {
266 if !can_merge(self.last_class(), class, &text) {
269 self.close_last_if_needed(out)
270 }
271
272 let current_class = self.last_class();
273
274 if class.is_none() && !self.last_class_is_open() {
278 if let Some(current_class_info) = self.open_classes.last_mut() {
279 let class_s = current_class_info.class.as_html();
280 if !class_s.is_empty() {
281 write!(out, "<span class=\"{class_s}\">").unwrap();
282 }
283 current_class_info.closing_tag = Some("</span>");
284 }
285 }
286
287 let current_class_is_open = self.open_classes.last().is_some_and(|c| c.is_open());
288 let can_merge = can_merge(class, current_class, &text);
289 let should_open_tag = !current_class_is_open || !can_merge;
290
291 let text =
292 if needs_escape { Either::Left(&EscapeBodyText(&text)) } else { Either::Right(text) };
293
294 let closing_tag =
295 string_without_closing_tag(out, &text, class, href_context, should_open_tag);
296 if class.is_some() && should_open_tag && closing_tag.is_none() {
297 panic!(
298 "called `string_without_closing_tag` with a class but no closing tag was returned"
299 );
300 } else if let Some(closing_tag) = closing_tag
301 && !closing_tag.is_empty()
302 {
303 if closing_tag == "</a>" {
306 out.write_str(closing_tag).unwrap();
307 } else if let Some(class) = class
309 && !can_merge
310 {
311 self.enter_elem(out, href_context, class, Some("</span>"));
312 } else if let Some(current_class_info) = self.open_classes.last_mut() {
314 current_class_info.closing_tag = Some("</span>");
315 }
316 }
317 }
318
319 fn empty_stack<W: Write>(&mut self, out: &mut W) -> Vec<Class> {
326 let mut classes = Vec::with_capacity(self.open_classes.len());
327
328 while let Some(class_info) = self.open_classes.pop() {
330 class_info.close_tag(out);
331 if !class_info.pending_exit {
332 classes.push(class_info.class);
333 }
334 }
335 classes
336 }
337}
338
339struct TokenHandler<'a, 'tcx, F: Write> {
342 out: &'a mut F,
343 class_stack: ClassStack,
344 href_context: Option<HrefContext<'a, 'tcx>>,
347 line_number_kind: LineNumberKind,
348 line: u32,
349 max_lines: u32,
350}
351
352impl<F: Write> std::fmt::Debug for TokenHandler<'_, '_, F> {
353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354 f.debug_struct("TokenHandler").field("class_stack", &self.class_stack).finish()
355 }
356}
357
358impl<'a, F: Write> TokenHandler<'a, '_, F> {
359 fn handle_backline(&mut self) -> Option<impl Display + use<F>> {
360 self.line += 1;
361 if self.line < self.max_lines {
362 return Some(self.line_number_kind.render(self.line));
363 }
364 None
365 }
366
367 fn push_token_without_backline_check(
368 &mut self,
369 class: Option<Class>,
370 text: Cow<'a, str>,
371 needs_escape: bool,
372 ) {
373 self.class_stack.push(self.out, &self.href_context, class, text, needs_escape);
374 }
375
376 fn push_token(&mut self, class: Option<Class>, text: Cow<'a, str>) {
377 if text == "\n"
378 && let Some(backline) = self.handle_backline()
379 {
380 write!(self.out, "{text}{backline}").unwrap();
381 } else {
382 self.push_token_without_backline_check(class, text, true);
383 }
384 }
385
386 fn start_expansion(&mut self) {
387 let classes = self.class_stack.empty_stack(self.out);
389
390 self.class_stack.enter_elem(self.out, &self.href_context, Class::Expansion, None);
392 self.push_token_without_backline_check(
393 Some(Class::Expansion),
394 Cow::Owned(format!(
395 "<input id=expand-{} \
396 tabindex=0 \
397 type=checkbox \
398 aria-label=\"Collapse/expand macro\" \
399 title=\"Collapse/expand macro\">",
400 self.line,
401 )),
402 false,
403 );
404
405 for class in classes.into_iter().rev() {
407 self.class_stack.enter_elem(self.out, &self.href_context, class, None);
408 }
409 }
410
411 fn add_expanded_code(&mut self, expanded_code: &ExpandedCode) {
412 self.push_token_without_backline_check(
413 None,
414 Cow::Owned(format!("<span class=expanded>{}</span>", expanded_code.code)),
415 false,
416 );
417 self.class_stack.enter_elem(self.out, &self.href_context, Class::Original, None);
418 }
419
420 fn close_expansion(&mut self) {
421 let classes = self.class_stack.empty_stack(self.out);
423
424 for class in classes.into_iter().rev() {
426 if !matches!(class, Class::Expansion | Class::Original) {
427 self.class_stack.enter_elem(self.out, &self.href_context, class, None);
428 }
429 }
430 }
431
432 fn close_original_tag(&mut self) {
435 let mut classes_to_reopen = Vec::new();
436 while let Some(mut class_info) = self.class_stack.open_classes.pop() {
437 if class_info.class == Class::Original {
438 while let Some(class_info) = classes_to_reopen.pop() {
439 self.class_stack.open_classes.push(class_info);
440 }
441 class_info.close_tag(self.out);
442 return;
443 }
444 class_info.close_tag(self.out);
445 if !class_info.pending_exit {
446 class_info.closing_tag = None;
447 classes_to_reopen.push(class_info);
448 }
449 }
450 panic!("Didn't find `Class::Original` to close");
451 }
452}
453
454impl<F: Write> Drop for TokenHandler<'_, '_, F> {
455 fn drop(&mut self) {
457 self.class_stack.empty_stack(self.out);
458 }
459}
460
461#[derive(Clone, Copy)]
463enum LineNumberKind {
464 Scraped,
466 Normal,
468 Empty,
470}
471
472impl LineNumberKind {
473 fn render(self, line: u32) -> impl Display {
474 fmt::from_fn(move |f| {
475 match self {
476 Self::Scraped => write!(f, "<span data-nosnippet>{line}</span>"),
479 Self::Normal => write!(f, "<a href=#{line} id={line} data-nosnippet>{line}</a>"),
480 Self::Empty => Ok(()),
481 }
482 })
483 }
484}
485
486fn get_next_expansion(
487 expanded_codes: &[ExpandedCode],
488 line: u32,
489 span: Span,
490) -> Option<&ExpandedCode> {
491 expanded_codes.iter().find(|code| code.start_line == line && code.span.lo() > span.lo())
492}
493
494fn get_expansion<'a, W: Write>(
495 token_handler: &mut TokenHandler<'_, '_, W>,
496 expanded_codes: &'a [ExpandedCode],
497 span: Span,
498) -> Option<&'a ExpandedCode> {
499 let expanded_code = get_next_expansion(expanded_codes, token_handler.line, span)?;
500 token_handler.start_expansion();
501 Some(expanded_code)
502}
503
504fn end_expansion<'a, W: Write>(
505 token_handler: &mut TokenHandler<'_, '_, W>,
506 expanded_codes: &'a [ExpandedCode],
507 span: Span,
508) -> Option<&'a ExpandedCode> {
509 token_handler.close_original_tag();
511 let expansion = get_next_expansion(expanded_codes, token_handler.line, span);
513 if expansion.is_none() {
514 token_handler.close_expansion();
515 }
516 expansion
517}
518
519#[derive(Clone, Copy)]
520pub(super) struct LineInfo {
521 pub(super) start_line: u32,
522 max_lines: u32,
523 pub(super) is_scraped_example: bool,
524}
525
526impl LineInfo {
527 pub(super) fn new(max_lines: u32) -> Self {
528 Self { start_line: 1, max_lines: max_lines + 1, is_scraped_example: false }
529 }
530
531 pub(super) fn new_scraped(max_lines: u32, start_line: u32) -> Self {
532 Self {
533 start_line: start_line + 1,
534 max_lines: max_lines + start_line + 1,
535 is_scraped_example: true,
536 }
537 }
538}
539
540pub(super) fn write_code(
552 out: &mut impl Write,
553 src: &str,
554 href_context: Option<HrefContext<'_, '_>>,
555 decoration_info: Option<&DecorationInfo>,
556 line_info: Option<LineInfo>,
557) {
558 let src =
560 if src.contains('\r') { src.replace("\r\n", "\n").into() } else { Cow::Borrowed(src) };
566 let mut token_handler = TokenHandler {
567 out,
568 href_context,
569 line_number_kind: match line_info {
570 Some(line_info) => {
571 if line_info.is_scraped_example {
572 LineNumberKind::Scraped
573 } else {
574 LineNumberKind::Normal
575 }
576 }
577 None => LineNumberKind::Empty,
578 },
579 line: 0,
580 max_lines: u32::MAX,
581 class_stack: ClassStack::new(),
582 };
583
584 if let Some(line_info) = line_info {
585 token_handler.line = line_info.start_line - 1;
586 token_handler.max_lines = line_info.max_lines;
587 if let Some(backline) = token_handler.handle_backline() {
588 token_handler.push_token_without_backline_check(
589 None,
590 Cow::Owned(backline.to_string()),
591 false,
592 );
593 }
594 }
595
596 let (expanded_codes, file_span) = match token_handler.href_context.as_ref().and_then(|c| {
597 let expanded_codes = c.context.shared.expanded_codes.get(&c.file_span.lo())?;
598 Some((expanded_codes, c.file_span))
599 }) {
600 Some((expanded_codes, file_span)) => (expanded_codes.as_slice(), file_span),
601 None => (&[] as &[ExpandedCode], DUMMY_SP),
602 };
603 let mut current_expansion = get_expansion(&mut token_handler, expanded_codes, file_span);
604
605 classify(
606 &src,
607 token_handler.href_context.as_ref().map_or(DUMMY_SP, |c| c.file_span),
608 decoration_info,
609 &mut |span, highlight| match highlight {
610 Highlight::Token { text, class } => {
611 token_handler.push_token(class, Cow::Borrowed(text));
612
613 if text == "\n" {
614 if current_expansion.is_none() {
615 current_expansion = get_expansion(&mut token_handler, expanded_codes, span);
616 }
617 if let Some(ref current_expansion) = current_expansion
618 && current_expansion.span.lo() == span.hi()
619 {
620 token_handler.add_expanded_code(current_expansion);
621 }
622 } else {
623 let mut need_end = false;
624 if let Some(ref current_expansion) = current_expansion {
625 if current_expansion.span.lo() == span.hi() {
626 token_handler.add_expanded_code(current_expansion);
627 } else if current_expansion.end_line == token_handler.line
628 && span.hi() >= current_expansion.span.hi()
629 {
630 need_end = true;
631 }
632 }
633 if need_end {
634 current_expansion = end_expansion(&mut token_handler, expanded_codes, span);
635 }
636 }
637 }
638 Highlight::EnterSpan { class } => {
639 token_handler.class_stack.enter_elem(
640 token_handler.out,
641 &token_handler.href_context,
642 class,
643 None,
644 );
645 }
646 Highlight::ExitSpan => {
647 token_handler.class_stack.exit_elem();
648 }
649 },
650 );
651}
652
653fn write_footer(playground_button: Option<&str>) -> impl Display {
654 fmt::from_fn(move |f| write!(f, "</code></pre>{}</div>", playground_button.unwrap_or_default()))
655}
656
657#[derive(Clone, Copy, Debug, Eq, PartialEq)]
659enum Class {
660 Comment,
661 DocComment,
662 Attribute,
663 KeyWord,
664 RefKeyWord,
666 Self_(Span),
667 Macro(Span),
668 MacroNonTerminal,
669 String,
670 Number,
671 Bool,
672 Ident(Span),
674 Lifetime,
675 PreludeTy(Span),
676 PreludeVal(Span),
677 QuestionMark,
678 Decoration(&'static str),
679 Expansion,
681 Original,
683}
684
685impl Class {
686 fn is_equal_to(self, other: Self) -> bool {
691 match (self, other) {
692 (Self::Self_(_), Self::Self_(_))
693 | (Self::Macro(_), Self::Macro(_))
694 | (Self::Ident(_), Self::Ident(_)) => true,
695 (Self::Decoration(c1), Self::Decoration(c2)) => c1 == c2,
696 (x, y) => x == y,
697 }
698 }
699
700 fn as_html(self) -> &'static str {
702 match self {
703 Class::Comment => "comment",
704 Class::DocComment => "doccomment",
705 Class::Attribute => "attr",
706 Class::KeyWord => "kw",
707 Class::RefKeyWord => "kw-2",
708 Class::Self_(_) => "self",
709 Class::Macro(_) => "macro",
710 Class::MacroNonTerminal => "macro-nonterminal",
711 Class::String => "string",
712 Class::Number => "number",
713 Class::Bool => "bool-val",
714 Class::Ident(_) => "",
715 Class::Lifetime => "lifetime",
716 Class::PreludeTy(_) => "prelude-ty",
717 Class::PreludeVal(_) => "prelude-val",
718 Class::QuestionMark => "question-mark",
719 Class::Decoration(kind) => kind,
720 Class::Expansion => "expansion",
721 Class::Original => "original",
722 }
723 }
724
725 fn get_span(self) -> Option<Span> {
728 match self {
729 Self::Ident(sp)
730 | Self::Self_(sp)
731 | Self::Macro(sp)
732 | Self::PreludeTy(sp)
733 | Self::PreludeVal(sp) => Some(sp),
734 Self::Comment
735 | Self::DocComment
736 | Self::Attribute
737 | Self::KeyWord
738 | Self::RefKeyWord
739 | Self::MacroNonTerminal
740 | Self::String
741 | Self::Number
742 | Self::Bool
743 | Self::Lifetime
744 | Self::QuestionMark
745 | Self::Decoration(_)
746 | Self::Original
747 | Self::Expansion => None,
748 }
749 }
750}
751
752impl fmt::Display for Class {
753 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
754 let html = self.as_html();
755 if html.is_empty() {
756 return Ok(());
757 }
758 write!(f, " class=\"{html}\"")
759 }
760}
761
762#[derive(Debug)]
763enum Highlight<'a> {
764 Token { text: &'a str, class: Option<Class> },
765 EnterSpan { class: Class },
766 ExitSpan,
767}
768
769struct TokenIter<'a> {
770 src: &'a str,
771 cursor: Cursor<'a>,
772}
773
774impl<'a> TokenIter<'a> {
775 fn new(src: &'a str) -> Self {
776 Self { src, cursor: Cursor::new(src, FrontmatterAllowed::Yes) }
777 }
778}
779
780impl<'a> Iterator for TokenIter<'a> {
781 type Item = (TokenKind, &'a str);
782 fn next(&mut self) -> Option<(TokenKind, &'a str)> {
783 let token = self.cursor.advance_token();
784 if token.kind == TokenKind::Eof {
785 return None;
786 }
787 let (text, rest) = self.src.split_at(token.len as usize);
788 self.src = rest;
789 Some((token.kind, text))
790 }
791}
792
793const NON_MACRO_KEYWORDS: &[&str] = &["if", "while", "match", "break", "return", "impl"];
795
796struct PeekIter<'a> {
802 stored: VecDeque<(TokenKind, &'a str)>,
803 peek_pos: usize,
805 iter: TokenIter<'a>,
806}
807
808impl<'a> PeekIter<'a> {
809 fn new(iter: TokenIter<'a>) -> Self {
810 Self { stored: VecDeque::new(), peek_pos: 0, iter }
811 }
812 fn peek(&mut self) -> Option<(TokenKind, &'a str)> {
814 if self.stored.is_empty()
815 && let Some(next) = self.iter.next()
816 {
817 self.stored.push_back(next);
818 }
819 self.stored.front().copied()
820 }
821 fn peek_next(&mut self) -> Option<(TokenKind, &'a str)> {
823 self.peek_pos += 1;
824 if self.peek_pos - 1 < self.stored.len() {
825 self.stored.get(self.peek_pos - 1)
826 } else if let Some(next) = self.iter.next() {
827 self.stored.push_back(next);
828 self.stored.back()
829 } else {
830 None
831 }
832 .copied()
833 }
834
835 fn peek_next_if<F: Fn((TokenKind, &'a str)) -> bool>(
836 &mut self,
837 f: F,
838 ) -> Option<(TokenKind, &'a str)> {
839 let next = self.peek_next()?;
840 if f(next) {
841 Some(next)
842 } else {
843 self.peek_pos -= 1;
845 None
846 }
847 }
848
849 fn stop_peeking(&mut self) {
850 self.peek_pos = 0;
851 }
852}
853
854impl<'a> Iterator for PeekIter<'a> {
855 type Item = (TokenKind, &'a str);
856 fn next(&mut self) -> Option<Self::Item> {
857 if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() }
858 }
859}
860
861struct Decorations {
863 starts: Vec<(u32, &'static str)>,
864 ends: Vec<u32>,
865}
866
867impl Decorations {
868 fn new(info: &DecorationInfo) -> Self {
869 let (mut starts, mut ends): (Vec<_>, Vec<_>) = info
871 .0
872 .iter()
873 .flat_map(|(&kind, ranges)| ranges.iter().map(move |&(lo, hi)| ((lo, kind), hi)))
874 .unzip();
875
876 starts.sort_by_key(|(lo, _)| *lo);
878 ends.sort();
879
880 Decorations { starts, ends }
881 }
882}
883
884fn new_span(lo: u32, text: &str, file_span: Span) -> Span {
886 let hi = lo + text.len() as u32;
887 let file_lo = file_span.lo();
888 file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi))
889}
890
891fn classify<'src>(
892 src: &'src str,
893 file_span: Span,
894 decoration_info: Option<&DecorationInfo>,
895 sink: &mut dyn FnMut(Span, Highlight<'src>),
896) {
897 let offset = rustc_lexer::strip_shebang(src);
898
899 if let Some(offset) = offset {
900 sink(DUMMY_SP, Highlight::Token { text: &src[..offset], class: Some(Class::Comment) });
901 }
902
903 let mut classifier =
904 Classifier::new(src, offset.unwrap_or_default(), file_span, decoration_info);
905
906 loop {
907 if let Some(decs) = classifier.decorations.as_mut() {
908 let byte_pos = classifier.byte_pos;
909 let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count();
910 for (_, kind) in decs.starts.drain(0..n_starts) {
911 sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Decoration(kind) });
912 }
913
914 let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count();
915 for _ in decs.ends.drain(0..n_ends) {
916 sink(DUMMY_SP, Highlight::ExitSpan);
917 }
918 }
919
920 if let Some((TokenKind::Colon | TokenKind::Ident, _)) = classifier.tokens.peek()
921 && let Some(nb_items) = classifier.get_full_ident_path()
922 {
923 let start = classifier.byte_pos as usize;
924 let len: usize = iter::from_fn(|| classifier.next())
925 .take(nb_items)
926 .map(|(_, text, _)| text.len())
927 .sum();
928 let text = &classifier.src[start..start + len];
929 classifier.advance(TokenKind::Ident, text, sink, start as u32);
930 } else if let Some((token, text, before)) = classifier.next() {
931 classifier.advance(token, text, sink, before);
932 } else {
933 break;
934 }
935 }
936}
937
938struct Classifier<'src> {
941 tokens: PeekIter<'src>,
942 in_attribute: bool,
943 in_macro: bool,
944 in_macro_nonterminal: bool,
945 byte_pos: u32,
946 file_span: Span,
947 src: &'src str,
948 decorations: Option<Decorations>,
949}
950
951impl<'src> Classifier<'src> {
952 fn new(
955 src: &'src str,
956 byte_pos: usize,
957 file_span: Span,
958 decoration_info: Option<&DecorationInfo>,
959 ) -> Self {
960 Classifier {
961 tokens: PeekIter::new(TokenIter::new(&src[byte_pos..])),
962 in_attribute: false,
963 in_macro: false,
964 in_macro_nonterminal: false,
965 byte_pos: byte_pos as u32,
966 file_span,
967 src,
968 decorations: decoration_info.map(Decorations::new),
969 }
970 }
971
972 fn get_full_ident_path(&mut self) -> Option<usize> {
974 let mut has_ident = false;
975 let mut nb_items = 0;
976
977 let ret = loop {
978 let mut nb = 0;
979 while self.tokens.peek_next_if(|(token, _)| token == TokenKind::Colon).is_some() {
980 nb += 1;
981 nb_items += 1;
982 }
983 if has_ident && nb == 0 {
986 break Some(nb_items);
987 } else if nb != 0 && nb != 2 {
988 if has_ident {
989 break Some(nb_items - 1);
991 } else {
992 break None;
993 }
994 }
995
996 if let Some((TokenKind::Ident, text)) =
997 self.tokens.peek_next_if(|(token, _)| token == TokenKind::Ident)
998 && let symbol = Symbol::intern(text)
999 && (symbol.is_path_segment_keyword() || !is_keyword(symbol))
1000 {
1001 has_ident = true;
1002 nb_items += 1;
1003 } else if nb > 0 && has_ident {
1004 break Some(nb_items - 1);
1006 } else if has_ident {
1007 break Some(nb_items);
1008 } else {
1009 break None;
1010 }
1011 };
1012 self.tokens.stop_peeking();
1013 ret
1014 }
1015
1016 fn next(&mut self) -> Option<(TokenKind, &'src str, u32)> {
1021 if let Some((kind, text)) = self.tokens.next() {
1022 let before = self.byte_pos;
1023 self.byte_pos += text.len() as u32;
1024 Some((kind, text, before))
1025 } else {
1026 None
1027 }
1028 }
1029
1030 fn new_macro_span(
1031 &mut self,
1032 text: &'src str,
1033 sink: &mut dyn FnMut(Span, Highlight<'src>),
1034 before: u32,
1035 file_span: Span,
1036 ) {
1037 self.in_macro = true;
1038 let span = new_span(before, text, file_span);
1039 sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Macro(span) });
1040 sink(span, Highlight::Token { text, class: None });
1041 }
1042
1043 fn advance(
1050 &mut self,
1051 token: TokenKind,
1052 text: &'src str,
1053 sink: &mut dyn FnMut(Span, Highlight<'src>),
1054 before: u32,
1055 ) {
1056 let lookahead = self.peek();
1057 let file_span = self.file_span;
1058 let no_highlight = |sink: &mut dyn FnMut(_, _)| {
1059 sink(new_span(before, text, file_span), Highlight::Token { text, class: None })
1060 };
1061 let whitespace = |sink: &mut dyn FnMut(_, _)| {
1062 let mut start = 0u32;
1063 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
1064 sink(
1065 new_span(before + start, part, file_span),
1066 Highlight::Token { text: part, class: None },
1067 );
1068 start += part.len() as u32;
1069 }
1070 };
1071 let class = match token {
1072 TokenKind::Whitespace => return whitespace(sink),
1073 TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => {
1074 if doc_style.is_some() {
1075 Class::DocComment
1076 } else {
1077 Class::Comment
1078 }
1079 }
1080 TokenKind::Frontmatter { .. } => Class::Comment,
1081 TokenKind::Bang if self.in_macro => {
1084 self.in_macro = false;
1085 sink(new_span(before, text, file_span), Highlight::Token { text, class: None });
1086 sink(DUMMY_SP, Highlight::ExitSpan);
1087 return;
1088 }
1089
1090 TokenKind::Star => match self.tokens.peek() {
1094 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
1095 Some((TokenKind::Ident, "mut")) => {
1096 self.next();
1097 sink(
1098 DUMMY_SP,
1099 Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) },
1100 );
1101 return;
1102 }
1103 Some((TokenKind::Ident, "const")) => {
1104 self.next();
1105 sink(
1106 DUMMY_SP,
1107 Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) },
1108 );
1109 return;
1110 }
1111 _ => Class::RefKeyWord,
1112 },
1113 TokenKind::And => match self.tokens.peek() {
1114 Some((TokenKind::And, _)) => {
1115 self.next();
1116 sink(DUMMY_SP, Highlight::Token { text: "&&", class: None });
1117 return;
1118 }
1119 Some((TokenKind::Eq, _)) => {
1120 self.next();
1121 sink(DUMMY_SP, Highlight::Token { text: "&=", class: None });
1122 return;
1123 }
1124 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
1125 Some((TokenKind::Ident, "mut")) => {
1126 self.next();
1127 sink(
1128 DUMMY_SP,
1129 Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) },
1130 );
1131 return;
1132 }
1133 _ => Class::RefKeyWord,
1134 },
1135
1136 TokenKind::Eq => match lookahead {
1138 Some(TokenKind::Eq) => {
1139 self.next();
1140 sink(DUMMY_SP, Highlight::Token { text: "==", class: None });
1141 return;
1142 }
1143 Some(TokenKind::Gt) => {
1144 self.next();
1145 sink(DUMMY_SP, Highlight::Token { text: "=>", class: None });
1146 return;
1147 }
1148 _ => return no_highlight(sink),
1149 },
1150 TokenKind::Minus if lookahead == Some(TokenKind::Gt) => {
1151 self.next();
1152 sink(DUMMY_SP, Highlight::Token { text: "->", class: None });
1153 return;
1154 }
1155
1156 TokenKind::Minus
1158 | TokenKind::Plus
1159 | TokenKind::Or
1160 | TokenKind::Slash
1161 | TokenKind::Caret
1162 | TokenKind::Percent
1163 | TokenKind::Bang
1164 | TokenKind::Lt
1165 | TokenKind::Gt => return no_highlight(sink),
1166
1167 TokenKind::Dot
1169 | TokenKind::Semi
1170 | TokenKind::Comma
1171 | TokenKind::OpenParen
1172 | TokenKind::CloseParen
1173 | TokenKind::OpenBrace
1174 | TokenKind::CloseBrace
1175 | TokenKind::OpenBracket
1176 | TokenKind::At
1177 | TokenKind::Tilde
1178 | TokenKind::Colon
1179 | TokenKind::Unknown => return no_highlight(sink),
1180
1181 TokenKind::Question => Class::QuestionMark,
1182
1183 TokenKind::Dollar => match lookahead {
1184 Some(TokenKind::Ident) => {
1185 self.in_macro_nonterminal = true;
1186 Class::MacroNonTerminal
1187 }
1188 _ => return no_highlight(sink),
1189 },
1190
1191 TokenKind::Pound => {
1196 match lookahead {
1197 Some(TokenKind::Bang) => {
1199 self.next();
1200 if let Some(TokenKind::OpenBracket) = self.peek() {
1201 self.in_attribute = true;
1202 sink(
1203 new_span(before, text, file_span),
1204 Highlight::EnterSpan { class: Class::Attribute },
1205 );
1206 }
1207 sink(DUMMY_SP, Highlight::Token { text: "#", class: None });
1208 sink(DUMMY_SP, Highlight::Token { text: "!", class: None });
1209 return;
1210 }
1211 Some(TokenKind::OpenBracket) => {
1213 self.in_attribute = true;
1214 sink(
1215 new_span(before, text, file_span),
1216 Highlight::EnterSpan { class: Class::Attribute },
1217 );
1218 }
1219 _ => (),
1220 }
1221 return no_highlight(sink);
1222 }
1223 TokenKind::CloseBracket => {
1224 if self.in_attribute {
1225 self.in_attribute = false;
1226 sink(
1227 new_span(before, text, file_span),
1228 Highlight::Token { text: "]", class: None },
1229 );
1230 sink(DUMMY_SP, Highlight::ExitSpan);
1231 return;
1232 }
1233 return no_highlight(sink);
1234 }
1235 TokenKind::Literal { kind, .. } => match kind {
1236 LiteralKind::Byte { .. }
1238 | LiteralKind::Char { .. }
1239 | LiteralKind::Str { .. }
1240 | LiteralKind::ByteStr { .. }
1241 | LiteralKind::RawStr { .. }
1242 | LiteralKind::RawByteStr { .. }
1243 | LiteralKind::CStr { .. }
1244 | LiteralKind::RawCStr { .. } => Class::String,
1245 LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number,
1247 },
1248 TokenKind::GuardedStrPrefix => return no_highlight(sink),
1249 TokenKind::RawIdent if let Some((TokenKind::Bang, _)) = self.peek_non_trivia() => {
1250 self.new_macro_span(text, sink, before, file_span);
1251 return;
1252 }
1253 TokenKind::Ident if self.in_macro_nonterminal => {
1255 self.in_macro_nonterminal = false;
1256 Class::MacroNonTerminal
1257 }
1258 TokenKind::Ident => {
1259 let span = || new_span(before, text, file_span);
1260
1261 match text {
1262 "ref" | "mut" => Class::RefKeyWord,
1263 "false" | "true" => Class::Bool,
1264 "self" | "Self" => Class::Self_(span()),
1265 "Option" | "Result" => Class::PreludeTy(span()),
1266 "Some" | "None" | "Ok" | "Err" => Class::PreludeVal(span()),
1267 _ if self.is_weak_keyword(text) || is_keyword(Symbol::intern(text)) => {
1268 if !NON_MACRO_KEYWORDS.contains(&text)
1272 && matches!(self.peek_non_trivia(), Some((TokenKind::Bang, _)))
1273 {
1274 self.new_macro_span(text, sink, before, file_span);
1275 return;
1276 }
1277 Class::KeyWord
1278 }
1279 _ if matches!(self.peek_non_trivia(), Some((TokenKind::Bang, _))) => {
1282 self.new_macro_span(text, sink, before, file_span);
1283 return;
1284 }
1285 _ => Class::Ident(span()),
1286 }
1287 }
1288 TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => {
1289 Class::Ident(new_span(before, text, file_span))
1290 }
1291 TokenKind::Lifetime { .. }
1292 | TokenKind::RawLifetime
1293 | TokenKind::UnknownPrefixLifetime => Class::Lifetime,
1294 TokenKind::Eof => panic!("Eof in advance"),
1295 };
1296 let mut start = 0u32;
1299 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
1300 sink(
1301 new_span(before + start, part, file_span),
1302 Highlight::Token { text: part, class: Some(class) },
1303 );
1304 start += part.len() as u32;
1305 }
1306 }
1307
1308 fn is_weak_keyword(&mut self, text: &str) -> bool {
1309 let matches = match text {
1314 "auto" => |text| text == "trait", "pin" => |text| text == "const" || text == "mut", "raw" => |text| text == "const" || text == "mut", "safe" => |text| text == "fn" || text == "extern", "union" => |_| true, _ => return false,
1320 };
1321 matches!(self.peek_non_trivia(), Some((TokenKind::Ident, text)) if matches(text))
1322 }
1323
1324 fn peek(&mut self) -> Option<TokenKind> {
1325 self.tokens.peek().map(|(kind, _)| kind)
1326 }
1327
1328 fn peek_non_trivia(&mut self) -> Option<(TokenKind, &str)> {
1329 while let Some(token @ (kind, _)) = self.tokens.peek_next() {
1330 if let TokenKind::Whitespace
1331 | TokenKind::LineComment { doc_style: None }
1332 | TokenKind::BlockComment { doc_style: None, .. } = kind
1333 {
1334 continue;
1335 }
1336 self.tokens.stop_peeking();
1337 return Some(token);
1338 }
1339 self.tokens.stop_peeking();
1340 None
1341 }
1342}
1343
1344fn is_keyword(symbol: Symbol) -> bool {
1345 symbol.is_reserved(|| Edition::Edition2024)
1347}
1348
1349fn generate_link_to_def(
1350 out: &mut impl Write,
1351 text_s: &str,
1352 klass: Class,
1353 href_context: &Option<HrefContext<'_, '_>>,
1354 def_span: Span,
1355 open_tag: bool,
1356) -> bool {
1357 if let Some(href_context) = href_context
1358 && let Some(href) =
1359 href_context.context.shared.span_correspondence_map.get(&def_span).and_then(|href| {
1360 let context = href_context.context;
1361 match href {
1367 LinkFromSrc::Local(span) => {
1368 context.href_from_span_relative(*span, &href_context.current_href)
1369 }
1370 LinkFromSrc::External(def_id) => {
1371 format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1372 .ok()
1373 .map(|HrefInfo { url, .. }| url)
1374 }
1375 LinkFromSrc::Primitive(prim) => format::href_with_root_path(
1376 PrimitiveType::primitive_locations(context.tcx())[prim],
1377 context,
1378 Some(href_context.root_path),
1379 )
1380 .ok()
1381 .map(|HrefInfo { url, .. }| url),
1382 LinkFromSrc::Doc(def_id) => {
1383 format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1384 .ok()
1385 .map(|HrefInfo { url, .. }| url)
1386 }
1387 }
1388 })
1389 {
1390 if !open_tag {
1391 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1394 } else {
1395 let klass_s = klass.as_html();
1396 if klass_s.is_empty() {
1397 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1398 } else {
1399 write!(out, "<a class=\"{klass_s}\" href=\"{href}\">{text_s}").unwrap();
1400 }
1401 }
1402 return true;
1403 }
1404 false
1405}
1406
1407fn string_without_closing_tag<T: Display>(
1417 out: &mut impl Write,
1418 text: T,
1419 klass: Option<Class>,
1420 href_context: &Option<HrefContext<'_, '_>>,
1421 open_tag: bool,
1422) -> Option<&'static str> {
1423 let Some(klass) = klass else {
1424 write!(out, "{text}").unwrap();
1425 return None;
1426 };
1427 let Some(def_span) = klass.get_span() else {
1428 if !open_tag {
1429 write!(out, "{text}").unwrap();
1430 return None;
1431 }
1432 write!(out, "<span class=\"{klass}\">{text}", klass = klass.as_html()).unwrap();
1433 return Some("</span>");
1434 };
1435
1436 let mut added_links = false;
1437 let mut text_s = text.to_string();
1438 if text_s.contains("::") {
1439 let mut span = def_span.with_hi(def_span.lo());
1440 text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| {
1441 span = span.with_hi(span.hi() + BytePos(t.len() as _));
1442 match t {
1443 "::" => write!(&mut path, "::"),
1444 "self" | "Self" => write!(
1445 &mut path,
1446 "<span class=\"{klass}\">{t}</span>",
1447 klass = Class::Self_(DUMMY_SP).as_html(),
1448 ),
1449 "crate" | "super" => {
1450 write!(
1451 &mut path,
1452 "<span class=\"{klass}\">{t}</span>",
1453 klass = Class::KeyWord.as_html(),
1454 )
1455 }
1456 t => {
1457 if !t.is_empty()
1458 && generate_link_to_def(&mut path, t, klass, href_context, span, open_tag)
1459 {
1460 added_links = true;
1461 write!(&mut path, "</a>")
1462 } else {
1463 write!(&mut path, "{t}")
1464 }
1465 }
1466 }
1467 .expect("Failed to build source HTML path");
1468 span = span.with_lo(span.lo() + BytePos(t.len() as _));
1469 path
1470 });
1471 }
1472
1473 if !added_links && generate_link_to_def(out, &text_s, klass, href_context, def_span, open_tag) {
1474 return Some("</a>");
1475 }
1476 if !open_tag {
1477 out.write_str(&text_s).unwrap();
1478 return None;
1479 }
1480 let klass_s = klass.as_html();
1481 if klass_s.is_empty() {
1482 out.write_str(&text_s).unwrap();
1483 Some("")
1484 } else {
1485 write!(out, "<span class=\"{klass_s}\">{text_s}").unwrap();
1486 Some("</span>")
1487 }
1488}
1489
1490#[cfg(test)]
1491mod tests;