| //! Basic syntax highlighting functionality. | |
| //! | |
| //! This module uses librustc_ast's lexer to provide token-based highlighting for | |
| //! the HTML documentation generated by rustdoc. | |
| //! | |
| //! Use the `render_with_highlighting` to highlight some rust code. | |
| use std::borrow::Cow; | |
| use std::collections::VecDeque; | |
| use std::fmt::{self, Display, Write}; | |
| use std::iter; | |
| use itertools::Either; | |
| use rustc_data_structures::fx::FxIndexMap; | |
| use rustc_lexer::{Cursor, FrontmatterAllowed, LiteralKind, TokenKind}; | |
| use rustc_span::BytePos; | |
| use rustc_span::edition::Edition; | |
| use rustc_span::symbol::Symbol; | |
| use super::format; | |
| use crate::clean::PrimitiveType; | |
| use crate::display::Joined as _; | |
| use crate::html::escape::EscapeBodyText; | |
| use crate::html::format::HrefInfo; | |
| use crate::html::macro_expansion::ExpandedCode; | |
| use crate::html::render::span_map::{DUMMY_SP, Span}; | |
| use crate::html::render::{Context, LinkFromSrc}; | |
| /// This type is needed in case we want to render links on items to allow to go to their definition. | |
| pub(crate) struct HrefContext<'a, 'tcx> { | |
| pub(crate) context: &'a Context<'tcx>, | |
| /// This span contains the current file we're going through. | |
| pub(crate) file_span: Span, | |
| /// This field is used to know "how far" from the top of the directory we are to link to either | |
| /// documentation pages or other source pages. | |
| pub(crate) root_path: &'a str, | |
| /// This field is used to calculate precise local URLs. | |
| pub(crate) current_href: String, | |
| } | |
| /// Decorations are represented as a map from CSS class to vector of character ranges. | |
| /// Each range will be wrapped in a span with that class. | |
| #[derive(Default)] | |
| pub(crate) struct DecorationInfo(pub(crate) FxIndexMap<&'static str, Vec<(u32, u32)>>); | |
| #[derive(Eq, PartialEq, Clone)] | |
| pub(crate) enum Tooltip { | |
| IgnoreAll, | |
| IgnoreSome(Vec<String>), | |
| CompileFail, | |
| ShouldPanic, | |
| Edition(Edition), | |
| } | |
| /// Highlights `src` as an inline example, returning the HTML output. | |
| pub(crate) fn render_example_with_highlighting( | |
| src: &str, | |
| tooltip: Option<&Tooltip>, | |
| playground_button: Option<&str>, | |
| extra_classes: &[String], | |
| ) -> impl Display { | |
| fmt::from_fn(move |f| { | |
| write_header("rust-example-rendered", tooltip, extra_classes).fmt(f)?; | |
| write_code(f, src, None, None, None); | |
| write_footer(playground_button).fmt(f) | |
| }) | |
| } | |
| fn write_header(class: &str, tooltip: Option<&Tooltip>, extra_classes: &[String]) -> impl Display { | |
| fmt::from_fn(move |f| { | |
| write!( | |
| f, | |
| "<div class=\"example-wrap{}\">", | |
| tooltip | |
| .map(|tooltip| match tooltip { | |
| Tooltip::IgnoreAll | Tooltip::IgnoreSome(_) => " ignore", | |
| Tooltip::CompileFail => " compile_fail", | |
| Tooltip::ShouldPanic => " should_panic", | |
| Tooltip::Edition(_) => " edition", | |
| }) | |
| .unwrap_or_default() | |
| )?; | |
| if let Some(tooltip) = tooltip { | |
| let tooltip = fmt::from_fn(|f| match tooltip { | |
| Tooltip::IgnoreAll => f.write_str("This example is not tested"), | |
| Tooltip::IgnoreSome(platforms) => { | |
| f.write_str("This example is not tested on ")?; | |
| match &platforms[..] { | |
| [] => unreachable!(), | |
| [platform] => f.write_str(platform)?, | |
| [first, second] => write!(f, "{first} or {second}")?, | |
| [platforms @ .., last] => { | |
| for platform in platforms { | |
| write!(f, "{platform}, ")?; | |
| } | |
| write!(f, "or {last}")?; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| Tooltip::CompileFail => f.write_str("This example deliberately fails to compile"), | |
| Tooltip::ShouldPanic => f.write_str("This example panics"), | |
| Tooltip::Edition(edition) => write!(f, "This example runs with edition {edition}"), | |
| }); | |
| write!(f, "<a href=\"#\" class=\"tooltip\" title=\"{tooltip}\">ⓘ</a>")?; | |
| } | |
| let classes = fmt::from_fn(|f| { | |
| iter::once("rust") | |
| .chain(Some(class).filter(|class| !class.is_empty())) | |
| .chain(extra_classes.iter().map(String::as_str)) | |
| .joined(" ", f) | |
| }); | |
| write!(f, "<pre class=\"{classes}\"><code>") | |
| }) | |
| } | |
| /// Check if two `Class` can be merged together. In the following rules, "unclassified" means `None` | |
| /// basically (since it's `Option<Class>`). The following rules apply: | |
| /// | |
| /// * If two `Class` have the same variant, then they can be merged. | |
| /// * If the other `Class` is unclassified and only contains white characters (backline, | |
| /// whitespace, etc), it can be merged. | |
| /// * `Class::Ident` is considered the same as unclassified (because it doesn't have an associated | |
| /// CSS class). | |
| fn can_merge(class1: Option<Class>, class2: Option<Class>, text: &str) -> bool { | |
| match (class1, class2) { | |
| (Some(c1), Some(c2)) => c1.is_equal_to(c2), | |
| (Some(Class::Ident(_)), None) | (None, Some(Class::Ident(_))) => true, | |
| (Some(Class::Macro(_)), _) => false, | |
| (Some(_), None) | (None, Some(_)) => text.trim().is_empty(), | |
| (None, None) => true, | |
| } | |
| } | |
| #[derive(Debug)] | |
| struct ClassInfo { | |
| class: Class, | |
| /// If `Some`, then it means the tag was opened and needs to be closed. | |
| closing_tag: Option<&'static str>, | |
| /// Set to `true` by `exit_elem` to signal that all the elements of this class have been pushed. | |
| /// | |
| /// The class will be closed and removed from the stack when the next non-mergeable item is | |
| /// pushed. When it is removed, the closing tag will be written if (and only if) | |
| /// `self.closing_tag` is `Some`. | |
| pending_exit: bool, | |
| } | |
| impl ClassInfo { | |
| fn new(class: Class, closing_tag: Option<&'static str>) -> Self { | |
| Self { class, closing_tag, pending_exit: closing_tag.is_some() } | |
| } | |
| fn close_tag<W: Write>(&self, out: &mut W) { | |
| if let Some(closing_tag) = self.closing_tag { | |
| out.write_str(closing_tag).unwrap(); | |
| } | |
| } | |
| fn is_open(&self) -> bool { | |
| self.closing_tag.is_some() | |
| } | |
| } | |
| /// This represents the stack of HTML elements. For example a macro expansion | |
| /// will contain other elements which might themselves contain other elements | |
| /// (like macros). | |
| /// | |
| /// This allows to easily handle HTML tags instead of having a more complicated | |
| /// state machine to keep track of which tags are open. | |
| #[derive(Debug)] | |
| struct ClassStack { | |
| open_classes: Vec<ClassInfo>, | |
| } | |
| impl ClassStack { | |
| fn new() -> Self { | |
| Self { open_classes: Vec::new() } | |
| } | |
| fn enter_elem<W: Write>( | |
| &mut self, | |
| out: &mut W, | |
| href_context: &Option<HrefContext<'_, '_>>, | |
| new_class: Class, | |
| closing_tag: Option<&'static str>, | |
| ) { | |
| if let Some(current_class) = self.open_classes.last_mut() { | |
| if can_merge(Some(current_class.class), Some(new_class), "") { | |
| current_class.pending_exit = false; | |
| return; | |
| } else if current_class.pending_exit { | |
| current_class.close_tag(out); | |
| self.open_classes.pop(); | |
| } | |
| } | |
| let mut class_info = ClassInfo::new(new_class, closing_tag); | |
| if closing_tag.is_none() { | |
| if matches!(new_class, Class::Decoration(_) | Class::Original) { | |
| // Even if a whitespace characters follows, we need to open the class right away | |
| // as these characters are part of the element. | |
| // FIXME: Should we instead add a new boolean field to `ClassInfo` to force a | |
| // non-open tag to be added if another one comes before it's open? | |
| write!(out, "<span class=\"{}\">", new_class.as_html()).unwrap(); | |
| class_info.closing_tag = Some("</span>"); | |
| } else if new_class.get_span().is_some() | |
| && let Some(closing_tag) = | |
| string_without_closing_tag(out, "", Some(class_info.class), href_context, false) | |
| && !closing_tag.is_empty() | |
| { | |
| class_info.closing_tag = Some(closing_tag); | |
| } | |
| } | |
| self.open_classes.push(class_info); | |
| } | |
| /// This sets the `pending_exit` field to `true`. Meaning that if we try to push another stack | |
| /// which is not compatible with this one, it will exit the current one before adding the new | |
| /// one. | |
| fn exit_elem(&mut self) { | |
| let current_class = | |
| self.open_classes.last_mut().expect("`exit_elem` called on empty class stack"); | |
| if !current_class.pending_exit { | |
| current_class.pending_exit = true; | |
| return; | |
| } | |
| // If the current class was already closed, it means we are actually closing its parent. | |
| self.open_classes.pop(); | |
| let current_class = | |
| self.open_classes.last_mut().expect("`exit_elem` called on empty class stack parent"); | |
| current_class.pending_exit = true; | |
| } | |
| fn last_class(&self) -> Option<Class> { | |
| self.open_classes.last().map(|c| c.class) | |
| } | |
| fn last_class_is_open(&self) -> bool { | |
| if let Some(last) = self.open_classes.last() { | |
| last.is_open() | |
| } else { | |
| // If there is no class, then it's already open. | |
| true | |
| } | |
| } | |
| fn close_last_if_needed<W: Write>(&mut self, out: &mut W) { | |
| if let Some(last) = self.open_classes.pop_if(|class| class.pending_exit && class.is_open()) | |
| { | |
| last.close_tag(out); | |
| } | |
| } | |
| fn push<W: Write>( | |
| &mut self, | |
| out: &mut W, | |
| href_context: &Option<HrefContext<'_, '_>>, | |
| class: Option<Class>, | |
| text: Cow<'_, str>, | |
| needs_escape: bool, | |
| ) { | |
| // If the new token cannot be merged with the currently open `Class`, we close the `Class` | |
| // if possible. | |
| if !can_merge(self.last_class(), class, &text) { | |
| self.close_last_if_needed(out) | |
| } | |
| let current_class = self.last_class(); | |
| // If we have a `Class` that hasn't been "open" yet (ie, we received only an `EnterSpan` | |
| // event), we need to open the `Class` before going any further so the new token will be | |
| // written inside it. | |
| if class.is_none() && !self.last_class_is_open() { | |
| if let Some(current_class_info) = self.open_classes.last_mut() { | |
| let class_s = current_class_info.class.as_html(); | |
| if !class_s.is_empty() { | |
| write!(out, "<span class=\"{class_s}\">").unwrap(); | |
| } | |
| current_class_info.closing_tag = Some("</span>"); | |
| } | |
| } | |
| let current_class_is_open = self.open_classes.last().is_some_and(|c| c.is_open()); | |
| let can_merge = can_merge(class, current_class, &text); | |
| let should_open_tag = !current_class_is_open || !can_merge; | |
| let text = | |
| if needs_escape { Either::Left(&EscapeBodyText(&text)) } else { Either::Right(text) }; | |
| let closing_tag = | |
| string_without_closing_tag(out, &text, class, href_context, should_open_tag); | |
| if class.is_some() && should_open_tag && closing_tag.is_none() { | |
| panic!( | |
| "called `string_without_closing_tag` with a class but no closing tag was returned" | |
| ); | |
| } else if let Some(closing_tag) = closing_tag | |
| && !closing_tag.is_empty() | |
| { | |
| // If this is a link, we need to close it right away and not open a new `Class`, | |
| // otherwise extra content would go into the `<a>` HTML tag. | |
| if closing_tag == "</a>" { | |
| out.write_str(closing_tag).unwrap(); | |
| // If the current `Class` is not compatible with this one, we create a new `Class`. | |
| } else if let Some(class) = class | |
| && !can_merge | |
| { | |
| self.enter_elem(out, href_context, class, Some("</span>")); | |
| // Otherwise, we consider the actual `Class` to have been open. | |
| } else if let Some(current_class_info) = self.open_classes.last_mut() { | |
| current_class_info.closing_tag = Some("</span>"); | |
| } | |
| } | |
| } | |
| /// This method closes all open tags and returns the list of `Class` which were not already | |
| /// closed (ie `pending_exit` set to `true`). | |
| /// | |
| /// It is used when starting a macro expansion: we need to close all HTML tags and then to | |
| /// reopen them inside the newly created expansion HTML tag. Same goes when we close the | |
| /// expansion. | |
| fn empty_stack<W: Write>(&mut self, out: &mut W) -> Vec<Class> { | |
| let mut classes = Vec::with_capacity(self.open_classes.len()); | |
| // We close all open tags and only keep the ones that were not already waiting to be closed. | |
| while let Some(class_info) = self.open_classes.pop() { | |
| class_info.close_tag(out); | |
| if !class_info.pending_exit { | |
| classes.push(class_info.class); | |
| } | |
| } | |
| classes | |
| } | |
| } | |
| /// This type is used as a conveniency to prevent having to pass all its fields as arguments into | |
| /// the various functions (which became its methods). | |
| struct TokenHandler<'a, 'tcx, F: Write> { | |
| out: &'a mut F, | |
| class_stack: ClassStack, | |
| /// We need to keep the `Class` for each element because it could contain a `Span` which is | |
| /// used to generate links. | |
| href_context: Option<HrefContext<'a, 'tcx>>, | |
| line_number_kind: LineNumberKind, | |
| line: u32, | |
| max_lines: u32, | |
| } | |
| impl<F: Write> std::fmt::Debug for TokenHandler<'_, '_, F> { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| f.debug_struct("TokenHandler").field("class_stack", &self.class_stack).finish() | |
| } | |
| } | |
| impl<'a, F: Write> TokenHandler<'a, '_, F> { | |
| fn handle_backline(&mut self) -> Option<impl Display + use<F>> { | |
| self.line += 1; | |
| if self.line < self.max_lines { | |
| return Some(self.line_number_kind.render(self.line)); | |
| } | |
| None | |
| } | |
| fn push_token_without_backline_check( | |
| &mut self, | |
| class: Option<Class>, | |
| text: Cow<'a, str>, | |
| needs_escape: bool, | |
| ) { | |
| self.class_stack.push(self.out, &self.href_context, class, text, needs_escape); | |
| } | |
| fn push_token(&mut self, class: Option<Class>, text: Cow<'a, str>) { | |
| if text == "\n" | |
| && let Some(backline) = self.handle_backline() | |
| { | |
| write!(self.out, "{text}{backline}").unwrap(); | |
| } else { | |
| self.push_token_without_backline_check(class, text, true); | |
| } | |
| } | |
| fn start_expansion(&mut self) { | |
| // We close all open tags. | |
| let classes = self.class_stack.empty_stack(self.out); | |
| // We start the expansion tag. | |
| self.class_stack.enter_elem(self.out, &self.href_context, Class::Expansion, None); | |
| self.push_token_without_backline_check( | |
| Some(Class::Expansion), | |
| Cow::Owned(format!( | |
| "<input id=expand-{} \ | |
| tabindex=0 \ | |
| type=checkbox \ | |
| aria-label=\"Collapse/expand macro\" \ | |
| title=\"Collapse/expand macro\">", | |
| self.line, | |
| )), | |
| false, | |
| ); | |
| // We re-open all tags that didn't have `pending_exit` set to `true`. | |
| for class in classes.into_iter().rev() { | |
| self.class_stack.enter_elem(self.out, &self.href_context, class, None); | |
| } | |
| } | |
| fn add_expanded_code(&mut self, expanded_code: &ExpandedCode) { | |
| self.push_token_without_backline_check( | |
| None, | |
| Cow::Owned(format!("<span class=expanded>{}</span>", expanded_code.code)), | |
| false, | |
| ); | |
| self.class_stack.enter_elem(self.out, &self.href_context, Class::Original, None); | |
| } | |
| fn close_expansion(&mut self) { | |
| // We close all open tags. | |
| let classes = self.class_stack.empty_stack(self.out); | |
| // We re-open all tags without expansion-related ones. | |
| for class in classes.into_iter().rev() { | |
| if !matches!(class, Class::Expansion | Class::Original) { | |
| self.class_stack.enter_elem(self.out, &self.href_context, class, None); | |
| } | |
| } | |
| } | |
| /// Used when we're done with the current expansion "original code" (ie code before expansion). | |
| /// We close all tags inside `Class::Original` and only keep the ones that were not closed yet. | |
| fn close_original_tag(&mut self) { | |
| let mut classes_to_reopen = Vec::new(); | |
| while let Some(mut class_info) = self.class_stack.open_classes.pop() { | |
| if class_info.class == Class::Original { | |
| while let Some(class_info) = classes_to_reopen.pop() { | |
| self.class_stack.open_classes.push(class_info); | |
| } | |
| class_info.close_tag(self.out); | |
| return; | |
| } | |
| class_info.close_tag(self.out); | |
| if !class_info.pending_exit { | |
| class_info.closing_tag = None; | |
| classes_to_reopen.push(class_info); | |
| } | |
| } | |
| panic!("Didn't find `Class::Original` to close"); | |
| } | |
| } | |
| impl<F: Write> Drop for TokenHandler<'_, '_, F> { | |
| /// When leaving, we need to flush all pending data to not have missing content. | |
| fn drop(&mut self) { | |
| self.class_stack.empty_stack(self.out); | |
| } | |
| } | |
| /// Represents the type of line number to be generated as HTML. | |
| #[derive(Clone, Copy)] | |
| enum LineNumberKind { | |
| /// Used for scraped code examples. | |
| Scraped, | |
| /// Used for source code pages. | |
| Normal, | |
| /// Code examples in documentation don't have line number generated by rustdoc. | |
| Empty, | |
| } | |
| impl LineNumberKind { | |
| fn render(self, line: u32) -> impl Display { | |
| fmt::from_fn(move |f| { | |
| match self { | |
| // https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#data-nosnippet-attr | |
| // Do not show "1 2 3 4 5 ..." in web search results. | |
| Self::Scraped => write!(f, "<span data-nosnippet>{line}</span>"), | |
| Self::Normal => write!(f, "<a href=#{line} id={line} data-nosnippet>{line}</a>"), | |
| Self::Empty => Ok(()), | |
| } | |
| }) | |
| } | |
| } | |
| fn get_next_expansion( | |
| expanded_codes: &[ExpandedCode], | |
| line: u32, | |
| span: Span, | |
| ) -> Option<&ExpandedCode> { | |
| expanded_codes.iter().find(|code| code.start_line == line && code.span.lo() > span.lo()) | |
| } | |
| fn get_expansion<'a, W: Write>( | |
| token_handler: &mut TokenHandler<'_, '_, W>, | |
| expanded_codes: &'a [ExpandedCode], | |
| span: Span, | |
| ) -> Option<&'a ExpandedCode> { | |
| let expanded_code = get_next_expansion(expanded_codes, token_handler.line, span)?; | |
| token_handler.start_expansion(); | |
| Some(expanded_code) | |
| } | |
| fn end_expansion<'a, W: Write>( | |
| token_handler: &mut TokenHandler<'_, '_, W>, | |
| expanded_codes: &'a [ExpandedCode], | |
| span: Span, | |
| ) -> Option<&'a ExpandedCode> { | |
| // We close `Class::Original` and everything open inside it. | |
| token_handler.close_original_tag(); | |
| // Then we check if we have another macro expansion on the same line. | |
| let expansion = get_next_expansion(expanded_codes, token_handler.line, span); | |
| if expansion.is_none() { | |
| token_handler.close_expansion(); | |
| } | |
| expansion | |
| } | |
| #[derive(Clone, Copy)] | |
| pub(super) struct LineInfo { | |
| pub(super) start_line: u32, | |
| max_lines: u32, | |
| pub(super) is_scraped_example: bool, | |
| } | |
| impl LineInfo { | |
| pub(super) fn new(max_lines: u32) -> Self { | |
| Self { start_line: 1, max_lines: max_lines + 1, is_scraped_example: false } | |
| } | |
| pub(super) fn new_scraped(max_lines: u32, start_line: u32) -> Self { | |
| Self { | |
| start_line: start_line + 1, | |
| max_lines: max_lines + start_line + 1, | |
| is_scraped_example: true, | |
| } | |
| } | |
| } | |
| /// Convert the given `src` source code into HTML by adding classes for highlighting. | |
| /// | |
| /// This code is used to render code blocks (in the documentation) as well as the source code pages. | |
| /// | |
| /// Some explanations on the last arguments: | |
| /// | |
| /// In case we are rendering a code block and not a source code file, `href_context` will be `None`. | |
| /// To put it more simply: if `href_context` is `None`, the code won't try to generate links to an | |
| /// item definition. | |
| /// | |
| /// More explanations about spans and how we use them here are provided in the | |
| pub(super) fn write_code( | |
| out: &mut impl Write, | |
| src: &str, | |
| href_context: Option<HrefContext<'_, '_>>, | |
| decoration_info: Option<&DecorationInfo>, | |
| line_info: Option<LineInfo>, | |
| ) { | |
| // This replace allows to fix how the code source with DOS backline characters is displayed. | |
| let src = | |
| // The first "\r\n" should be fairly close to the beginning of the string relatively | |
| // to its overall length, and most strings handled by rustdoc likely don't have | |
| // DOS backlines anyway. | |
| // Checking for the single ASCII character '\r' is much more efficient than checking for | |
| // the whole string "\r\n". | |
| if src.contains('\r') { src.replace("\r\n", "\n").into() } else { Cow::Borrowed(src) }; | |
| let mut token_handler = TokenHandler { | |
| out, | |
| href_context, | |
| line_number_kind: match line_info { | |
| Some(line_info) => { | |
| if line_info.is_scraped_example { | |
| LineNumberKind::Scraped | |
| } else { | |
| LineNumberKind::Normal | |
| } | |
| } | |
| None => LineNumberKind::Empty, | |
| }, | |
| line: 0, | |
| max_lines: u32::MAX, | |
| class_stack: ClassStack::new(), | |
| }; | |
| if let Some(line_info) = line_info { | |
| token_handler.line = line_info.start_line - 1; | |
| token_handler.max_lines = line_info.max_lines; | |
| if let Some(backline) = token_handler.handle_backline() { | |
| token_handler.push_token_without_backline_check( | |
| None, | |
| Cow::Owned(backline.to_string()), | |
| false, | |
| ); | |
| } | |
| } | |
| let (expanded_codes, file_span) = match token_handler.href_context.as_ref().and_then(|c| { | |
| let expanded_codes = c.context.shared.expanded_codes.get(&c.file_span.lo())?; | |
| Some((expanded_codes, c.file_span)) | |
| }) { | |
| Some((expanded_codes, file_span)) => (expanded_codes.as_slice(), file_span), | |
| None => (&[] as &[ExpandedCode], DUMMY_SP), | |
| }; | |
| let mut current_expansion = get_expansion(&mut token_handler, expanded_codes, file_span); | |
| classify( | |
| &src, | |
| token_handler.href_context.as_ref().map_or(DUMMY_SP, |c| c.file_span), | |
| decoration_info, | |
| &mut |span, highlight| match highlight { | |
| Highlight::Token { text, class } => { | |
| token_handler.push_token(class, Cow::Borrowed(text)); | |
| if text == "\n" { | |
| if current_expansion.is_none() { | |
| current_expansion = get_expansion(&mut token_handler, expanded_codes, span); | |
| } | |
| if let Some(ref current_expansion) = current_expansion | |
| && current_expansion.span.lo() == span.hi() | |
| { | |
| token_handler.add_expanded_code(current_expansion); | |
| } | |
| } else { | |
| let mut need_end = false; | |
| if let Some(ref current_expansion) = current_expansion { | |
| if current_expansion.span.lo() == span.hi() { | |
| token_handler.add_expanded_code(current_expansion); | |
| } else if current_expansion.end_line == token_handler.line | |
| && span.hi() >= current_expansion.span.hi() | |
| { | |
| need_end = true; | |
| } | |
| } | |
| if need_end { | |
| current_expansion = end_expansion(&mut token_handler, expanded_codes, span); | |
| } | |
| } | |
| } | |
| Highlight::EnterSpan { class } => { | |
| token_handler.class_stack.enter_elem( | |
| token_handler.out, | |
| &token_handler.href_context, | |
| class, | |
| None, | |
| ); | |
| } | |
| Highlight::ExitSpan => { | |
| token_handler.class_stack.exit_elem(); | |
| } | |
| }, | |
| ); | |
| } | |
| fn write_footer(playground_button: Option<&str>) -> impl Display { | |
| fmt::from_fn(move |f| write!(f, "</code></pre>{}</div>", playground_button.unwrap_or_default())) | |
| } | |
| /// How a span of text is classified. Mostly corresponds to token kinds. | |
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | |
| enum Class { | |
| Comment, | |
| DocComment, | |
| Attribute, | |
| KeyWord, | |
| /// Keywords that do pointer/reference stuff. | |
| RefKeyWord, | |
| Self_(Span), | |
| Macro(Span), | |
| MacroNonTerminal, | |
| String, | |
| Number, | |
| Bool, | |
| /// `Ident` isn't rendered in the HTML but we still need it for the `Span` it contains. | |
| Ident(Span), | |
| Lifetime, | |
| PreludeTy(Span), | |
| PreludeVal(Span), | |
| QuestionMark, | |
| Decoration(&'static str), | |
| /// Macro expansion. | |
| Expansion, | |
| /// "original" code without macro expansion. | |
| Original, | |
| } | |
| impl Class { | |
| /// It is only looking at the variant, not the variant content. | |
| /// | |
| /// It is used mostly to group multiple similar HTML elements into one `<span>` instead of | |
| /// multiple ones. | |
| fn is_equal_to(self, other: Self) -> bool { | |
| match (self, other) { | |
| (Self::Self_(_), Self::Self_(_)) | |
| | (Self::Macro(_), Self::Macro(_)) | |
| | (Self::Ident(_), Self::Ident(_)) => true, | |
| (Self::Decoration(c1), Self::Decoration(c2)) => c1 == c2, | |
| (x, y) => x == y, | |
| } | |
| } | |
| /// Returns the css class expected by rustdoc for each `Class`. | |
| fn as_html(self) -> &'static str { | |
| match self { | |
| Class::Comment => "comment", | |
| Class::DocComment => "doccomment", | |
| Class::Attribute => "attr", | |
| Class::KeyWord => "kw", | |
| Class::RefKeyWord => "kw-2", | |
| Class::Self_(_) => "self", | |
| Class::Macro(_) => "macro", | |
| Class::MacroNonTerminal => "macro-nonterminal", | |
| Class::String => "string", | |
| Class::Number => "number", | |
| Class::Bool => "bool-val", | |
| Class::Ident(_) => "", | |
| Class::Lifetime => "lifetime", | |
| Class::PreludeTy(_) => "prelude-ty", | |
| Class::PreludeVal(_) => "prelude-val", | |
| Class::QuestionMark => "question-mark", | |
| Class::Decoration(kind) => kind, | |
| Class::Expansion => "expansion", | |
| Class::Original => "original", | |
| } | |
| } | |
| /// In case this is an item which can be converted into a link to a definition, it'll contain | |
| /// a "span" (a tuple representing `(lo, hi)` equivalent of `Span`). | |
| fn get_span(self) -> Option<Span> { | |
| match self { | |
| Self::Ident(sp) | |
| | Self::Self_(sp) | |
| | Self::Macro(sp) | |
| | Self::PreludeTy(sp) | |
| | Self::PreludeVal(sp) => Some(sp), | |
| Self::Comment | |
| | Self::DocComment | |
| | Self::Attribute | |
| | Self::KeyWord | |
| | Self::RefKeyWord | |
| | Self::MacroNonTerminal | |
| | Self::String | |
| | Self::Number | |
| | Self::Bool | |
| | Self::Lifetime | |
| | Self::QuestionMark | |
| | Self::Decoration(_) | |
| | Self::Original | |
| | Self::Expansion => None, | |
| } | |
| } | |
| } | |
| impl fmt::Display for Class { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| let html = self.as_html(); | |
| if html.is_empty() { | |
| return Ok(()); | |
| } | |
| write!(f, " class=\"{html}\"") | |
| } | |
| } | |
| #[derive(Debug)] | |
| enum Highlight<'a> { | |
| Token { text: &'a str, class: Option<Class> }, | |
| EnterSpan { class: Class }, | |
| ExitSpan, | |
| } | |
| struct TokenIter<'a> { | |
| src: &'a str, | |
| cursor: Cursor<'a>, | |
| } | |
| impl<'a> TokenIter<'a> { | |
| fn new(src: &'a str) -> Self { | |
| Self { src, cursor: Cursor::new(src, FrontmatterAllowed::Yes) } | |
| } | |
| } | |
| impl<'a> Iterator for TokenIter<'a> { | |
| type Item = (TokenKind, &'a str); | |
| fn next(&mut self) -> Option<(TokenKind, &'a str)> { | |
| let token = self.cursor.advance_token(); | |
| if token.kind == TokenKind::Eof { | |
| return None; | |
| } | |
| let (text, rest) = self.src.split_at(token.len as usize); | |
| self.src = rest; | |
| Some((token.kind, text)) | |
| } | |
| } | |
| /// Used to know if a keyword followed by a `!` should never be treated as a macro. | |
| const NON_MACRO_KEYWORDS: &[&str] = &["if", "while", "match", "break", "return", "impl"]; | |
| /// This iterator comes from the same idea than "Peekable" except that it allows to "peek" more than | |
| /// just the next item by using `peek_next`. The `peek` method always returns the next item after | |
| /// the current one whereas `peek_next` will return the next item after the last one peeked. | |
| /// | |
| /// You can use both `peek` and `peek_next` at the same time without problem. | |
| struct PeekIter<'a> { | |
| stored: VecDeque<(TokenKind, &'a str)>, | |
| /// This position is reinitialized when using `next`. It is used in `peek_next`. | |
| peek_pos: usize, | |
| iter: TokenIter<'a>, | |
| } | |
| impl<'a> PeekIter<'a> { | |
| fn new(iter: TokenIter<'a>) -> Self { | |
| Self { stored: VecDeque::new(), peek_pos: 0, iter } | |
| } | |
| /// Returns the next item after the current one. It doesn't interfere with `peek_next` output. | |
| fn peek(&mut self) -> Option<(TokenKind, &'a str)> { | |
| if self.stored.is_empty() | |
| && let Some(next) = self.iter.next() | |
| { | |
| self.stored.push_back(next); | |
| } | |
| self.stored.front().copied() | |
| } | |
| /// Returns the next item after the last one peeked. It doesn't interfere with `peek` output. | |
| fn peek_next(&mut self) -> Option<(TokenKind, &'a str)> { | |
| self.peek_pos += 1; | |
| if self.peek_pos - 1 < self.stored.len() { | |
| self.stored.get(self.peek_pos - 1) | |
| } else if let Some(next) = self.iter.next() { | |
| self.stored.push_back(next); | |
| self.stored.back() | |
| } else { | |
| None | |
| } | |
| .copied() | |
| } | |
| fn peek_next_if<F: Fn((TokenKind, &'a str)) -> bool>( | |
| &mut self, | |
| f: F, | |
| ) -> Option<(TokenKind, &'a str)> { | |
| let next = self.peek_next()?; | |
| if f(next) { | |
| Some(next) | |
| } else { | |
| // We go one step back. | |
| self.peek_pos -= 1; | |
| None | |
| } | |
| } | |
| fn stop_peeking(&mut self) { | |
| self.peek_pos = 0; | |
| } | |
| } | |
| impl<'a> Iterator for PeekIter<'a> { | |
| type Item = (TokenKind, &'a str); | |
| fn next(&mut self) -> Option<Self::Item> { | |
| if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() } | |
| } | |
| } | |
| /// Custom spans inserted into the source. Eg --scrape-examples uses this to highlight function calls | |
| struct Decorations { | |
| starts: Vec<(u32, &'static str)>, | |
| ends: Vec<u32>, | |
| } | |
| impl Decorations { | |
| fn new(info: &DecorationInfo) -> Self { | |
| // Extract tuples (start, end, kind) into separate sequences of (start, kind) and (end). | |
| let (mut starts, mut ends): (Vec<_>, Vec<_>) = info | |
| .0 | |
| .iter() | |
| .flat_map(|(&kind, ranges)| ranges.iter().map(move |&(lo, hi)| ((lo, kind), hi))) | |
| .unzip(); | |
| // Sort the sequences in document order. | |
| starts.sort_by_key(|(lo, _)| *lo); | |
| ends.sort(); | |
| Decorations { starts, ends } | |
| } | |
| } | |
| /// Convenient wrapper to create a [`Span`] from a position in the file. | |
| fn new_span(lo: u32, text: &str, file_span: Span) -> Span { | |
| let hi = lo + text.len() as u32; | |
| let file_lo = file_span.lo(); | |
| file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi)) | |
| } | |
| fn classify<'src>( | |
| src: &'src str, | |
| file_span: Span, | |
| decoration_info: Option<&DecorationInfo>, | |
| sink: &mut dyn FnMut(Span, Highlight<'src>), | |
| ) { | |
| let offset = rustc_lexer::strip_shebang(src); | |
| if let Some(offset) = offset { | |
| sink(DUMMY_SP, Highlight::Token { text: &src[..offset], class: Some(Class::Comment) }); | |
| } | |
| let mut classifier = | |
| Classifier::new(src, offset.unwrap_or_default(), file_span, decoration_info); | |
| loop { | |
| if let Some(decs) = classifier.decorations.as_mut() { | |
| let byte_pos = classifier.byte_pos; | |
| let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count(); | |
| for (_, kind) in decs.starts.drain(0..n_starts) { | |
| sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Decoration(kind) }); | |
| } | |
| let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count(); | |
| for _ in decs.ends.drain(0..n_ends) { | |
| sink(DUMMY_SP, Highlight::ExitSpan); | |
| } | |
| } | |
| if let Some((TokenKind::Colon | TokenKind::Ident, _)) = classifier.tokens.peek() | |
| && let Some(nb_items) = classifier.get_full_ident_path() | |
| { | |
| let start = classifier.byte_pos as usize; | |
| let len: usize = iter::from_fn(|| classifier.next()) | |
| .take(nb_items) | |
| .map(|(_, text, _)| text.len()) | |
| .sum(); | |
| let text = &classifier.src[start..start + len]; | |
| classifier.advance(TokenKind::Ident, text, sink, start as u32); | |
| } else if let Some((token, text, before)) = classifier.next() { | |
| classifier.advance(token, text, sink, before); | |
| } else { | |
| break; | |
| } | |
| } | |
| } | |
| /// Processes program tokens, classifying strings of text by highlighting | |
| /// category (`Class`). | |
| struct Classifier<'src> { | |
| tokens: PeekIter<'src>, | |
| in_attribute: bool, | |
| in_macro: bool, | |
| in_macro_nonterminal: bool, | |
| byte_pos: u32, | |
| file_span: Span, | |
| src: &'src str, | |
| decorations: Option<Decorations>, | |
| } | |
| impl<'src> Classifier<'src> { | |
| /// Takes as argument the source code to HTML-ify and the source code file span | |
| /// which will be used later on by the `span_correspondence_map`. | |
| fn new( | |
| src: &'src str, | |
| byte_pos: usize, | |
| file_span: Span, | |
| decoration_info: Option<&DecorationInfo>, | |
| ) -> Self { | |
| Classifier { | |
| tokens: PeekIter::new(TokenIter::new(&src[byte_pos..])), | |
| in_attribute: false, | |
| in_macro: false, | |
| in_macro_nonterminal: false, | |
| byte_pos: byte_pos as u32, | |
| file_span, | |
| src, | |
| decorations: decoration_info.map(Decorations::new), | |
| } | |
| } | |
| /// Concatenate colons and idents as one when possible. | |
| fn get_full_ident_path(&mut self) -> Option<usize> { | |
| let mut has_ident = false; | |
| let mut nb_items = 0; | |
| let ret = loop { | |
| let mut nb = 0; | |
| while self.tokens.peek_next_if(|(token, _)| token == TokenKind::Colon).is_some() { | |
| nb += 1; | |
| nb_items += 1; | |
| } | |
| // Ident path can start with "::" but if we already have content in the ident path, | |
| // the "::" is mandatory. | |
| if has_ident && nb == 0 { | |
| break Some(nb_items); | |
| } else if nb != 0 && nb != 2 { | |
| if has_ident { | |
| // Following `;` will be handled on its own. | |
| break Some(nb_items - 1); | |
| } else { | |
| break None; | |
| } | |
| } | |
| if let Some((TokenKind::Ident, text)) = | |
| self.tokens.peek_next_if(|(token, _)| token == TokenKind::Ident) | |
| && let symbol = Symbol::intern(text) | |
| && (symbol.is_path_segment_keyword() || !is_keyword(symbol)) | |
| { | |
| has_ident = true; | |
| nb_items += 1; | |
| } else if nb > 0 && has_ident { | |
| // Following `;` will be handled on its own. | |
| break Some(nb_items - 1); | |
| } else if has_ident { | |
| break Some(nb_items); | |
| } else { | |
| break None; | |
| } | |
| }; | |
| self.tokens.stop_peeking(); | |
| ret | |
| } | |
| /// Wraps the tokens iteration to ensure that the `byte_pos` is always correct. | |
| /// | |
| /// It returns the token's kind, the token as a string and its byte position in the source | |
| /// string. | |
| fn next(&mut self) -> Option<(TokenKind, &'src str, u32)> { | |
| if let Some((kind, text)) = self.tokens.next() { | |
| let before = self.byte_pos; | |
| self.byte_pos += text.len() as u32; | |
| Some((kind, text, before)) | |
| } else { | |
| None | |
| } | |
| } | |
| fn new_macro_span( | |
| &mut self, | |
| text: &'src str, | |
| sink: &mut dyn FnMut(Span, Highlight<'src>), | |
| before: u32, | |
| file_span: Span, | |
| ) { | |
| self.in_macro = true; | |
| let span = new_span(before, text, file_span); | |
| sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Macro(span) }); | |
| sink(span, Highlight::Token { text, class: None }); | |
| } | |
| /// Single step of highlighting. This will classify `token`, but maybe also a couple of | |
| /// following ones as well. | |
| /// | |
| /// `before` is the position of the given token in the `source` string and is used as "lo" byte | |
| /// in case we want to try to generate a link for this token using the | |
| /// `span_correspondence_map`. | |
| fn advance( | |
| &mut self, | |
| token: TokenKind, | |
| text: &'src str, | |
| sink: &mut dyn FnMut(Span, Highlight<'src>), | |
| before: u32, | |
| ) { | |
| let lookahead = self.peek(); | |
| let file_span = self.file_span; | |
| let no_highlight = |sink: &mut dyn FnMut(_, _)| { | |
| sink(new_span(before, text, file_span), Highlight::Token { text, class: None }) | |
| }; | |
| let whitespace = |sink: &mut dyn FnMut(_, _)| { | |
| let mut start = 0u32; | |
| for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) { | |
| sink( | |
| new_span(before + start, part, file_span), | |
| Highlight::Token { text: part, class: None }, | |
| ); | |
| start += part.len() as u32; | |
| } | |
| }; | |
| let class = match token { | |
| TokenKind::Whitespace => return whitespace(sink), | |
| TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => { | |
| if doc_style.is_some() { | |
| Class::DocComment | |
| } else { | |
| Class::Comment | |
| } | |
| } | |
| TokenKind::Frontmatter { .. } => Class::Comment, | |
| // Consider this as part of a macro invocation if there was a | |
| // leading identifier. | |
| TokenKind::Bang if self.in_macro => { | |
| self.in_macro = false; | |
| sink(new_span(before, text, file_span), Highlight::Token { text, class: None }); | |
| sink(DUMMY_SP, Highlight::ExitSpan); | |
| return; | |
| } | |
| // Assume that '&' or '*' is the reference or dereference operator | |
| // or a reference or pointer type. Unless, of course, it looks like | |
| // a logical and or a multiplication operator: `&&` or `* `. | |
| TokenKind::Star => match self.tokens.peek() { | |
| Some((TokenKind::Whitespace, _)) => return whitespace(sink), | |
| Some((TokenKind::Ident, "mut")) => { | |
| self.next(); | |
| sink( | |
| DUMMY_SP, | |
| Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) }, | |
| ); | |
| return; | |
| } | |
| Some((TokenKind::Ident, "const")) => { | |
| self.next(); | |
| sink( | |
| DUMMY_SP, | |
| Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) }, | |
| ); | |
| return; | |
| } | |
| _ => Class::RefKeyWord, | |
| }, | |
| TokenKind::And => match self.tokens.peek() { | |
| Some((TokenKind::And, _)) => { | |
| self.next(); | |
| sink(DUMMY_SP, Highlight::Token { text: "&&", class: None }); | |
| return; | |
| } | |
| Some((TokenKind::Eq, _)) => { | |
| self.next(); | |
| sink(DUMMY_SP, Highlight::Token { text: "&=", class: None }); | |
| return; | |
| } | |
| Some((TokenKind::Whitespace, _)) => return whitespace(sink), | |
| Some((TokenKind::Ident, "mut")) => { | |
| self.next(); | |
| sink( | |
| DUMMY_SP, | |
| Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) }, | |
| ); | |
| return; | |
| } | |
| _ => Class::RefKeyWord, | |
| }, | |
| // These can either be operators, or arrows. | |
| TokenKind::Eq => match lookahead { | |
| Some(TokenKind::Eq) => { | |
| self.next(); | |
| sink(DUMMY_SP, Highlight::Token { text: "==", class: None }); | |
| return; | |
| } | |
| Some(TokenKind::Gt) => { | |
| self.next(); | |
| sink(DUMMY_SP, Highlight::Token { text: "=>", class: None }); | |
| return; | |
| } | |
| _ => return no_highlight(sink), | |
| }, | |
| TokenKind::Minus if lookahead == Some(TokenKind::Gt) => { | |
| self.next(); | |
| sink(DUMMY_SP, Highlight::Token { text: "->", class: None }); | |
| return; | |
| } | |
| // Other operators. | |
| TokenKind::Minus | |
| | TokenKind::Plus | |
| | TokenKind::Or | |
| | TokenKind::Slash | |
| | TokenKind::Caret | |
| | TokenKind::Percent | |
| | TokenKind::Bang | |
| | TokenKind::Lt | |
| | TokenKind::Gt => return no_highlight(sink), | |
| // Miscellaneous, no highlighting. | |
| TokenKind::Dot | |
| | TokenKind::Semi | |
| | TokenKind::Comma | |
| | TokenKind::OpenParen | |
| | TokenKind::CloseParen | |
| | TokenKind::OpenBrace | |
| | TokenKind::CloseBrace | |
| | TokenKind::OpenBracket | |
| | TokenKind::At | |
| | TokenKind::Tilde | |
| | TokenKind::Colon | |
| | TokenKind::Unknown => return no_highlight(sink), | |
| TokenKind::Question => Class::QuestionMark, | |
| TokenKind::Dollar => match lookahead { | |
| Some(TokenKind::Ident) => { | |
| self.in_macro_nonterminal = true; | |
| Class::MacroNonTerminal | |
| } | |
| _ => return no_highlight(sink), | |
| }, | |
| // This might be the start of an attribute. We're going to want to | |
| // continue highlighting it as an attribute until the ending ']' is | |
| // seen, so skip out early. Down below we terminate the attribute | |
| // span when we see the ']'. | |
| TokenKind::Pound => { | |
| match lookahead { | |
| // Case 1: #![inner_attribute] | |
| Some(TokenKind::Bang) => { | |
| self.next(); | |
| if let Some(TokenKind::OpenBracket) = self.peek() { | |
| self.in_attribute = true; | |
| sink( | |
| new_span(before, text, file_span), | |
| Highlight::EnterSpan { class: Class::Attribute }, | |
| ); | |
| } | |
| sink(DUMMY_SP, Highlight::Token { text: "#", class: None }); | |
| sink(DUMMY_SP, Highlight::Token { text: "!", class: None }); | |
| return; | |
| } | |
| // Case 2: #[outer_attribute] | |
| Some(TokenKind::OpenBracket) => { | |
| self.in_attribute = true; | |
| sink( | |
| new_span(before, text, file_span), | |
| Highlight::EnterSpan { class: Class::Attribute }, | |
| ); | |
| } | |
| _ => (), | |
| } | |
| return no_highlight(sink); | |
| } | |
| TokenKind::CloseBracket => { | |
| if self.in_attribute { | |
| self.in_attribute = false; | |
| sink( | |
| new_span(before, text, file_span), | |
| Highlight::Token { text: "]", class: None }, | |
| ); | |
| sink(DUMMY_SP, Highlight::ExitSpan); | |
| return; | |
| } | |
| return no_highlight(sink); | |
| } | |
| TokenKind::Literal { kind, .. } => match kind { | |
| // Text literals. | |
| LiteralKind::Byte { .. } | |
| | LiteralKind::Char { .. } | |
| | LiteralKind::Str { .. } | |
| | LiteralKind::ByteStr { .. } | |
| | LiteralKind::RawStr { .. } | |
| | LiteralKind::RawByteStr { .. } | |
| | LiteralKind::CStr { .. } | |
| | LiteralKind::RawCStr { .. } => Class::String, | |
| // Number literals. | |
| LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number, | |
| }, | |
| TokenKind::GuardedStrPrefix => return no_highlight(sink), | |
| TokenKind::RawIdent if self.check_if_macro_call("") => { | |
| self.new_macro_span(text, sink, before, file_span); | |
| return; | |
| } | |
| // Macro non-terminals (meta vars) take precedence. | |
| TokenKind::Ident if self.in_macro_nonterminal => { | |
| self.in_macro_nonterminal = false; | |
| Class::MacroNonTerminal | |
| } | |
| TokenKind::Ident => { | |
| let span = || new_span(before, text, file_span); | |
| match text { | |
| "ref" | "mut" => Class::RefKeyWord, | |
| "false" | "true" => Class::Bool, | |
| "self" | "Self" => Class::Self_(span()), | |
| "Option" | "Result" => Class::PreludeTy(span()), | |
| "Some" | "None" | "Ok" | "Err" => Class::PreludeVal(span()), | |
| _ if self.is_weak_keyword(text) || is_keyword(Symbol::intern(text)) => { | |
| // So if it's not a keyword which can be followed by a value (like `if` or | |
| // `return`) and the next non-whitespace token is a `!`, then we consider | |
| // it's a macro. | |
| if !NON_MACRO_KEYWORDS.contains(&text) && self.check_if_macro_call(text) { | |
| self.new_macro_span(text, sink, before, file_span); | |
| return; | |
| } | |
| Class::KeyWord | |
| } | |
| // If it's not a keyword and the next non whitespace token is a `!`, then | |
| // we consider it's a macro. | |
| _ if self.check_if_macro_call(text) => { | |
| self.new_macro_span(text, sink, before, file_span); | |
| return; | |
| } | |
| _ => Class::Ident(span()), | |
| } | |
| } | |
| TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => { | |
| Class::Ident(new_span(before, text, file_span)) | |
| } | |
| TokenKind::Lifetime { .. } | |
| | TokenKind::RawLifetime | |
| | TokenKind::UnknownPrefixLifetime => Class::Lifetime, | |
| TokenKind::Eof => panic!("Eof in advance"), | |
| }; | |
| // Anything that didn't return above is the simple case where we the | |
| // class just spans a single token, so we can use the `string` method. | |
| let mut start = 0u32; | |
| for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) { | |
| sink( | |
| new_span(before + start, part, file_span), | |
| Highlight::Token { text: part, class: Some(class) }, | |
| ); | |
| start += part.len() as u32; | |
| } | |
| } | |
| fn is_weak_keyword(&mut self, text: &str) -> bool { | |
| // NOTE: `yeet` (`do yeet $expr`), `catch` (`do catch $block`), `default` (specialization), | |
| // `contract_{ensures,requires}`, `builtin` (builtin_syntax) & `reuse` (fn_delegation) are | |
| // too difficult or annoying to properly detect under this simple scheme. | |
| let matches = match text { | |
| "auto" => |text| text == "trait", // `auto trait Trait {}` (`auto_traits`) | |
| "pin" => |text| text == "const" || text == "mut", // `&pin mut Type` (`pin_ergonomics`) | |
| "raw" => |text| text == "const" || text == "mut", // `&raw const local` | |
| "safe" => |text| text == "fn" || text == "extern", // `unsafe extern { safe fn f(); }` | |
| "union" => |_| true, // `union Untagged { field: () }` | |
| _ => return false, | |
| }; | |
| matches!(self.peek_non_trivia(), Some((TokenKind::Ident, text)) if matches(text)) | |
| } | |
| fn peek(&mut self) -> Option<TokenKind> { | |
| self.tokens.peek().map(|(kind, _)| kind) | |
| } | |
| fn peek_non_trivia(&mut self) -> Option<(TokenKind, &str)> { | |
| while let Some(token @ (kind, _)) = self.tokens.peek_next() { | |
| if let TokenKind::Whitespace | |
| | TokenKind::LineComment { doc_style: None } | |
| | TokenKind::BlockComment { doc_style: None, .. } = kind | |
| { | |
| continue; | |
| } | |
| self.tokens.stop_peeking(); | |
| return Some(token); | |
| } | |
| self.tokens.stop_peeking(); | |
| None | |
| } | |
| fn check_if_macro_call(&mut self, ident: &str) -> bool { | |
| let mut has_bang = false; | |
| let is_macro_rule_ident = ident == "macro_rules"; | |
| while let Some((kind, _)) = self.tokens.peek_next() { | |
| if let TokenKind::Whitespace | |
| | TokenKind::LineComment { doc_style: None } | |
| | TokenKind::BlockComment { doc_style: None, .. } = kind | |
| { | |
| continue; | |
| } | |
| if !has_bang { | |
| if kind != TokenKind::Bang { | |
| break; | |
| } | |
| has_bang = true; | |
| continue; | |
| } | |
| self.tokens.stop_peeking(); | |
| if is_macro_rule_ident { | |
| return matches!(kind, TokenKind::Ident | TokenKind::RawIdent); | |
| } | |
| return matches!( | |
| kind, | |
| TokenKind::OpenParen | TokenKind::OpenBracket | TokenKind::OpenBrace | |
| ); | |
| } | |
| self.tokens.stop_peeking(); | |
| false | |
| } | |
| } | |
| fn is_keyword(symbol: Symbol) -> bool { | |
| // FIXME(#148221): Don't hard-code the edition. The classifier should take it as an argument. | |
| symbol.is_reserved(|| Edition::Edition2024) | |
| } | |
| fn generate_link_to_def( | |
| out: &mut impl Write, | |
| text_s: &str, | |
| klass: Class, | |
| href_context: &Option<HrefContext<'_, '_>>, | |
| def_span: Span, | |
| open_tag: bool, | |
| ) -> bool { | |
| if let Some(href_context) = href_context | |
| && let Some(href) = | |
| href_context.context.shared.span_correspondence_map.get(&def_span).and_then(|href| { | |
| let context = href_context.context; | |
| // FIXME: later on, it'd be nice to provide two links (if possible) for all items: | |
| // one to the documentation page and one to the source definition. | |
| // FIXME: currently, external items only generate a link to their documentation, | |
| // a link to their definition can be generated using this: | |
| // https://github.com/rust-lang/rust/blob/60f1a2fc4b535ead9c85ce085fdce49b1b097531/src/librustdoc/html/render/context.rs#L315-L338 | |
| match href { | |
| LinkFromSrc::Local(span) => { | |
| context.href_from_span_relative(*span, &href_context.current_href) | |
| } | |
| LinkFromSrc::External(def_id) => { | |
| format::href_with_root_path(*def_id, context, Some(href_context.root_path)) | |
| .ok() | |
| .map(|HrefInfo { url, .. }| url) | |
| } | |
| LinkFromSrc::Primitive(prim) => format::href_with_root_path( | |
| PrimitiveType::primitive_locations(context.tcx())[prim], | |
| context, | |
| Some(href_context.root_path), | |
| ) | |
| .ok() | |
| .map(|HrefInfo { url, .. }| url), | |
| LinkFromSrc::Doc(def_id) => { | |
| format::href_with_root_path(*def_id, context, Some(href_context.root_path)) | |
| .ok() | |
| .map(|HrefInfo { url, .. }| url) | |
| } | |
| } | |
| }) | |
| { | |
| if !open_tag { | |
| // We're already inside an element which has the same klass, no need to give it | |
| // again. | |
| write!(out, "<a href=\"{href}\">{text_s}").unwrap(); | |
| } else { | |
| let klass_s = klass.as_html(); | |
| if klass_s.is_empty() { | |
| write!(out, "<a href=\"{href}\">{text_s}").unwrap(); | |
| } else { | |
| write!(out, "<a class=\"{klass_s}\" href=\"{href}\">{text_s}").unwrap(); | |
| } | |
| } | |
| return true; | |
| } | |
| false | |
| } | |
| /// This function writes `text` into `out` with some modifications depending on `klass`: | |
| /// | |
| /// * If `klass` is `None`, `text` is written into `out` with no modification. | |
| /// * If `klass` is `Some` but `klass.get_span()` is `None`, it writes the text wrapped in a | |
| /// `<span>` with the provided `klass`. | |
| /// * If `klass` is `Some` and has a [`rustc_span::Span`], it then tries to generate a link (`<a>` | |
| /// element) by retrieving the link information from the `span_correspondence_map` that was filled | |
| /// in `span_map.rs::collect_spans_and_sources`. If it cannot retrieve the information, then it's | |
| /// the same as the second point (`klass` is `Some` but doesn't have a [`rustc_span::Span`]). | |
| fn string_without_closing_tag<T: Display>( | |
| out: &mut impl Write, | |
| text: T, | |
| klass: Option<Class>, | |
| href_context: &Option<HrefContext<'_, '_>>, | |
| open_tag: bool, | |
| ) -> Option<&'static str> { | |
| let Some(klass) = klass else { | |
| write!(out, "{text}").unwrap(); | |
| return None; | |
| }; | |
| let Some(def_span) = klass.get_span() else { | |
| if !open_tag { | |
| write!(out, "{text}").unwrap(); | |
| return None; | |
| } | |
| write!(out, "<span class=\"{klass}\">{text}", klass = klass.as_html()).unwrap(); | |
| return Some("</span>"); | |
| }; | |
| let mut added_links = false; | |
| let mut text_s = text.to_string(); | |
| if text_s.contains("::") { | |
| let mut span = def_span.with_hi(def_span.lo()); | |
| text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| { | |
| span = span.with_hi(span.hi() + BytePos(t.len() as _)); | |
| match t { | |
| "::" => write!(&mut path, "::"), | |
| "self" | "Self" => write!( | |
| &mut path, | |
| "<span class=\"{klass}\">{t}</span>", | |
| klass = Class::Self_(DUMMY_SP).as_html(), | |
| ), | |
| "crate" | "super" => { | |
| write!( | |
| &mut path, | |
| "<span class=\"{klass}\">{t}</span>", | |
| klass = Class::KeyWord.as_html(), | |
| ) | |
| } | |
| t => { | |
| if !t.is_empty() | |
| && generate_link_to_def(&mut path, t, klass, href_context, span, open_tag) | |
| { | |
| added_links = true; | |
| write!(&mut path, "</a>") | |
| } else { | |
| write!(&mut path, "{t}") | |
| } | |
| } | |
| } | |
| .expect("Failed to build source HTML path"); | |
| span = span.with_lo(span.lo() + BytePos(t.len() as _)); | |
| path | |
| }); | |
| } | |
| if !added_links && generate_link_to_def(out, &text_s, klass, href_context, def_span, open_tag) { | |
| return Some("</a>"); | |
| } | |
| if !open_tag { | |
| out.write_str(&text_s).unwrap(); | |
| return None; | |
| } | |
| let klass_s = klass.as_html(); | |
| if klass_s.is_empty() { | |
| out.write_str(&text_s).unwrap(); | |
| Some("") | |
| } else { | |
| write!(out, "<span class=\"{klass_s}\">{text_s}").unwrap(); | |
| Some("</span>") | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests; |