//! 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 crate::clean::PrimitiveType; use crate::html::escape::Escape; use crate::html::render::{Context, LinkFromSrc}; use std::collections::VecDeque; use std::fmt::{Display, Write}; use rustc_data_structures::fx::FxHashMap; use rustc_lexer::{Cursor, LiteralKind, TokenKind}; use rustc_span::edition::Edition; use rustc_span::symbol::Symbol; use rustc_span::{BytePos, Span, DUMMY_SP}; use super::format::{self, Buffer}; /// 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) FxHashMap<&'static str, Vec<(u32, u32)>>); #[derive(Eq, PartialEq, Clone, Copy)] pub(crate) enum Tooltip { Ignore, CompileFail, ShouldPanic, Edition(Edition), None, } /// Highlights `src` as an inline example, returning the HTML output. pub(crate) fn render_example_with_highlighting( src: &str, out: &mut Buffer, tooltip: Tooltip, playground_button: Option<&str>, ) { write_header(out, "rust-example-rendered", None, tooltip); write_code(out, src, None, None); write_footer(out, playground_button); } /// Highlights `src` as an item-decl, returning the HTML output. pub(crate) fn render_item_decl_with_highlighting(src: &str, out: &mut Buffer) { write!(out, "
"); write_code(out, src, None, None); write!(out, ""); } fn write_header(out: &mut Buffer, class: &str, extra_content: Option
"); } else { write!(out, ""); } write!(out, "{}"); } /// Check if two `Class` can be merged together. In the following rules, "unclassified" means `None` /// basically (since it's `Option
`). 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 , class2: Option , 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(_), None) | (None, Some(_)) => text.trim().is_empty(), (None, None) => true, } } /// 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, /// It contains the closing tag and the associated `Class`. closing_tags: Vec<(&'static str, Class)>, /// This is used because we don't automatically generate the closing tag on `ExitSpan` in /// case an `EnterSpan` event with the same class follows. pending_exit_span: Option , /// `current_class` and `pending_elems` are used to group HTML elements with same `class` /// attributes to reduce the DOM size. current_class: Option , /// We need to keep the `Class` for each element because it could contain a `Span` which is /// used to generate links. pending_elems: Vec<(&'a str, Option )>, href_context: Option >, } impl<'a, 'tcx, F: Write> TokenHandler<'a, 'tcx, F> { fn handle_exit_span(&mut self) { // We can't get the last `closing_tags` element using `pop()` because `closing_tags` is // being used in `write_pending_elems`. let class = self.closing_tags.last().expect("ExitSpan without EnterSpan").1; // We flush everything just in case... self.write_pending_elems(Some(class)); exit_span(self.out, self.closing_tags.pop().expect("ExitSpan without EnterSpan").0); self.pending_exit_span = None; } /// Write all the pending elements sharing a same (or at mergeable) `Class`. /// /// If there is a "parent" (if a `EnterSpan` event was encountered) and the parent can be merged /// with the elements' class, then we simply write the elements since the `ExitSpan` event will /// close the tag. /// /// Otherwise, if there is only one pending element, we let the `string` function handle both /// opening and closing the tag, otherwise we do it into this function. /// /// It returns `true` if `current_class` must be set to `None` afterwards. fn write_pending_elems(&mut self, current_class: Option ) -> bool { if self.pending_elems.is_empty() { return false; } if let Some((_, parent_class)) = self.closing_tags.last() && can_merge(current_class, Some(*parent_class), "") { for (text, class) in self.pending_elems.iter() { string(self.out, Escape(text), *class, &self.href_context, false); } } else { // We only want to "open" the tag ourselves if we have more than one pending and if the // current parent tag is not the same as our pending content. let close_tag = if self.pending_elems.len() > 1 && let Some(current_class) = current_class { Some(enter_span(self.out, current_class, &self.href_context)) } else { None }; for (text, class) in self.pending_elems.iter() { string(self.out, Escape(text), *class, &self.href_context, close_tag.is_none()); } if let Some(close_tag) = close_tag { exit_span(self.out, close_tag); } } self.pending_elems.clear(); true } } impl<'a, 'tcx, F: Write> Drop for TokenHandler<'a, 'tcx, F> { /// When leaving, we need to flush all pending data to not have missing content. fn drop(&mut self) { if self.pending_exit_span.is_some() { self.handle_exit_span(); } else { self.write_pending_elems(self.current_class); } } } /// 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 >, decoration_info: Option , ) { // This replace allows to fix how the code source with DOS backline characters is displayed. let src = src.replace("\r\n", "\n"); let mut token_handler = TokenHandler { out, closing_tags: Vec::new(), pending_exit_span: None, current_class: None, pending_elems: Vec::new(), href_context, }; Classifier::new( &src, token_handler.href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP), decoration_info, ) .highlight(&mut |highlight| { match highlight { Highlight::Token { text, class } => { // If we received a `ExitSpan` event and then have a non-compatible `Class`, we // need to close the ``. let need_current_class_update = if let Some(pending) = token_handler.pending_exit_span && !can_merge(Some(pending), class, text) { token_handler.handle_exit_span(); true // If the two `Class` are different, time to flush the current content and start // a new one. } else if !can_merge(token_handler.current_class, class, text) { token_handler.write_pending_elems(token_handler.current_class); true } else { token_handler.current_class.is_none() }; if need_current_class_update { token_handler.current_class = class.map(Class::dummy); } token_handler.pending_elems.push((text, class)); } Highlight::EnterSpan { class } => { let mut should_add = true; if let Some(pending_exit_span) = token_handler.pending_exit_span { if class.is_equal_to(pending_exit_span) { should_add = false; } else { token_handler.handle_exit_span(); } } else { // We flush everything just in case... if token_handler.write_pending_elems(token_handler.current_class) { token_handler.current_class = None; } } if should_add { let closing_tag = enter_span(token_handler.out, class, &token_handler.href_context); token_handler.closing_tags.push((closing_tag, class)); } token_handler.current_class = None; token_handler.pending_exit_span = None; } Highlight::ExitSpan => { token_handler.current_class = None; token_handler.pending_exit_span = Some(token_handler.closing_tags.last().as_ref().expect("ExitSpan without EnterSpan").1); } }; }); } fn write_footer(out: &mut Buffer, playground_button: Option<&str>) { writeln!(out, "