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 stop_peeking(&mut self) {
836 self.peek_pos = 0;
837 }
838}
839
840impl<'a> Iterator for PeekIter<'a> {
841 type Item = (TokenKind, &'a str);
842 fn next(&mut self) -> Option<Self::Item> {
843 if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() }
844 }
845}
846
847struct Decorations {
849 starts: Vec<(u32, &'static str)>,
850 ends: Vec<u32>,
851}
852
853impl Decorations {
854 fn new(info: &DecorationInfo) -> Self {
855 let (mut starts, mut ends): (Vec<_>, Vec<_>) = info
857 .0
858 .iter()
859 .flat_map(|(&kind, ranges)| ranges.iter().map(move |&(lo, hi)| ((lo, kind), hi)))
860 .unzip();
861
862 starts.sort_by_key(|(lo, _)| *lo);
864 ends.sort();
865
866 Decorations { starts, ends }
867 }
868}
869
870fn new_span(lo: u32, text: &str, file_span: Span) -> Span {
872 let hi = lo + text.len() as u32;
873 let file_lo = file_span.lo();
874 file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi))
875}
876
877fn classify<'src>(
878 src: &'src str,
879 file_span: Span,
880 decoration_info: Option<&DecorationInfo>,
881 sink: &mut dyn FnMut(Span, Highlight<'src>),
882) {
883 let offset = rustc_lexer::strip_shebang(src);
884
885 if let Some(offset) = offset {
886 sink(DUMMY_SP, Highlight::Token { text: &src[..offset], class: Some(Class::Comment) });
887 }
888
889 let mut classifier =
890 Classifier::new(src, offset.unwrap_or_default(), file_span, decoration_info);
891
892 loop {
893 if let Some(decs) = classifier.decorations.as_mut() {
894 let byte_pos = classifier.byte_pos;
895 let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count();
896 for (_, kind) in decs.starts.drain(0..n_starts) {
897 sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Decoration(kind) });
898 }
899
900 let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count();
901 for _ in decs.ends.drain(0..n_ends) {
902 sink(DUMMY_SP, Highlight::ExitSpan);
903 }
904 }
905
906 if let Some((TokenKind::Colon | TokenKind::Ident, _)) = classifier.tokens.peek() {
907 let tokens = classifier.get_full_ident_path();
908 for &(token, start, end) in &tokens {
909 let text = &classifier.src[start..end];
910 classifier.advance(token, text, sink, start as u32);
911 classifier.byte_pos += text.len() as u32;
912 }
913 if !tokens.is_empty() {
914 continue;
915 }
916 }
917 if let Some((token, text, before)) = classifier.next() {
918 classifier.advance(token, text, sink, before);
919 } else {
920 break;
921 }
922 }
923}
924
925struct Classifier<'src> {
928 tokens: PeekIter<'src>,
929 in_attribute: bool,
930 in_macro: bool,
931 in_macro_nonterminal: bool,
932 byte_pos: u32,
933 file_span: Span,
934 src: &'src str,
935 decorations: Option<Decorations>,
936}
937
938impl<'src> Classifier<'src> {
939 fn new(
942 src: &'src str,
943 byte_pos: usize,
944 file_span: Span,
945 decoration_info: Option<&DecorationInfo>,
946 ) -> Self {
947 Classifier {
948 tokens: PeekIter::new(TokenIter::new(&src[byte_pos..])),
949 in_attribute: false,
950 in_macro: false,
951 in_macro_nonterminal: false,
952 byte_pos: byte_pos as u32,
953 file_span,
954 src,
955 decorations: decoration_info.map(Decorations::new),
956 }
957 }
958
959 fn get_full_ident_path(&mut self) -> Vec<(TokenKind, usize, usize)> {
961 let start = self.byte_pos as usize;
962 let mut pos = start;
963 let mut has_ident = false;
964
965 loop {
966 let mut nb = 0;
967 while let Some((TokenKind::Colon, _)) = self.tokens.peek() {
968 self.tokens.next();
969 nb += 1;
970 }
971 if has_ident && nb == 0 {
974 return vec![(TokenKind::Ident, start, pos)];
975 } else if nb != 0 && nb != 2 {
976 if has_ident {
977 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
978 } else {
979 return vec![(TokenKind::Colon, start, pos + nb)];
980 }
981 }
982
983 if let Some((TokenKind::Ident, text)) = self.tokens.peek()
984 && let symbol = Symbol::intern(text)
985 && (symbol.is_path_segment_keyword() || !is_keyword(symbol))
986 {
987 pos += text.len() + nb;
989 has_ident = true;
990 self.tokens.next();
991 } else if nb > 0 && has_ident {
992 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
993 } else if nb > 0 {
994 return vec![(TokenKind::Colon, start, start + nb)];
995 } else if has_ident {
996 return vec![(TokenKind::Ident, start, pos)];
997 } else {
998 return Vec::new();
999 }
1000 }
1001 }
1002
1003 fn next(&mut self) -> Option<(TokenKind, &'src str, u32)> {
1008 if let Some((kind, text)) = self.tokens.next() {
1009 let before = self.byte_pos;
1010 self.byte_pos += text.len() as u32;
1011 Some((kind, text, before))
1012 } else {
1013 None
1014 }
1015 }
1016
1017 fn new_macro_span(
1018 &mut self,
1019 text: &'src str,
1020 sink: &mut dyn FnMut(Span, Highlight<'src>),
1021 before: u32,
1022 file_span: Span,
1023 ) {
1024 self.in_macro = true;
1025 let span = new_span(before, text, file_span);
1026 sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Macro(span) });
1027 sink(span, Highlight::Token { text, class: None });
1028 }
1029
1030 fn advance(
1037 &mut self,
1038 token: TokenKind,
1039 text: &'src str,
1040 sink: &mut dyn FnMut(Span, Highlight<'src>),
1041 before: u32,
1042 ) {
1043 let lookahead = self.peek();
1044 let file_span = self.file_span;
1045 let no_highlight = |sink: &mut dyn FnMut(_, _)| {
1046 sink(new_span(before, text, file_span), Highlight::Token { text, class: None })
1047 };
1048 let whitespace = |sink: &mut dyn FnMut(_, _)| {
1049 let mut start = 0u32;
1050 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
1051 sink(
1052 new_span(before + start, part, file_span),
1053 Highlight::Token { text: part, class: None },
1054 );
1055 start += part.len() as u32;
1056 }
1057 };
1058 let class = match token {
1059 TokenKind::Whitespace => return whitespace(sink),
1060 TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => {
1061 if doc_style.is_some() {
1062 Class::DocComment
1063 } else {
1064 Class::Comment
1065 }
1066 }
1067 TokenKind::Frontmatter { .. } => Class::Comment,
1068 TokenKind::Bang if self.in_macro => {
1071 self.in_macro = false;
1072 sink(new_span(before, text, file_span), Highlight::Token { text, class: None });
1073 sink(DUMMY_SP, Highlight::ExitSpan);
1074 return;
1075 }
1076
1077 TokenKind::Star => match self.tokens.peek() {
1081 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
1082 Some((TokenKind::Ident, "mut")) => {
1083 self.next();
1084 sink(
1085 DUMMY_SP,
1086 Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) },
1087 );
1088 return;
1089 }
1090 Some((TokenKind::Ident, "const")) => {
1091 self.next();
1092 sink(
1093 DUMMY_SP,
1094 Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) },
1095 );
1096 return;
1097 }
1098 _ => Class::RefKeyWord,
1099 },
1100 TokenKind::And => match self.tokens.peek() {
1101 Some((TokenKind::And, _)) => {
1102 self.next();
1103 sink(DUMMY_SP, Highlight::Token { text: "&&", class: None });
1104 return;
1105 }
1106 Some((TokenKind::Eq, _)) => {
1107 self.next();
1108 sink(DUMMY_SP, Highlight::Token { text: "&=", class: None });
1109 return;
1110 }
1111 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
1112 Some((TokenKind::Ident, "mut")) => {
1113 self.next();
1114 sink(
1115 DUMMY_SP,
1116 Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) },
1117 );
1118 return;
1119 }
1120 _ => Class::RefKeyWord,
1121 },
1122
1123 TokenKind::Eq => match lookahead {
1125 Some(TokenKind::Eq) => {
1126 self.next();
1127 sink(DUMMY_SP, Highlight::Token { text: "==", class: None });
1128 return;
1129 }
1130 Some(TokenKind::Gt) => {
1131 self.next();
1132 sink(DUMMY_SP, Highlight::Token { text: "=>", class: None });
1133 return;
1134 }
1135 _ => return no_highlight(sink),
1136 },
1137 TokenKind::Minus if lookahead == Some(TokenKind::Gt) => {
1138 self.next();
1139 sink(DUMMY_SP, Highlight::Token { text: "->", class: None });
1140 return;
1141 }
1142
1143 TokenKind::Minus
1145 | TokenKind::Plus
1146 | TokenKind::Or
1147 | TokenKind::Slash
1148 | TokenKind::Caret
1149 | TokenKind::Percent
1150 | TokenKind::Bang
1151 | TokenKind::Lt
1152 | TokenKind::Gt => return no_highlight(sink),
1153
1154 TokenKind::Dot
1156 | TokenKind::Semi
1157 | TokenKind::Comma
1158 | TokenKind::OpenParen
1159 | TokenKind::CloseParen
1160 | TokenKind::OpenBrace
1161 | TokenKind::CloseBrace
1162 | TokenKind::OpenBracket
1163 | TokenKind::At
1164 | TokenKind::Tilde
1165 | TokenKind::Colon
1166 | TokenKind::Unknown => return no_highlight(sink),
1167
1168 TokenKind::Question => Class::QuestionMark,
1169
1170 TokenKind::Dollar => match lookahead {
1171 Some(TokenKind::Ident) => {
1172 self.in_macro_nonterminal = true;
1173 Class::MacroNonTerminal
1174 }
1175 _ => return no_highlight(sink),
1176 },
1177
1178 TokenKind::Pound => {
1183 match lookahead {
1184 Some(TokenKind::Bang) => {
1186 self.next();
1187 if let Some(TokenKind::OpenBracket) = self.peek() {
1188 self.in_attribute = true;
1189 sink(
1190 new_span(before, text, file_span),
1191 Highlight::EnterSpan { class: Class::Attribute },
1192 );
1193 }
1194 sink(DUMMY_SP, Highlight::Token { text: "#", class: None });
1195 sink(DUMMY_SP, Highlight::Token { text: "!", class: None });
1196 return;
1197 }
1198 Some(TokenKind::OpenBracket) => {
1200 self.in_attribute = true;
1201 sink(
1202 new_span(before, text, file_span),
1203 Highlight::EnterSpan { class: Class::Attribute },
1204 );
1205 }
1206 _ => (),
1207 }
1208 return no_highlight(sink);
1209 }
1210 TokenKind::CloseBracket => {
1211 if self.in_attribute {
1212 self.in_attribute = false;
1213 sink(
1214 new_span(before, text, file_span),
1215 Highlight::Token { text: "]", class: None },
1216 );
1217 sink(DUMMY_SP, Highlight::ExitSpan);
1218 return;
1219 }
1220 return no_highlight(sink);
1221 }
1222 TokenKind::Literal { kind, .. } => match kind {
1223 LiteralKind::Byte { .. }
1225 | LiteralKind::Char { .. }
1226 | LiteralKind::Str { .. }
1227 | LiteralKind::ByteStr { .. }
1228 | LiteralKind::RawStr { .. }
1229 | LiteralKind::RawByteStr { .. }
1230 | LiteralKind::CStr { .. }
1231 | LiteralKind::RawCStr { .. } => Class::String,
1232 LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number,
1234 },
1235 TokenKind::GuardedStrPrefix => return no_highlight(sink),
1236 TokenKind::RawIdent if let Some((TokenKind::Bang, _)) = self.peek_non_trivia() => {
1237 self.new_macro_span(text, sink, before, file_span);
1238 return;
1239 }
1240 TokenKind::Ident if self.in_macro_nonterminal => {
1242 self.in_macro_nonterminal = false;
1243 Class::MacroNonTerminal
1244 }
1245 TokenKind::Ident => {
1246 let file_span = self.file_span;
1247 let span = || new_span(before, text, file_span);
1248
1249 match text {
1250 "ref" | "mut" => Class::RefKeyWord,
1251 "false" | "true" => Class::Bool,
1252 "self" | "Self" => Class::Self_(span()),
1253 "Option" | "Result" => Class::PreludeTy(span()),
1254 "Some" | "None" | "Ok" | "Err" => Class::PreludeVal(span()),
1255 _ if self.is_weak_keyword(text) || is_keyword(Symbol::intern(text)) => {
1256 if !NON_MACRO_KEYWORDS.contains(&text)
1260 && matches!(self.peek_non_trivia(), Some((TokenKind::Bang, _)))
1261 {
1262 self.new_macro_span(text, sink, before, file_span);
1263 return;
1264 }
1265 Class::KeyWord
1266 }
1267 _ if matches!(self.peek_non_trivia(), Some((TokenKind::Bang, _))) => {
1270 self.new_macro_span(text, sink, before, file_span);
1271 return;
1272 }
1273 _ => Class::Ident(span()),
1274 }
1275 }
1276 TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => {
1277 Class::Ident(new_span(before, text, file_span))
1278 }
1279 TokenKind::Lifetime { .. }
1280 | TokenKind::RawLifetime
1281 | TokenKind::UnknownPrefixLifetime => Class::Lifetime,
1282 TokenKind::Eof => panic!("Eof in advance"),
1283 };
1284 let mut start = 0u32;
1287 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
1288 sink(
1289 new_span(before + start, part, file_span),
1290 Highlight::Token { text: part, class: Some(class) },
1291 );
1292 start += part.len() as u32;
1293 }
1294 }
1295
1296 fn is_weak_keyword(&mut self, text: &str) -> bool {
1297 let matches = match text {
1302 "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,
1308 };
1309 matches!(self.peek_non_trivia(), Some((TokenKind::Ident, text)) if matches(text))
1310 }
1311
1312 fn peek(&mut self) -> Option<TokenKind> {
1313 self.tokens.peek().map(|(kind, _)| kind)
1314 }
1315
1316 fn peek_non_trivia(&mut self) -> Option<(TokenKind, &str)> {
1317 while let Some(token @ (kind, _)) = self.tokens.peek_next() {
1318 if let TokenKind::Whitespace
1319 | TokenKind::LineComment { doc_style: None }
1320 | TokenKind::BlockComment { doc_style: None, .. } = kind
1321 {
1322 continue;
1323 }
1324 self.tokens.stop_peeking();
1325 return Some(token);
1326 }
1327 self.tokens.stop_peeking();
1328 None
1329 }
1330}
1331
1332fn is_keyword(symbol: Symbol) -> bool {
1333 symbol.is_reserved(|| Edition::Edition2024)
1335}
1336
1337fn generate_link_to_def(
1338 out: &mut impl Write,
1339 text_s: &str,
1340 klass: Class,
1341 href_context: &Option<HrefContext<'_, '_>>,
1342 def_span: Span,
1343 open_tag: bool,
1344) -> bool {
1345 if let Some(href_context) = href_context
1346 && let Some(href) =
1347 href_context.context.shared.span_correspondence_map.get(&def_span).and_then(|href| {
1348 let context = href_context.context;
1349 match href {
1355 LinkFromSrc::Local(span) => {
1356 context.href_from_span_relative(*span, &href_context.current_href)
1357 }
1358 LinkFromSrc::External(def_id) => {
1359 format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1360 .ok()
1361 .map(|HrefInfo { url, .. }| url)
1362 }
1363 LinkFromSrc::Primitive(prim) => format::href_with_root_path(
1364 PrimitiveType::primitive_locations(context.tcx())[prim],
1365 context,
1366 Some(href_context.root_path),
1367 )
1368 .ok()
1369 .map(|HrefInfo { url, .. }| url),
1370 LinkFromSrc::Doc(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 }
1376 })
1377 {
1378 if !open_tag {
1379 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1382 } else {
1383 let klass_s = klass.as_html();
1384 if klass_s.is_empty() {
1385 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1386 } else {
1387 write!(out, "<a class=\"{klass_s}\" href=\"{href}\">{text_s}").unwrap();
1388 }
1389 }
1390 return true;
1391 }
1392 false
1393}
1394
1395fn string_without_closing_tag<T: Display>(
1405 out: &mut impl Write,
1406 text: T,
1407 klass: Option<Class>,
1408 href_context: &Option<HrefContext<'_, '_>>,
1409 open_tag: bool,
1410) -> Option<&'static str> {
1411 let Some(klass) = klass else {
1412 write!(out, "{text}").unwrap();
1413 return None;
1414 };
1415 let Some(def_span) = klass.get_span() else {
1416 if !open_tag {
1417 write!(out, "{text}").unwrap();
1418 return None;
1419 }
1420 write!(out, "<span class=\"{klass}\">{text}", klass = klass.as_html()).unwrap();
1421 return Some("</span>");
1422 };
1423
1424 let mut added_links = false;
1425 let mut text_s = text.to_string();
1426 if text_s.contains("::") {
1427 let mut span = def_span.with_hi(def_span.lo());
1428 text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| {
1429 span = span.with_hi(span.hi() + BytePos(t.len() as _));
1430 match t {
1431 "::" => write!(&mut path, "::"),
1432 "self" | "Self" => write!(
1433 &mut path,
1434 "<span class=\"{klass}\">{t}</span>",
1435 klass = Class::Self_(DUMMY_SP).as_html(),
1436 ),
1437 "crate" | "super" => {
1438 write!(
1439 &mut path,
1440 "<span class=\"{klass}\">{t}</span>",
1441 klass = Class::KeyWord.as_html(),
1442 )
1443 }
1444 t => {
1445 if !t.is_empty()
1446 && generate_link_to_def(&mut path, t, klass, href_context, span, open_tag)
1447 {
1448 added_links = true;
1449 write!(&mut path, "</a>")
1450 } else {
1451 write!(&mut path, "{t}")
1452 }
1453 }
1454 }
1455 .expect("Failed to build source HTML path");
1456 span = span.with_lo(span.lo() + BytePos(t.len() as _));
1457 path
1458 });
1459 }
1460
1461 if !added_links && generate_link_to_def(out, &text_s, klass, href_context, def_span, open_tag) {
1462 return Some("</a>");
1463 }
1464 if !open_tag {
1465 out.write_str(&text_s).unwrap();
1466 return None;
1467 }
1468 let klass_s = klass.as_html();
1469 if klass_s.is_empty() {
1470 out.write_str(&text_s).unwrap();
1471 Some("")
1472 } else {
1473 write!(out, "<span class=\"{klass_s}\">{text_s}").unwrap();
1474 Some("</span>")
1475 }
1476}
1477
1478#[cfg(test)]
1479mod tests;