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