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