1use std::collections::VecDeque;
9use std::fmt::{Display, Write};
10
11use rustc_data_structures::fx::FxIndexMap;
12use rustc_lexer::{Cursor, FrontmatterAllowed, LiteralKind, TokenKind};
13use rustc_span::edition::Edition;
14use rustc_span::symbol::Symbol;
15use rustc_span::{BytePos, DUMMY_SP, Span};
16
17use super::format::{self, write_str};
18use crate::clean::PrimitiveType;
19use crate::html::escape::EscapeBodyText;
20use crate::html::render::{Context, LinkFromSrc};
21
22pub(crate) struct HrefContext<'a, 'tcx> {
24 pub(crate) context: &'a Context<'tcx>,
25 pub(crate) file_span: Span,
27 pub(crate) root_path: &'a str,
30 pub(crate) current_href: String,
32}
33
34#[derive(Default)]
37pub(crate) struct DecorationInfo(pub(crate) FxIndexMap<&'static str, Vec<(u32, u32)>>);
38
39#[derive(Eq, PartialEq, Clone, Copy)]
40pub(crate) enum Tooltip {
41 Ignore,
42 CompileFail,
43 ShouldPanic,
44 Edition(Edition),
45 None,
46}
47
48pub(crate) fn render_example_with_highlighting(
50 src: &str,
51 out: &mut String,
52 tooltip: Tooltip,
53 playground_button: Option<&str>,
54 extra_classes: &[String],
55) {
56 write_header(out, "rust-example-rendered", None, tooltip, extra_classes);
57 write_code(out, src, None, None, None);
58 write_footer(out, playground_button);
59}
60
61fn write_header(
62 out: &mut String,
63 class: &str,
64 extra_content: Option<&str>,
65 tooltip: Tooltip,
66 extra_classes: &[String],
67) {
68 write_str(
69 out,
70 format_args!(
71 "<div class=\"example-wrap{}\">",
72 match tooltip {
73 Tooltip::Ignore => " ignore",
74 Tooltip::CompileFail => " compile_fail",
75 Tooltip::ShouldPanic => " should_panic",
76 Tooltip::Edition(_) => " edition",
77 Tooltip::None => "",
78 }
79 ),
80 );
81
82 if tooltip != Tooltip::None {
83 let edition_code;
84 write_str(
85 out,
86 format_args!(
87 "<a href=\"#\" class=\"tooltip\" title=\"{}\">ⓘ</a>",
88 match tooltip {
89 Tooltip::Ignore => "This example is not tested",
90 Tooltip::CompileFail => "This example deliberately fails to compile",
91 Tooltip::ShouldPanic => "This example panics",
92 Tooltip::Edition(edition) => {
93 edition_code = format!("This example runs with edition {edition}");
94 &edition_code
95 }
96 Tooltip::None => unreachable!(),
97 }
98 ),
99 );
100 }
101
102 if let Some(extra) = extra_content {
103 out.push_str(extra);
104 }
105 if class.is_empty() {
106 write_str(
107 out,
108 format_args!(
109 "<pre class=\"rust{}{}\">",
110 if extra_classes.is_empty() { "" } else { " " },
111 extra_classes.join(" ")
112 ),
113 );
114 } else {
115 write_str(
116 out,
117 format_args!(
118 "<pre class=\"rust {class}{}{}\">",
119 if extra_classes.is_empty() { "" } else { " " },
120 extra_classes.join(" ")
121 ),
122 );
123 }
124 write_str(out, format_args!("<code>"));
125}
126
127fn can_merge(class1: Option<Class>, class2: Option<Class>, text: &str) -> bool {
136 match (class1, class2) {
137 (Some(c1), Some(c2)) => c1.is_equal_to(c2),
138 (Some(Class::Ident(_)), None) | (None, Some(Class::Ident(_))) => true,
139 (Some(Class::Macro(_)), _) => false,
140 (Some(_), None) | (None, Some(_)) => text.trim().is_empty(),
141 (None, None) => true,
142 }
143}
144
145struct TokenHandler<'a, 'tcx, F: Write> {
148 out: &'a mut F,
149 closing_tags: Vec<(&'static str, Class)>,
151 pending_exit_span: Option<Class>,
154 current_class: Option<Class>,
157 pending_elems: Vec<(&'a str, Option<Class>)>,
160 href_context: Option<HrefContext<'a, 'tcx>>,
161 write_line_number: fn(&mut F, u32, &'static str),
162}
163
164impl<F: Write> TokenHandler<'_, '_, F> {
165 fn handle_exit_span(&mut self) {
166 let class = self.closing_tags.last().expect("ExitSpan without EnterSpan").1;
169 self.write_pending_elems(Some(class));
171
172 exit_span(self.out, self.closing_tags.pop().expect("ExitSpan without EnterSpan").0);
173 self.pending_exit_span = None;
174 }
175
176 fn write_pending_elems(&mut self, current_class: Option<Class>) -> bool {
187 if self.pending_elems.is_empty() {
188 return false;
189 }
190 if let Some((_, parent_class)) = self.closing_tags.last()
191 && can_merge(current_class, Some(*parent_class), "")
192 {
193 for (text, class) in self.pending_elems.iter() {
194 string(
195 self.out,
196 EscapeBodyText(text),
197 *class,
198 &self.href_context,
199 false,
200 self.write_line_number,
201 );
202 }
203 } else {
204 let close_tag = if self.pending_elems.len() > 1
207 && let Some(current_class) = current_class
208 && !matches!(current_class, Class::PreludeTy(_))
211 {
212 Some(enter_span(self.out, current_class, &self.href_context))
213 } else {
214 None
215 };
216 for (text, class) in self.pending_elems.iter() {
217 string(
218 self.out,
219 EscapeBodyText(text),
220 *class,
221 &self.href_context,
222 close_tag.is_none(),
223 self.write_line_number,
224 );
225 }
226 if let Some(close_tag) = close_tag {
227 exit_span(self.out, close_tag);
228 }
229 }
230 self.pending_elems.clear();
231 true
232 }
233
234 #[inline]
235 fn write_line_number(&mut self, line: u32, extra: &'static str) {
236 (self.write_line_number)(self.out, line, extra);
237 }
238}
239
240impl<F: Write> Drop for TokenHandler<'_, '_, F> {
241 fn drop(&mut self) {
243 if self.pending_exit_span.is_some() {
244 self.handle_exit_span();
245 } else {
246 self.write_pending_elems(self.current_class);
247 }
248 }
249}
250
251fn write_scraped_line_number(out: &mut impl Write, line: u32, extra: &'static str) {
252 write!(out, "{extra}<span data-nosnippet>{line}</span>",).unwrap();
255}
256
257fn write_line_number(out: &mut impl Write, line: u32, extra: &'static str) {
258 write!(out, "{extra}<a href=#{line} id={line} data-nosnippet>{line}</a>",).unwrap();
261}
262
263fn empty_line_number(out: &mut impl Write, _: u32, extra: &'static str) {
264 out.write_str(extra).unwrap();
265}
266
267#[derive(Clone, Copy)]
268pub(super) struct LineInfo {
269 pub(super) start_line: u32,
270 max_lines: u32,
271 pub(super) is_scraped_example: bool,
272}
273
274impl LineInfo {
275 pub(super) fn new(max_lines: u32) -> Self {
276 Self { start_line: 1, max_lines: max_lines + 1, is_scraped_example: false }
277 }
278
279 pub(super) fn new_scraped(max_lines: u32, start_line: u32) -> Self {
280 Self {
281 start_line: start_line + 1,
282 max_lines: max_lines + start_line + 1,
283 is_scraped_example: true,
284 }
285 }
286}
287
288pub(super) fn write_code(
300 out: &mut impl Write,
301 src: &str,
302 href_context: Option<HrefContext<'_, '_>>,
303 decoration_info: Option<&DecorationInfo>,
304 line_info: Option<LineInfo>,
305) {
306 let src = src.replace("\r\n", "\n");
308 let mut token_handler = TokenHandler {
309 out,
310 closing_tags: Vec::new(),
311 pending_exit_span: None,
312 current_class: None,
313 pending_elems: Vec::new(),
314 href_context,
315 write_line_number: match line_info {
316 Some(line_info) => {
317 if line_info.is_scraped_example {
318 write_scraped_line_number
319 } else {
320 write_line_number
321 }
322 }
323 None => empty_line_number,
324 },
325 };
326
327 let (mut line, max_lines) = if let Some(line_info) = line_info {
328 token_handler.write_line_number(line_info.start_line, "");
329 (line_info.start_line, line_info.max_lines)
330 } else {
331 (0, u32::MAX)
332 };
333
334 Classifier::new(
335 &src,
336 token_handler.href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
337 decoration_info,
338 )
339 .highlight(&mut |highlight| {
340 match highlight {
341 Highlight::Token { text, class } => {
342 let need_current_class_update = if let Some(pending) =
345 token_handler.pending_exit_span
346 && !can_merge(Some(pending), class, text)
347 {
348 token_handler.handle_exit_span();
349 true
350 } else if !can_merge(token_handler.current_class, class, text) {
353 token_handler.write_pending_elems(token_handler.current_class);
354 true
355 } else {
356 token_handler.current_class.is_none()
357 };
358
359 if need_current_class_update {
360 token_handler.current_class = class.map(Class::dummy);
361 }
362 if text == "\n" {
363 line += 1;
364 if line < max_lines {
365 token_handler.pending_elems.push((text, Some(Class::Backline(line))));
366 }
367 } else {
368 token_handler.pending_elems.push((text, class));
369 }
370 }
371 Highlight::EnterSpan { class } => {
372 let mut should_add = true;
373 if let Some(pending_exit_span) = token_handler.pending_exit_span {
374 if class.is_equal_to(pending_exit_span) {
375 should_add = false;
376 } else {
377 token_handler.handle_exit_span();
378 }
379 } else {
380 if token_handler.write_pending_elems(token_handler.current_class) {
382 token_handler.current_class = None;
383 }
384 }
385 if should_add {
386 let closing_tag =
387 enter_span(token_handler.out, class, &token_handler.href_context);
388 token_handler.closing_tags.push((closing_tag, class));
389 }
390
391 token_handler.current_class = None;
392 token_handler.pending_exit_span = None;
393 }
394 Highlight::ExitSpan => {
395 token_handler.current_class = None;
396 token_handler.pending_exit_span = Some(
397 token_handler
398 .closing_tags
399 .last()
400 .as_ref()
401 .expect("ExitSpan without EnterSpan")
402 .1,
403 );
404 }
405 };
406 });
407}
408
409fn write_footer(out: &mut String, playground_button: Option<&str>) {
410 write_str(out, format_args_nl!("</code></pre>{}</div>", playground_button.unwrap_or_default()));
411}
412
413#[derive(Clone, Copy, Debug, Eq, PartialEq)]
415enum Class {
416 Comment,
417 DocComment,
418 Attribute,
419 KeyWord,
420 RefKeyWord,
422 Self_(Span),
423 Macro(Span),
424 MacroNonTerminal,
425 String,
426 Number,
427 Bool,
428 Ident(Span),
430 Lifetime,
431 PreludeTy(Span),
432 PreludeVal(Span),
433 QuestionMark,
434 Decoration(&'static str),
435 Backline(u32),
436}
437
438impl Class {
439 fn is_equal_to(self, other: Self) -> bool {
444 match (self, other) {
445 (Self::Self_(_), Self::Self_(_))
446 | (Self::Macro(_), Self::Macro(_))
447 | (Self::Ident(_), Self::Ident(_)) => true,
448 (Self::Decoration(c1), Self::Decoration(c2)) => c1 == c2,
449 (x, y) => x == y,
450 }
451 }
452
453 fn dummy(self) -> Self {
456 match self {
457 Self::Self_(_) => Self::Self_(DUMMY_SP),
458 Self::Macro(_) => Self::Macro(DUMMY_SP),
459 Self::Ident(_) => Self::Ident(DUMMY_SP),
460 s => s,
461 }
462 }
463
464 fn as_html(self) -> &'static str {
466 match self {
467 Class::Comment => "comment",
468 Class::DocComment => "doccomment",
469 Class::Attribute => "attr",
470 Class::KeyWord => "kw",
471 Class::RefKeyWord => "kw-2",
472 Class::Self_(_) => "self",
473 Class::Macro(_) => "macro",
474 Class::MacroNonTerminal => "macro-nonterminal",
475 Class::String => "string",
476 Class::Number => "number",
477 Class::Bool => "bool-val",
478 Class::Ident(_) => "",
479 Class::Lifetime => "lifetime",
480 Class::PreludeTy(_) => "prelude-ty",
481 Class::PreludeVal(_) => "prelude-val",
482 Class::QuestionMark => "question-mark",
483 Class::Decoration(kind) => kind,
484 Class::Backline(_) => "",
485 }
486 }
487
488 fn get_span(self) -> Option<Span> {
491 match self {
492 Self::Ident(sp)
493 | Self::Self_(sp)
494 | Self::Macro(sp)
495 | Self::PreludeTy(sp)
496 | Self::PreludeVal(sp) => Some(sp),
497 Self::Comment
498 | Self::DocComment
499 | Self::Attribute
500 | Self::KeyWord
501 | Self::RefKeyWord
502 | Self::MacroNonTerminal
503 | Self::String
504 | Self::Number
505 | Self::Bool
506 | Self::Lifetime
507 | Self::QuestionMark
508 | Self::Decoration(_)
509 | Self::Backline(_) => None,
510 }
511 }
512}
513
514#[derive(Debug)]
515enum Highlight<'a> {
516 Token { text: &'a str, class: Option<Class> },
517 EnterSpan { class: Class },
518 ExitSpan,
519}
520
521struct TokenIter<'a> {
522 src: &'a str,
523 cursor: Cursor<'a>,
524}
525
526impl<'a> Iterator for TokenIter<'a> {
527 type Item = (TokenKind, &'a str);
528 fn next(&mut self) -> Option<(TokenKind, &'a str)> {
529 let token = self.cursor.advance_token();
530 if token.kind == TokenKind::Eof {
531 return None;
532 }
533 let (text, rest) = self.src.split_at(token.len as usize);
534 self.src = rest;
535 Some((token.kind, text))
536 }
537}
538
539fn get_real_ident_class(text: &str, allow_path_keywords: bool) -> Option<Class> {
541 let ignore: &[&str] =
542 if allow_path_keywords { &["self", "Self", "super", "crate"] } else { &["self", "Self"] };
543 if ignore.iter().any(|k| *k == text) {
544 return None;
545 }
546 Some(match text {
547 "ref" | "mut" => Class::RefKeyWord,
548 "false" | "true" => Class::Bool,
549 _ if Symbol::intern(text).is_reserved(|| Edition::Edition2021) => Class::KeyWord,
550 _ => return None,
551 })
552}
553
554struct PeekIter<'a> {
560 stored: VecDeque<(TokenKind, &'a str)>,
561 peek_pos: usize,
563 iter: TokenIter<'a>,
564}
565
566impl<'a> PeekIter<'a> {
567 fn new(iter: TokenIter<'a>) -> Self {
568 Self { stored: VecDeque::new(), peek_pos: 0, iter }
569 }
570 fn peek(&mut self) -> Option<&(TokenKind, &'a str)> {
572 if self.stored.is_empty()
573 && let Some(next) = self.iter.next()
574 {
575 self.stored.push_back(next);
576 }
577 self.stored.front()
578 }
579 fn peek_next(&mut self) -> Option<&(TokenKind, &'a str)> {
581 self.peek_pos += 1;
582 if self.peek_pos - 1 < self.stored.len() {
583 self.stored.get(self.peek_pos - 1)
584 } else if let Some(next) = self.iter.next() {
585 self.stored.push_back(next);
586 self.stored.back()
587 } else {
588 None
589 }
590 }
591}
592
593impl<'a> Iterator for PeekIter<'a> {
594 type Item = (TokenKind, &'a str);
595 fn next(&mut self) -> Option<Self::Item> {
596 self.peek_pos = 0;
597 if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() }
598 }
599}
600
601struct Decorations {
603 starts: Vec<(u32, &'static str)>,
604 ends: Vec<u32>,
605}
606
607impl Decorations {
608 fn new(info: &DecorationInfo) -> Self {
609 let (mut starts, mut ends): (Vec<_>, Vec<_>) = info
611 .0
612 .iter()
613 .flat_map(|(&kind, ranges)| ranges.iter().map(move |&(lo, hi)| ((lo, kind), hi)))
614 .unzip();
615
616 starts.sort_by_key(|(lo, _)| *lo);
618 ends.sort();
619
620 Decorations { starts, ends }
621 }
622}
623
624struct Classifier<'src> {
627 tokens: PeekIter<'src>,
628 in_attribute: bool,
629 in_macro: bool,
630 in_macro_nonterminal: bool,
631 byte_pos: u32,
632 file_span: Span,
633 src: &'src str,
634 decorations: Option<Decorations>,
635}
636
637impl<'src> Classifier<'src> {
638 fn new(src: &'src str, file_span: Span, decoration_info: Option<&DecorationInfo>) -> Self {
641 let tokens =
642 PeekIter::new(TokenIter { src, cursor: Cursor::new(src, FrontmatterAllowed::Yes) });
643 let decorations = decoration_info.map(Decorations::new);
644 Classifier {
645 tokens,
646 in_attribute: false,
647 in_macro: false,
648 in_macro_nonterminal: false,
649 byte_pos: 0,
650 file_span,
651 src,
652 decorations,
653 }
654 }
655
656 fn new_span(&self, lo: u32, text: &str) -> Span {
658 let hi = lo + text.len() as u32;
659 let file_lo = self.file_span.lo();
660 self.file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi))
661 }
662
663 fn get_full_ident_path(&mut self) -> Vec<(TokenKind, usize, usize)> {
665 let start = self.byte_pos as usize;
666 let mut pos = start;
667 let mut has_ident = false;
668
669 loop {
670 let mut nb = 0;
671 while let Some((TokenKind::Colon, _)) = self.tokens.peek() {
672 self.tokens.next();
673 nb += 1;
674 }
675 if has_ident && nb == 0 {
678 return vec![(TokenKind::Ident, start, pos)];
679 } else if nb != 0 && nb != 2 {
680 if has_ident {
681 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
682 } else {
683 return vec![(TokenKind::Colon, start, pos + nb)];
684 }
685 }
686
687 if let Some((None, text)) = self.tokens.peek().map(|(token, text)| {
688 if *token == TokenKind::Ident {
689 let class = get_real_ident_class(text, true);
690 (class, text)
691 } else {
692 (Some(Class::Comment), text)
694 }
695 }) {
696 pos += text.len() + nb;
698 has_ident = true;
699 self.tokens.next();
700 } else if nb > 0 && has_ident {
701 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
702 } else if nb > 0 {
703 return vec![(TokenKind::Colon, start, start + nb)];
704 } else if has_ident {
705 return vec![(TokenKind::Ident, start, pos)];
706 } else {
707 return Vec::new();
708 }
709 }
710 }
711
712 fn next(&mut self) -> Option<(TokenKind, &'src str, u32)> {
717 if let Some((kind, text)) = self.tokens.next() {
718 let before = self.byte_pos;
719 self.byte_pos += text.len() as u32;
720 Some((kind, text, before))
721 } else {
722 None
723 }
724 }
725
726 fn highlight(mut self, sink: &mut dyn FnMut(Highlight<'src>)) {
732 loop {
733 if let Some(decs) = self.decorations.as_mut() {
734 let byte_pos = self.byte_pos;
735 let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count();
736 for (_, kind) in decs.starts.drain(0..n_starts) {
737 sink(Highlight::EnterSpan { class: Class::Decoration(kind) });
738 }
739
740 let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count();
741 for _ in decs.ends.drain(0..n_ends) {
742 sink(Highlight::ExitSpan);
743 }
744 }
745
746 if self
747 .tokens
748 .peek()
749 .map(|t| matches!(t.0, TokenKind::Colon | TokenKind::Ident))
750 .unwrap_or(false)
751 {
752 let tokens = self.get_full_ident_path();
753 for (token, start, end) in &tokens {
754 let text = &self.src[*start..*end];
755 self.advance(*token, text, sink, *start as u32);
756 self.byte_pos += text.len() as u32;
757 }
758 if !tokens.is_empty() {
759 continue;
760 }
761 }
762 if let Some((token, text, before)) = self.next() {
763 self.advance(token, text, sink, before);
764 } else {
765 break;
766 }
767 }
768 }
769
770 fn advance(
777 &mut self,
778 token: TokenKind,
779 text: &'src str,
780 sink: &mut dyn FnMut(Highlight<'src>),
781 before: u32,
782 ) {
783 let lookahead = self.peek();
784 let no_highlight = |sink: &mut dyn FnMut(_)| sink(Highlight::Token { text, class: None });
785 let whitespace = |sink: &mut dyn FnMut(_)| {
786 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
787 sink(Highlight::Token { text: part, class: None });
788 }
789 };
790 let class = match token {
791 TokenKind::Whitespace => return whitespace(sink),
792 TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => {
793 if doc_style.is_some() {
794 Class::DocComment
795 } else {
796 Class::Comment
797 }
798 }
799 TokenKind::Bang if self.in_macro => {
802 self.in_macro = false;
803 sink(Highlight::Token { text, class: None });
804 sink(Highlight::ExitSpan);
805 return;
806 }
807
808 TokenKind::Star => match self.tokens.peek() {
812 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
813 Some((TokenKind::Ident, "mut")) => {
814 self.next();
815 sink(Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) });
816 return;
817 }
818 Some((TokenKind::Ident, "const")) => {
819 self.next();
820 sink(Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) });
821 return;
822 }
823 _ => Class::RefKeyWord,
824 },
825 TokenKind::And => match self.tokens.peek() {
826 Some((TokenKind::And, _)) => {
827 self.next();
828 sink(Highlight::Token { text: "&&", class: None });
829 return;
830 }
831 Some((TokenKind::Eq, _)) => {
832 self.next();
833 sink(Highlight::Token { text: "&=", class: None });
834 return;
835 }
836 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
837 Some((TokenKind::Ident, "mut")) => {
838 self.next();
839 sink(Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) });
840 return;
841 }
842 _ => Class::RefKeyWord,
843 },
844
845 TokenKind::Eq => match lookahead {
847 Some(TokenKind::Eq) => {
848 self.next();
849 sink(Highlight::Token { text: "==", class: None });
850 return;
851 }
852 Some(TokenKind::Gt) => {
853 self.next();
854 sink(Highlight::Token { text: "=>", class: None });
855 return;
856 }
857 _ => return no_highlight(sink),
858 },
859 TokenKind::Minus if lookahead == Some(TokenKind::Gt) => {
860 self.next();
861 sink(Highlight::Token { text: "->", class: None });
862 return;
863 }
864
865 TokenKind::Minus
867 | TokenKind::Plus
868 | TokenKind::Or
869 | TokenKind::Slash
870 | TokenKind::Caret
871 | TokenKind::Percent
872 | TokenKind::Bang
873 | TokenKind::Lt
874 | TokenKind::Gt => return no_highlight(sink),
875
876 TokenKind::Dot
878 | TokenKind::Semi
879 | TokenKind::Comma
880 | TokenKind::OpenParen
881 | TokenKind::CloseParen
882 | TokenKind::OpenBrace
883 | TokenKind::CloseBrace
884 | TokenKind::OpenBracket
885 | TokenKind::At
886 | TokenKind::Tilde
887 | TokenKind::Colon
888 | TokenKind::Frontmatter { .. }
889 | TokenKind::Unknown => return no_highlight(sink),
890
891 TokenKind::Question => Class::QuestionMark,
892
893 TokenKind::Dollar => match lookahead {
894 Some(TokenKind::Ident) => {
895 self.in_macro_nonterminal = true;
896 Class::MacroNonTerminal
897 }
898 _ => return no_highlight(sink),
899 },
900
901 TokenKind::Pound => {
906 match lookahead {
907 Some(TokenKind::Bang) => {
909 self.next();
910 if let Some(TokenKind::OpenBracket) = self.peek() {
911 self.in_attribute = true;
912 sink(Highlight::EnterSpan { class: Class::Attribute });
913 }
914 sink(Highlight::Token { text: "#", class: None });
915 sink(Highlight::Token { text: "!", class: None });
916 return;
917 }
918 Some(TokenKind::OpenBracket) => {
920 self.in_attribute = true;
921 sink(Highlight::EnterSpan { class: Class::Attribute });
922 }
923 _ => (),
924 }
925 return no_highlight(sink);
926 }
927 TokenKind::CloseBracket => {
928 if self.in_attribute {
929 self.in_attribute = false;
930 sink(Highlight::Token { text: "]", class: None });
931 sink(Highlight::ExitSpan);
932 return;
933 }
934 return no_highlight(sink);
935 }
936 TokenKind::Literal { kind, .. } => match kind {
937 LiteralKind::Byte { .. }
939 | LiteralKind::Char { .. }
940 | LiteralKind::Str { .. }
941 | LiteralKind::ByteStr { .. }
942 | LiteralKind::RawStr { .. }
943 | LiteralKind::RawByteStr { .. }
944 | LiteralKind::CStr { .. }
945 | LiteralKind::RawCStr { .. } => Class::String,
946 LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number,
948 },
949 TokenKind::GuardedStrPrefix => return no_highlight(sink),
950 TokenKind::Ident | TokenKind::RawIdent if lookahead == Some(TokenKind::Bang) => {
951 self.in_macro = true;
952 sink(Highlight::EnterSpan { class: Class::Macro(self.new_span(before, text)) });
953 sink(Highlight::Token { text, class: None });
954 return;
955 }
956 TokenKind::Ident => match get_real_ident_class(text, false) {
957 None => match text {
958 "Option" | "Result" => Class::PreludeTy(self.new_span(before, text)),
959 "Some" | "None" | "Ok" | "Err" => {
960 Class::PreludeVal(self.new_span(before, text))
961 }
962 "union" if self.check_if_is_union_keyword() => Class::KeyWord,
965 _ if self.in_macro_nonterminal => {
966 self.in_macro_nonterminal = false;
967 Class::MacroNonTerminal
968 }
969 "self" | "Self" => Class::Self_(self.new_span(before, text)),
970 _ => Class::Ident(self.new_span(before, text)),
971 },
972 Some(c) => c,
973 },
974 TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => {
975 Class::Ident(self.new_span(before, text))
976 }
977 TokenKind::Lifetime { .. }
978 | TokenKind::RawLifetime
979 | TokenKind::UnknownPrefixLifetime => Class::Lifetime,
980 TokenKind::Eof => panic!("Eof in advance"),
981 };
982 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
985 sink(Highlight::Token { text: part, class: Some(class) });
986 }
987 }
988
989 fn peek(&mut self) -> Option<TokenKind> {
990 self.tokens.peek().map(|(token_kind, _text)| *token_kind)
991 }
992
993 fn check_if_is_union_keyword(&mut self) -> bool {
994 while let Some(kind) = self.tokens.peek_next().map(|(token_kind, _text)| token_kind) {
995 if *kind == TokenKind::Whitespace {
996 continue;
997 }
998 return *kind == TokenKind::Ident;
999 }
1000 false
1001 }
1002}
1003
1004fn enter_span(
1007 out: &mut impl Write,
1008 klass: Class,
1009 href_context: &Option<HrefContext<'_, '_>>,
1010) -> &'static str {
1011 string_without_closing_tag(out, "", Some(klass), href_context, true).expect(
1012 "internal error: enter_span was called with Some(klass) but did not return a \
1013 closing HTML tag",
1014 )
1015}
1016
1017fn exit_span(out: &mut impl Write, closing_tag: &str) {
1019 out.write_str(closing_tag).unwrap();
1020}
1021
1022fn string<T: Display, W: Write>(
1039 out: &mut W,
1040 text: T,
1041 klass: Option<Class>,
1042 href_context: &Option<HrefContext<'_, '_>>,
1043 open_tag: bool,
1044 write_line_number_callback: fn(&mut W, u32, &'static str),
1045) {
1046 if let Some(Class::Backline(line)) = klass {
1047 write_line_number_callback(out, line, "\n");
1048 } else if let Some(closing_tag) =
1049 string_without_closing_tag(out, text, klass, href_context, open_tag)
1050 {
1051 out.write_str(closing_tag).unwrap();
1052 }
1053}
1054
1055fn string_without_closing_tag<T: Display>(
1065 out: &mut impl Write,
1066 text: T,
1067 klass: Option<Class>,
1068 href_context: &Option<HrefContext<'_, '_>>,
1069 open_tag: bool,
1070) -> Option<&'static str> {
1071 let Some(klass) = klass else {
1072 write!(out, "{text}").unwrap();
1073 return None;
1074 };
1075 let Some(def_span) = klass.get_span() else {
1076 if !open_tag {
1077 write!(out, "{text}").unwrap();
1078 return None;
1079 }
1080 write!(out, "<span class=\"{klass}\">{text}", klass = klass.as_html()).unwrap();
1081 return Some("</span>");
1082 };
1083
1084 let mut text_s = text.to_string();
1085 if text_s.contains("::") {
1086 text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| {
1087 match t {
1088 "self" | "Self" => write!(
1089 &mut path,
1090 "<span class=\"{klass}\">{t}</span>",
1091 klass = Class::Self_(DUMMY_SP).as_html(),
1092 ),
1093 "crate" | "super" => {
1094 write!(
1095 &mut path,
1096 "<span class=\"{klass}\">{t}</span>",
1097 klass = Class::KeyWord.as_html(),
1098 )
1099 }
1100 t => write!(&mut path, "{t}"),
1101 }
1102 .expect("Failed to build source HTML path");
1103 path
1104 });
1105 }
1106
1107 if let Some(href_context) = href_context
1108 && let Some(href) = href_context.context.shared.span_correspondence_map.get(&def_span)
1109 && let Some(href) = {
1110 let context = href_context.context;
1111 match href {
1117 LinkFromSrc::Local(span) => {
1118 context.href_from_span_relative(*span, &href_context.current_href)
1119 }
1120 LinkFromSrc::External(def_id) => {
1121 format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1122 .ok()
1123 .map(|(url, _, _)| url)
1124 }
1125 LinkFromSrc::Primitive(prim) => format::href_with_root_path(
1126 PrimitiveType::primitive_locations(context.tcx())[prim],
1127 context,
1128 Some(href_context.root_path),
1129 )
1130 .ok()
1131 .map(|(url, _, _)| url),
1132 LinkFromSrc::Doc(def_id) => {
1133 format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1134 .ok()
1135 .map(|(doc_link, _, _)| doc_link)
1136 }
1137 }
1138 }
1139 {
1140 if !open_tag {
1141 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1144 } else {
1145 let klass_s = klass.as_html();
1146 if klass_s.is_empty() {
1147 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1148 } else {
1149 write!(out, "<a class=\"{klass_s}\" href=\"{href}\">{text_s}").unwrap();
1150 }
1151 }
1152 return Some("</a>");
1153 }
1154 if !open_tag {
1155 write!(out, "{}", text_s).unwrap();
1156 return None;
1157 }
1158 let klass_s = klass.as_html();
1159 if klass_s.is_empty() {
1160 out.write_str(&text_s).unwrap();
1161 Some("")
1162 } else {
1163 write!(out, "<span class=\"{klass_s}\">{text_s}").unwrap();
1164 Some("</span>")
1165 }
1166}
1167
1168#[cfg(test)]
1169mod tests;