1use std::borrow::Cow;
9use std::collections::VecDeque;
10use std::fmt::{self, Display, Write};
11
12use rustc_data_structures::fx::FxIndexMap;
13use rustc_lexer::{Cursor, FrontmatterAllowed, LiteralKind, TokenKind};
14use rustc_span::edition::Edition;
15use rustc_span::symbol::Symbol;
16use rustc_span::{BytePos, DUMMY_SP, Span};
17
18use super::format::{self, write_str};
19use crate::clean::PrimitiveType;
20use crate::html::escape::EscapeBodyText;
21use crate::html::macro_expansion::ExpandedCode;
22use crate::html::render::{Context, LinkFromSrc};
23
24pub(crate) struct HrefContext<'a, 'tcx> {
26 pub(crate) context: &'a Context<'tcx>,
27 pub(crate) file_span: Span,
29 pub(crate) root_path: &'a str,
32 pub(crate) current_href: String,
34}
35
36#[derive(Default)]
39pub(crate) struct DecorationInfo(pub(crate) FxIndexMap<&'static str, Vec<(u32, u32)>>);
40
41#[derive(Eq, PartialEq, Clone)]
42pub(crate) enum Tooltip {
43 IgnoreAll,
44 IgnoreSome(Vec<String>),
45 CompileFail,
46 ShouldPanic,
47 Edition(Edition),
48 None,
49}
50
51pub(crate) fn render_example_with_highlighting(
53 src: &str,
54 out: &mut String,
55 tooltip: Tooltip,
56 playground_button: Option<&str>,
57 extra_classes: &[String],
58) {
59 write_header(out, "rust-example-rendered", None, tooltip, extra_classes);
60 write_code(out, src, None, None, None);
61 write_footer(out, playground_button);
62}
63
64fn write_header(
65 out: &mut String,
66 class: &str,
67 extra_content: Option<&str>,
68 tooltip: Tooltip,
69 extra_classes: &[String],
70) {
71 write_str(
72 out,
73 format_args!(
74 "<div class=\"example-wrap{}\">",
75 match tooltip {
76 Tooltip::IgnoreAll | Tooltip::IgnoreSome(_) => " ignore",
77 Tooltip::CompileFail => " compile_fail",
78 Tooltip::ShouldPanic => " should_panic",
79 Tooltip::Edition(_) => " edition",
80 Tooltip::None => "",
81 }
82 ),
83 );
84
85 if tooltip != Tooltip::None {
86 let tooltip = fmt::from_fn(|f| match &tooltip {
87 Tooltip::IgnoreAll => f.write_str("This example is not tested"),
88 Tooltip::IgnoreSome(platforms) => {
89 f.write_str("This example is not tested on ")?;
90 match &platforms[..] {
91 [] => unreachable!(),
92 [platform] => f.write_str(platform)?,
93 [first, second] => write!(f, "{first} or {second}")?,
94 [platforms @ .., last] => {
95 for platform in platforms {
96 write!(f, "{platform}, ")?;
97 }
98 write!(f, "or {last}")?;
99 }
100 }
101 Ok(())
102 }
103 Tooltip::CompileFail => f.write_str("This example deliberately fails to compile"),
104 Tooltip::ShouldPanic => f.write_str("This example panics"),
105 Tooltip::Edition(edition) => write!(f, "This example runs with edition {edition}"),
106 Tooltip::None => unreachable!(),
107 });
108 write_str(out, format_args!("<a href=\"#\" class=\"tooltip\" title=\"{tooltip}\">ⓘ</a>"));
109 }
110
111 if let Some(extra) = extra_content {
112 out.push_str(extra);
113 }
114 if class.is_empty() {
115 write_str(
116 out,
117 format_args!(
118 "<pre class=\"rust{}{}\">",
119 if extra_classes.is_empty() { "" } else { " " },
120 extra_classes.join(" ")
121 ),
122 );
123 } else {
124 write_str(
125 out,
126 format_args!(
127 "<pre class=\"rust {class}{}{}\">",
128 if extra_classes.is_empty() { "" } else { " " },
129 extra_classes.join(" ")
130 ),
131 );
132 }
133 write_str(out, format_args!("<code>"));
134}
135
136fn can_merge(class1: Option<Class>, class2: Option<Class>, text: &str) -> bool {
145 match (class1, class2) {
146 (Some(c1), Some(c2)) => c1.is_equal_to(c2),
147 (Some(Class::Ident(_)), None) | (None, Some(Class::Ident(_))) => true,
148 (Some(Class::Macro(_)), _) => false,
149 (Some(_), None) | (None, Some(_)) => text.trim().is_empty(),
150 (None, None) => true,
151 }
152}
153
154struct TokenHandler<'a, 'tcx, F: Write> {
157 out: &'a mut F,
158 closing_tags: Vec<(&'static str, Class)>,
160 pending_exit_span: Option<Class>,
163 current_class: Option<Class>,
166 pending_elems: Vec<(Cow<'a, str>, Option<Class>)>,
169 href_context: Option<HrefContext<'a, 'tcx>>,
170 write_line_number: fn(&mut F, u32, &'static str),
171}
172
173impl<F: Write> std::fmt::Debug for TokenHandler<'_, '_, F> {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 f.debug_struct("TokenHandler")
176 .field("closing_tags", &self.closing_tags)
177 .field("pending_exit_span", &self.pending_exit_span)
178 .field("current_class", &self.current_class)
179 .field("pending_elems", &self.pending_elems)
180 .finish()
181 }
182}
183
184impl<F: Write> TokenHandler<'_, '_, F> {
185 fn handle_exit_span(&mut self) {
186 let class = self.closing_tags.last().expect("ExitSpan without EnterSpan").1;
189 self.write_pending_elems(Some(class));
191
192 exit_span(self.out, self.closing_tags.pop().expect("ExitSpan without EnterSpan").0);
193 self.pending_exit_span = None;
194 }
195
196 fn write_pending_elems(&mut self, current_class: Option<Class>) -> bool {
207 if self.pending_elems.is_empty() {
208 return false;
209 }
210 if let Some((_, parent_class)) = self.closing_tags.last()
211 && can_merge(current_class, Some(*parent_class), "")
212 {
213 for (text, class) in self.pending_elems.iter() {
214 string(
215 self.out,
216 EscapeBodyText(text),
217 *class,
218 &self.href_context,
219 false,
220 self.write_line_number,
221 );
222 }
223 } else {
224 let close_tag = if self.pending_elems.len() > 1
227 && let Some(current_class) = current_class
228 && !matches!(current_class, Class::PreludeTy(_))
231 {
232 Some(enter_span(self.out, current_class, &self.href_context))
233 } else {
234 None
235 };
236 let last_pending =
239 self.pending_elems.pop_if(|(_, class)| *class == Some(Class::Expansion));
240 for (text, class) in self.pending_elems.iter() {
241 string(
242 self.out,
243 EscapeBodyText(text),
244 *class,
245 &self.href_context,
246 close_tag.is_none(),
247 self.write_line_number,
248 );
249 }
250 if let Some(close_tag) = close_tag {
251 exit_span(self.out, close_tag);
252 }
253 if let Some((text, class)) = last_pending {
254 string(
255 self.out,
256 EscapeBodyText(&text),
257 class,
258 &self.href_context,
259 close_tag.is_none(),
260 self.write_line_number,
261 );
262 }
263 }
264 self.pending_elems.clear();
265 true
266 }
267
268 #[inline]
269 fn write_line_number(&mut self, line: u32, extra: &'static str) {
270 (self.write_line_number)(self.out, line, extra);
271 }
272}
273
274impl<F: Write> Drop for TokenHandler<'_, '_, F> {
275 fn drop(&mut self) {
277 if self.pending_exit_span.is_some() {
278 self.handle_exit_span();
279 } else {
280 self.write_pending_elems(self.current_class);
281 }
282 }
283}
284
285fn write_scraped_line_number(out: &mut impl Write, line: u32, extra: &'static str) {
286 write!(out, "{extra}<span data-nosnippet>{line}</span>",).unwrap();
289}
290
291fn write_line_number(out: &mut impl Write, line: u32, extra: &'static str) {
292 write!(out, "{extra}<a href=#{line} id={line} data-nosnippet>{line}</a>",).unwrap();
295}
296
297fn empty_line_number(out: &mut impl Write, _: u32, extra: &'static str) {
298 out.write_str(extra).unwrap();
299}
300
301fn get_next_expansion(
302 expanded_codes: &[ExpandedCode],
303 line: u32,
304 span: Span,
305) -> Option<&ExpandedCode> {
306 expanded_codes.iter().find(|code| code.start_line == line && code.span.lo() > span.lo())
307}
308
309fn get_expansion<'a, W: Write>(
310 token_handler: &mut TokenHandler<'_, '_, W>,
311 expanded_codes: &'a [ExpandedCode],
312 line: u32,
313 span: Span,
314) -> Option<&'a ExpandedCode> {
315 if let Some(expanded_code) = get_next_expansion(expanded_codes, line, span) {
316 let (closing, reopening) = if let Some(current_class) = token_handler.current_class
317 && let class = current_class.as_html()
318 && !class.is_empty()
319 {
320 ("</span>", format!("<span class=\"{class}\">"))
321 } else {
322 ("", String::new())
323 };
324 let id = format!("expand-{line}");
325 token_handler.pending_elems.push((
326 Cow::Owned(format!(
327 "{closing}\
328<span class=expansion>\
329 <input id={id} \
330 tabindex=0 \
331 type=checkbox \
332 aria-label=\"Collapse/expand macro\" \
333 title=\"\"Collapse/expand macro\">{reopening}",
334 )),
335 Some(Class::Expansion),
336 ));
337 Some(expanded_code)
338 } else {
339 None
340 }
341}
342
343fn start_expansion(out: &mut Vec<(Cow<'_, str>, Option<Class>)>, expanded_code: &ExpandedCode) {
344 out.push((
345 Cow::Owned(format!(
346 "<span class=expanded>{}</span><span class=original>",
347 expanded_code.code,
348 )),
349 Some(Class::Expansion),
350 ));
351}
352
353fn end_expansion<'a, W: Write>(
354 token_handler: &mut TokenHandler<'_, '_, W>,
355 expanded_codes: &'a [ExpandedCode],
356 expansion_start_tags: &[(&'static str, Class)],
357 line: u32,
358 span: Span,
359) -> Option<&'a ExpandedCode> {
360 if let Some(expanded_code) = get_next_expansion(expanded_codes, line, span) {
361 token_handler.pending_elems.push((Cow::Borrowed("</span>"), Some(Class::Expansion)));
363 return Some(expanded_code);
364 }
365 if expansion_start_tags.is_empty() && token_handler.closing_tags.is_empty() {
366 token_handler.pending_elems.push((Cow::Borrowed("</span></span>"), Some(Class::Expansion)));
368 return None;
369 }
370
371 let mut out = String::new();
374 let mut end = String::new();
375
376 let mut closing_tags = token_handler.closing_tags.iter().peekable();
377 let mut start_closing_tags = expansion_start_tags.iter().peekable();
378
379 while let (Some(tag), Some(start_tag)) = (closing_tags.peek(), start_closing_tags.peek())
380 && tag == start_tag
381 {
382 closing_tags.next();
383 start_closing_tags.next();
384 }
385 for (tag, class) in start_closing_tags.chain(closing_tags) {
386 out.push_str(tag);
387 end.push_str(&format!("<span class=\"{}\">", class.as_html()));
388 }
389 token_handler
390 .pending_elems
391 .push((Cow::Owned(format!("</span></span>{out}{end}")), Some(Class::Expansion)));
392 None
393}
394
395#[derive(Clone, Copy)]
396pub(super) struct LineInfo {
397 pub(super) start_line: u32,
398 max_lines: u32,
399 pub(super) is_scraped_example: bool,
400}
401
402impl LineInfo {
403 pub(super) fn new(max_lines: u32) -> Self {
404 Self { start_line: 1, max_lines: max_lines + 1, is_scraped_example: false }
405 }
406
407 pub(super) fn new_scraped(max_lines: u32, start_line: u32) -> Self {
408 Self {
409 start_line: start_line + 1,
410 max_lines: max_lines + start_line + 1,
411 is_scraped_example: true,
412 }
413 }
414}
415
416pub(super) fn write_code(
428 out: &mut impl Write,
429 src: &str,
430 href_context: Option<HrefContext<'_, '_>>,
431 decoration_info: Option<&DecorationInfo>,
432 line_info: Option<LineInfo>,
433) {
434 let src = src.replace("\r\n", "\n");
436 let mut token_handler = TokenHandler {
437 out,
438 closing_tags: Vec::new(),
439 pending_exit_span: None,
440 current_class: None,
441 pending_elems: Vec::with_capacity(20),
442 href_context,
443 write_line_number: match line_info {
444 Some(line_info) => {
445 if line_info.is_scraped_example {
446 write_scraped_line_number
447 } else {
448 write_line_number
449 }
450 }
451 None => empty_line_number,
452 },
453 };
454
455 let (mut line, max_lines) = if let Some(line_info) = line_info {
456 token_handler.write_line_number(line_info.start_line, "");
457 (line_info.start_line, line_info.max_lines)
458 } else {
459 (0, u32::MAX)
460 };
461
462 let (expanded_codes, file_span) = match token_handler.href_context.as_ref().and_then(|c| {
463 let expanded_codes = c.context.shared.expanded_codes.get(&c.file_span.lo())?;
464 Some((expanded_codes, c.file_span))
465 }) {
466 Some((expanded_codes, file_span)) => (expanded_codes.as_slice(), file_span),
467 None => (&[] as &[ExpandedCode], DUMMY_SP),
468 };
469 let mut current_expansion = get_expansion(&mut token_handler, expanded_codes, line, file_span);
470 token_handler.write_pending_elems(None);
471 let mut expansion_start_tags = Vec::new();
472
473 Classifier::new(
474 &src,
475 token_handler.href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
476 decoration_info,
477 )
478 .highlight(&mut |span, highlight| {
479 match highlight {
480 Highlight::Token { text, class } => {
481 let need_current_class_update = if let Some(pending) =
484 token_handler.pending_exit_span
485 && !can_merge(Some(pending), class, text)
486 {
487 token_handler.handle_exit_span();
488 true
489 } else if !can_merge(token_handler.current_class, class, text) {
492 token_handler.write_pending_elems(token_handler.current_class);
493 true
494 } else {
495 token_handler.current_class.is_none()
496 };
497
498 if need_current_class_update {
499 token_handler.current_class = class.map(Class::dummy);
500 }
501 if text == "\n" {
502 line += 1;
503 if line < max_lines {
504 token_handler
505 .pending_elems
506 .push((Cow::Borrowed(text), Some(Class::Backline(line))));
507 }
508 if current_expansion.is_none() {
509 current_expansion =
510 get_expansion(&mut token_handler, expanded_codes, line, span);
511 expansion_start_tags = token_handler.closing_tags.clone();
512 }
513 if let Some(ref current_expansion) = current_expansion
514 && current_expansion.span.lo() == span.hi()
515 {
516 start_expansion(&mut token_handler.pending_elems, current_expansion);
517 }
518 } else {
519 token_handler.pending_elems.push((Cow::Borrowed(text), class));
520
521 let mut need_end = false;
522 if let Some(ref current_expansion) = current_expansion {
523 if current_expansion.span.lo() == span.hi() {
524 start_expansion(&mut token_handler.pending_elems, current_expansion);
525 } else if current_expansion.end_line == line
526 && span.hi() >= current_expansion.span.hi()
527 {
528 need_end = true;
529 }
530 }
531 if need_end {
532 current_expansion = end_expansion(
533 &mut token_handler,
534 expanded_codes,
535 &expansion_start_tags,
536 line,
537 span,
538 );
539 }
540 }
541 }
542 Highlight::EnterSpan { class } => {
543 let mut should_add = true;
544 if let Some(pending_exit_span) = token_handler.pending_exit_span {
545 if class.is_equal_to(pending_exit_span) {
546 should_add = false;
547 } else {
548 token_handler.handle_exit_span();
549 }
550 } else {
551 if token_handler.write_pending_elems(token_handler.current_class) {
553 token_handler.current_class = None;
554 }
555 }
556 if should_add {
557 let closing_tag =
558 enter_span(token_handler.out, class, &token_handler.href_context);
559 token_handler.closing_tags.push((closing_tag, class));
560 }
561
562 token_handler.current_class = None;
563 token_handler.pending_exit_span = None;
564 }
565 Highlight::ExitSpan => {
566 token_handler.current_class = None;
567 token_handler.pending_exit_span = Some(
568 token_handler
569 .closing_tags
570 .last()
571 .as_ref()
572 .expect("ExitSpan without EnterSpan")
573 .1,
574 );
575 }
576 };
577 });
578}
579
580fn write_footer(out: &mut String, playground_button: Option<&str>) {
581 write_str(out, format_args_nl!("</code></pre>{}</div>", playground_button.unwrap_or_default()));
582}
583
584#[derive(Clone, Copy, Debug, Eq, PartialEq)]
586enum Class {
587 Comment,
588 DocComment,
589 Attribute,
590 KeyWord,
591 RefKeyWord,
593 Self_(Span),
594 Macro(Span),
595 MacroNonTerminal,
596 String,
597 Number,
598 Bool,
599 Ident(Span),
601 Lifetime,
602 PreludeTy(Span),
603 PreludeVal(Span),
604 QuestionMark,
605 Decoration(&'static str),
606 Backline(u32),
607 Expansion,
609}
610
611impl Class {
612 fn is_equal_to(self, other: Self) -> bool {
617 match (self, other) {
618 (Self::Self_(_), Self::Self_(_))
619 | (Self::Macro(_), Self::Macro(_))
620 | (Self::Ident(_), Self::Ident(_)) => true,
621 (Self::Decoration(c1), Self::Decoration(c2)) => c1 == c2,
622 (x, y) => x == y,
623 }
624 }
625
626 fn dummy(self) -> Self {
629 match self {
630 Self::Self_(_) => Self::Self_(DUMMY_SP),
631 Self::Macro(_) => Self::Macro(DUMMY_SP),
632 Self::Ident(_) => Self::Ident(DUMMY_SP),
633 s => s,
634 }
635 }
636
637 fn as_html(self) -> &'static str {
639 match self {
640 Class::Comment => "comment",
641 Class::DocComment => "doccomment",
642 Class::Attribute => "attr",
643 Class::KeyWord => "kw",
644 Class::RefKeyWord => "kw-2",
645 Class::Self_(_) => "self",
646 Class::Macro(_) => "macro",
647 Class::MacroNonTerminal => "macro-nonterminal",
648 Class::String => "string",
649 Class::Number => "number",
650 Class::Bool => "bool-val",
651 Class::Ident(_) => "",
652 Class::Lifetime => "lifetime",
653 Class::PreludeTy(_) => "prelude-ty",
654 Class::PreludeVal(_) => "prelude-val",
655 Class::QuestionMark => "question-mark",
656 Class::Decoration(kind) => kind,
657 Class::Backline(_) => "",
658 Class::Expansion => "",
659 }
660 }
661
662 fn get_span(self) -> Option<Span> {
665 match self {
666 Self::Ident(sp)
667 | Self::Self_(sp)
668 | Self::Macro(sp)
669 | Self::PreludeTy(sp)
670 | Self::PreludeVal(sp) => Some(sp),
671 Self::Comment
672 | Self::DocComment
673 | Self::Attribute
674 | Self::KeyWord
675 | Self::RefKeyWord
676 | Self::MacroNonTerminal
677 | Self::String
678 | Self::Number
679 | Self::Bool
680 | Self::Lifetime
681 | Self::QuestionMark
682 | Self::Decoration(_)
683 | Self::Backline(_)
684 | Self::Expansion => None,
685 }
686 }
687}
688
689#[derive(Debug)]
690enum Highlight<'a> {
691 Token { text: &'a str, class: Option<Class> },
692 EnterSpan { class: Class },
693 ExitSpan,
694}
695
696struct TokenIter<'a> {
697 src: &'a str,
698 cursor: Cursor<'a>,
699}
700
701impl<'a> Iterator for TokenIter<'a> {
702 type Item = (TokenKind, &'a str);
703 fn next(&mut self) -> Option<(TokenKind, &'a str)> {
704 let token = self.cursor.advance_token();
705 if token.kind == TokenKind::Eof {
706 return None;
707 }
708 let (text, rest) = self.src.split_at(token.len as usize);
709 self.src = rest;
710 Some((token.kind, text))
711 }
712}
713
714fn get_real_ident_class(text: &str, allow_path_keywords: bool) -> Option<Class> {
716 let ignore: &[&str] =
717 if allow_path_keywords { &["self", "Self", "super", "crate"] } else { &["self", "Self"] };
718 if ignore.contains(&text) {
719 return None;
720 }
721 Some(match text {
722 "ref" | "mut" => Class::RefKeyWord,
723 "false" | "true" => Class::Bool,
724 _ if Symbol::intern(text).is_reserved(|| Edition::Edition2021) => Class::KeyWord,
725 _ => return None,
726 })
727}
728
729struct PeekIter<'a> {
735 stored: VecDeque<(TokenKind, &'a str)>,
736 peek_pos: usize,
738 iter: TokenIter<'a>,
739}
740
741impl<'a> PeekIter<'a> {
742 fn new(iter: TokenIter<'a>) -> Self {
743 Self { stored: VecDeque::new(), peek_pos: 0, iter }
744 }
745 fn peek(&mut self) -> Option<&(TokenKind, &'a str)> {
747 if self.stored.is_empty()
748 && let Some(next) = self.iter.next()
749 {
750 self.stored.push_back(next);
751 }
752 self.stored.front()
753 }
754 fn peek_next(&mut self) -> Option<&(TokenKind, &'a str)> {
756 self.peek_pos += 1;
757 if self.peek_pos - 1 < self.stored.len() {
758 self.stored.get(self.peek_pos - 1)
759 } else if let Some(next) = self.iter.next() {
760 self.stored.push_back(next);
761 self.stored.back()
762 } else {
763 None
764 }
765 }
766}
767
768impl<'a> Iterator for PeekIter<'a> {
769 type Item = (TokenKind, &'a str);
770 fn next(&mut self) -> Option<Self::Item> {
771 self.peek_pos = 0;
772 if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() }
773 }
774}
775
776struct Decorations {
778 starts: Vec<(u32, &'static str)>,
779 ends: Vec<u32>,
780}
781
782impl Decorations {
783 fn new(info: &DecorationInfo) -> Self {
784 let (mut starts, mut ends): (Vec<_>, Vec<_>) = info
786 .0
787 .iter()
788 .flat_map(|(&kind, ranges)| ranges.iter().map(move |&(lo, hi)| ((lo, kind), hi)))
789 .unzip();
790
791 starts.sort_by_key(|(lo, _)| *lo);
793 ends.sort();
794
795 Decorations { starts, ends }
796 }
797}
798
799fn new_span(lo: u32, text: &str, file_span: Span) -> Span {
801 let hi = lo + text.len() as u32;
802 let file_lo = file_span.lo();
803 file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi))
804}
805
806struct Classifier<'src> {
809 tokens: PeekIter<'src>,
810 in_attribute: bool,
811 in_macro: bool,
812 in_macro_nonterminal: bool,
813 byte_pos: u32,
814 file_span: Span,
815 src: &'src str,
816 decorations: Option<Decorations>,
817}
818
819impl<'src> Classifier<'src> {
820 fn new(src: &'src str, file_span: Span, decoration_info: Option<&DecorationInfo>) -> Self {
823 let tokens =
824 PeekIter::new(TokenIter { src, cursor: Cursor::new(src, FrontmatterAllowed::Yes) });
825 let decorations = decoration_info.map(Decorations::new);
826 Classifier {
827 tokens,
828 in_attribute: false,
829 in_macro: false,
830 in_macro_nonterminal: false,
831 byte_pos: 0,
832 file_span,
833 src,
834 decorations,
835 }
836 }
837
838 fn get_full_ident_path(&mut self) -> Vec<(TokenKind, usize, usize)> {
840 let start = self.byte_pos as usize;
841 let mut pos = start;
842 let mut has_ident = false;
843
844 loop {
845 let mut nb = 0;
846 while let Some((TokenKind::Colon, _)) = self.tokens.peek() {
847 self.tokens.next();
848 nb += 1;
849 }
850 if has_ident && nb == 0 {
853 return vec![(TokenKind::Ident, start, pos)];
854 } else if nb != 0 && nb != 2 {
855 if has_ident {
856 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
857 } else {
858 return vec![(TokenKind::Colon, start, pos + nb)];
859 }
860 }
861
862 if let Some((None, text)) = self.tokens.peek().map(|(token, text)| {
863 if *token == TokenKind::Ident {
864 let class = get_real_ident_class(text, true);
865 (class, text)
866 } else {
867 (Some(Class::Comment), text)
869 }
870 }) {
871 pos += text.len() + nb;
873 has_ident = true;
874 self.tokens.next();
875 } else if nb > 0 && has_ident {
876 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
877 } else if nb > 0 {
878 return vec![(TokenKind::Colon, start, start + nb)];
879 } else if has_ident {
880 return vec![(TokenKind::Ident, start, pos)];
881 } else {
882 return Vec::new();
883 }
884 }
885 }
886
887 fn next(&mut self) -> Option<(TokenKind, &'src str, u32)> {
892 if let Some((kind, text)) = self.tokens.next() {
893 let before = self.byte_pos;
894 self.byte_pos += text.len() as u32;
895 Some((kind, text, before))
896 } else {
897 None
898 }
899 }
900
901 fn highlight(mut self, sink: &mut dyn FnMut(Span, Highlight<'src>)) {
907 loop {
908 if let Some(decs) = self.decorations.as_mut() {
909 let byte_pos = self.byte_pos;
910 let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count();
911 for (_, kind) in decs.starts.drain(0..n_starts) {
912 sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Decoration(kind) });
913 }
914
915 let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count();
916 for _ in decs.ends.drain(0..n_ends) {
917 sink(DUMMY_SP, Highlight::ExitSpan);
918 }
919 }
920
921 if self
922 .tokens
923 .peek()
924 .map(|t| matches!(t.0, TokenKind::Colon | TokenKind::Ident))
925 .unwrap_or(false)
926 {
927 let tokens = self.get_full_ident_path();
928 for (token, start, end) in &tokens {
929 let text = &self.src[*start..*end];
930 self.advance(*token, text, sink, *start as u32);
931 self.byte_pos += text.len() as u32;
932 }
933 if !tokens.is_empty() {
934 continue;
935 }
936 }
937 if let Some((token, text, before)) = self.next() {
938 self.advance(token, text, sink, before);
939 } else {
940 break;
941 }
942 }
943 }
944
945 fn advance(
952 &mut self,
953 token: TokenKind,
954 text: &'src str,
955 sink: &mut dyn FnMut(Span, Highlight<'src>),
956 before: u32,
957 ) {
958 let lookahead = self.peek();
959 let file_span = self.file_span;
960 let no_highlight = |sink: &mut dyn FnMut(_, _)| {
961 sink(new_span(before, text, file_span), Highlight::Token { text, class: None })
962 };
963 let whitespace = |sink: &mut dyn FnMut(_, _)| {
964 let mut start = 0u32;
965 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
966 sink(
967 new_span(before + start, part, file_span),
968 Highlight::Token { text: part, class: None },
969 );
970 start += part.len() as u32;
971 }
972 };
973 let class = match token {
974 TokenKind::Whitespace => return whitespace(sink),
975 TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => {
976 if doc_style.is_some() {
977 Class::DocComment
978 } else {
979 Class::Comment
980 }
981 }
982 TokenKind::Bang if self.in_macro => {
985 self.in_macro = false;
986 sink(new_span(before, text, file_span), Highlight::Token { text, class: None });
987 sink(DUMMY_SP, Highlight::ExitSpan);
988 return;
989 }
990
991 TokenKind::Star => match self.tokens.peek() {
995 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
996 Some((TokenKind::Ident, "mut")) => {
997 self.next();
998 sink(
999 DUMMY_SP,
1000 Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) },
1001 );
1002 return;
1003 }
1004 Some((TokenKind::Ident, "const")) => {
1005 self.next();
1006 sink(
1007 DUMMY_SP,
1008 Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) },
1009 );
1010 return;
1011 }
1012 _ => Class::RefKeyWord,
1013 },
1014 TokenKind::And => match self.tokens.peek() {
1015 Some((TokenKind::And, _)) => {
1016 self.next();
1017 sink(DUMMY_SP, Highlight::Token { text: "&&", class: None });
1018 return;
1019 }
1020 Some((TokenKind::Eq, _)) => {
1021 self.next();
1022 sink(DUMMY_SP, Highlight::Token { text: "&=", class: None });
1023 return;
1024 }
1025 Some((TokenKind::Whitespace, _)) => return whitespace(sink),
1026 Some((TokenKind::Ident, "mut")) => {
1027 self.next();
1028 sink(
1029 DUMMY_SP,
1030 Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) },
1031 );
1032 return;
1033 }
1034 _ => Class::RefKeyWord,
1035 },
1036
1037 TokenKind::Eq => match lookahead {
1039 Some(TokenKind::Eq) => {
1040 self.next();
1041 sink(DUMMY_SP, Highlight::Token { text: "==", class: None });
1042 return;
1043 }
1044 Some(TokenKind::Gt) => {
1045 self.next();
1046 sink(DUMMY_SP, Highlight::Token { text: "=>", class: None });
1047 return;
1048 }
1049 _ => return no_highlight(sink),
1050 },
1051 TokenKind::Minus if lookahead == Some(TokenKind::Gt) => {
1052 self.next();
1053 sink(DUMMY_SP, Highlight::Token { text: "->", class: None });
1054 return;
1055 }
1056
1057 TokenKind::Minus
1059 | TokenKind::Plus
1060 | TokenKind::Or
1061 | TokenKind::Slash
1062 | TokenKind::Caret
1063 | TokenKind::Percent
1064 | TokenKind::Bang
1065 | TokenKind::Lt
1066 | TokenKind::Gt => return no_highlight(sink),
1067
1068 TokenKind::Dot
1070 | TokenKind::Semi
1071 | TokenKind::Comma
1072 | TokenKind::OpenParen
1073 | TokenKind::CloseParen
1074 | TokenKind::OpenBrace
1075 | TokenKind::CloseBrace
1076 | TokenKind::OpenBracket
1077 | TokenKind::At
1078 | TokenKind::Tilde
1079 | TokenKind::Colon
1080 | TokenKind::Frontmatter { .. }
1081 | TokenKind::Unknown => return no_highlight(sink),
1082
1083 TokenKind::Question => Class::QuestionMark,
1084
1085 TokenKind::Dollar => match lookahead {
1086 Some(TokenKind::Ident) => {
1087 self.in_macro_nonterminal = true;
1088 Class::MacroNonTerminal
1089 }
1090 _ => return no_highlight(sink),
1091 },
1092
1093 TokenKind::Pound => {
1098 match lookahead {
1099 Some(TokenKind::Bang) => {
1101 self.next();
1102 if let Some(TokenKind::OpenBracket) = self.peek() {
1103 self.in_attribute = true;
1104 sink(
1105 new_span(before, text, file_span),
1106 Highlight::EnterSpan { class: Class::Attribute },
1107 );
1108 }
1109 sink(DUMMY_SP, Highlight::Token { text: "#", class: None });
1110 sink(DUMMY_SP, Highlight::Token { text: "!", class: None });
1111 return;
1112 }
1113 Some(TokenKind::OpenBracket) => {
1115 self.in_attribute = true;
1116 sink(
1117 new_span(before, text, file_span),
1118 Highlight::EnterSpan { class: Class::Attribute },
1119 );
1120 }
1121 _ => (),
1122 }
1123 return no_highlight(sink);
1124 }
1125 TokenKind::CloseBracket => {
1126 if self.in_attribute {
1127 self.in_attribute = false;
1128 sink(
1129 new_span(before, text, file_span),
1130 Highlight::Token { text: "]", class: None },
1131 );
1132 sink(DUMMY_SP, Highlight::ExitSpan);
1133 return;
1134 }
1135 return no_highlight(sink);
1136 }
1137 TokenKind::Literal { kind, .. } => match kind {
1138 LiteralKind::Byte { .. }
1140 | LiteralKind::Char { .. }
1141 | LiteralKind::Str { .. }
1142 | LiteralKind::ByteStr { .. }
1143 | LiteralKind::RawStr { .. }
1144 | LiteralKind::RawByteStr { .. }
1145 | LiteralKind::CStr { .. }
1146 | LiteralKind::RawCStr { .. } => Class::String,
1147 LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number,
1149 },
1150 TokenKind::GuardedStrPrefix => return no_highlight(sink),
1151 TokenKind::Ident | TokenKind::RawIdent if lookahead == Some(TokenKind::Bang) => {
1152 self.in_macro = true;
1153 let span = new_span(before, text, file_span);
1154 sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Macro(span) });
1155 sink(span, Highlight::Token { text, class: None });
1156 return;
1157 }
1158 TokenKind::Ident => match get_real_ident_class(text, false) {
1159 None => match text {
1160 "Option" | "Result" => Class::PreludeTy(new_span(before, text, file_span)),
1161 "Some" | "None" | "Ok" | "Err" => {
1162 Class::PreludeVal(new_span(before, text, file_span))
1163 }
1164 "union" if self.check_if_is_union_keyword() => Class::KeyWord,
1167 _ if self.in_macro_nonterminal => {
1168 self.in_macro_nonterminal = false;
1169 Class::MacroNonTerminal
1170 }
1171 "self" | "Self" => Class::Self_(new_span(before, text, file_span)),
1172 _ => Class::Ident(new_span(before, text, file_span)),
1173 },
1174 Some(c) => c,
1175 },
1176 TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => {
1177 Class::Ident(new_span(before, text, file_span))
1178 }
1179 TokenKind::Lifetime { .. }
1180 | TokenKind::RawLifetime
1181 | TokenKind::UnknownPrefixLifetime => Class::Lifetime,
1182 TokenKind::Eof => panic!("Eof in advance"),
1183 };
1184 let mut start = 0u32;
1187 for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
1188 sink(
1189 new_span(before + start, part, file_span),
1190 Highlight::Token { text: part, class: Some(class) },
1191 );
1192 start += part.len() as u32;
1193 }
1194 }
1195
1196 fn peek(&mut self) -> Option<TokenKind> {
1197 self.tokens.peek().map(|(token_kind, _text)| *token_kind)
1198 }
1199
1200 fn check_if_is_union_keyword(&mut self) -> bool {
1201 while let Some(kind) = self.tokens.peek_next().map(|(token_kind, _text)| token_kind) {
1202 if *kind == TokenKind::Whitespace {
1203 continue;
1204 }
1205 return *kind == TokenKind::Ident;
1206 }
1207 false
1208 }
1209}
1210
1211fn enter_span(
1214 out: &mut impl Write,
1215 klass: Class,
1216 href_context: &Option<HrefContext<'_, '_>>,
1217) -> &'static str {
1218 string_without_closing_tag(out, "", Some(klass), href_context, true).expect(
1219 "internal error: enter_span was called with Some(klass) but did not return a \
1220 closing HTML tag",
1221 )
1222}
1223
1224fn exit_span(out: &mut impl Write, closing_tag: &str) {
1226 out.write_str(closing_tag).unwrap();
1227}
1228
1229fn string<W: Write>(
1246 out: &mut W,
1247 text: EscapeBodyText<'_>,
1248 klass: Option<Class>,
1249 href_context: &Option<HrefContext<'_, '_>>,
1250 open_tag: bool,
1251 write_line_number_callback: fn(&mut W, u32, &'static str),
1252) {
1253 if let Some(Class::Backline(line)) = klass {
1254 write_line_number_callback(out, line, "\n");
1255 } else if let Some(Class::Expansion) = klass {
1256 out.write_str(text.0).unwrap();
1258 } else if let Some(closing_tag) =
1259 string_without_closing_tag(out, text, klass, href_context, open_tag)
1260 {
1261 out.write_str(closing_tag).unwrap();
1262 }
1263}
1264
1265fn string_without_closing_tag<T: Display>(
1275 out: &mut impl Write,
1276 text: T,
1277 klass: Option<Class>,
1278 href_context: &Option<HrefContext<'_, '_>>,
1279 open_tag: bool,
1280) -> Option<&'static str> {
1281 let Some(klass) = klass else {
1282 write!(out, "{text}").unwrap();
1283 return None;
1284 };
1285 let Some(def_span) = klass.get_span() else {
1286 if !open_tag {
1287 write!(out, "{text}").unwrap();
1288 return None;
1289 }
1290 write!(out, "<span class=\"{klass}\">{text}", klass = klass.as_html()).unwrap();
1291 return Some("</span>");
1292 };
1293
1294 let mut text_s = text.to_string();
1295 if text_s.contains("::") {
1296 text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| {
1297 match t {
1298 "self" | "Self" => write!(
1299 &mut path,
1300 "<span class=\"{klass}\">{t}</span>",
1301 klass = Class::Self_(DUMMY_SP).as_html(),
1302 ),
1303 "crate" | "super" => {
1304 write!(
1305 &mut path,
1306 "<span class=\"{klass}\">{t}</span>",
1307 klass = Class::KeyWord.as_html(),
1308 )
1309 }
1310 t => write!(&mut path, "{t}"),
1311 }
1312 .expect("Failed to build source HTML path");
1313 path
1314 });
1315 }
1316
1317 if let Some(href_context) = href_context
1318 && let Some(href) = href_context.context.shared.span_correspondence_map.get(&def_span)
1319 && let Some(href) = {
1320 let context = href_context.context;
1321 match href {
1327 LinkFromSrc::Local(span) => {
1328 context.href_from_span_relative(*span, &href_context.current_href)
1329 }
1330 LinkFromSrc::External(def_id) => {
1331 format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1332 .ok()
1333 .map(|(url, _, _)| url)
1334 }
1335 LinkFromSrc::Primitive(prim) => format::href_with_root_path(
1336 PrimitiveType::primitive_locations(context.tcx())[prim],
1337 context,
1338 Some(href_context.root_path),
1339 )
1340 .ok()
1341 .map(|(url, _, _)| url),
1342 LinkFromSrc::Doc(def_id) => {
1343 format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1344 .ok()
1345 .map(|(doc_link, _, _)| doc_link)
1346 }
1347 }
1348 }
1349 {
1350 if !open_tag {
1351 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1354 } else {
1355 let klass_s = klass.as_html();
1356 if klass_s.is_empty() {
1357 write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1358 } else {
1359 write!(out, "<a class=\"{klass_s}\" href=\"{href}\">{text_s}").unwrap();
1360 }
1361 }
1362 return Some("</a>");
1363 }
1364 if !open_tag {
1365 out.write_str(&text_s).unwrap();
1366 return None;
1367 }
1368 let klass_s = klass.as_html();
1369 if klass_s.is_empty() {
1370 out.write_str(&text_s).unwrap();
1371 Some("")
1372 } else {
1373 write!(out, "<span class=\"{klass_s}\">{text_s}").unwrap();
1374 Some("</span>")
1375 }
1376}
1377
1378#[cfg(test)]
1379mod tests;