Rollup merge of #85833 - willcrichton:example-analyzer, r=jyn514

Scrape code examples from examples/ directory for Rustdoc

Adds support for the functionality described in https://github.com/rust-lang/rfcs/pull/3123

Matching changes to Cargo are here: https://github.com/rust-lang/cargo/pull/9525

Live demo here: https://willcrichton.net/example-analyzer/warp/trait.Filter.html#method.and
This commit is contained in:
Matthias Krüger 2021-10-23 14:58:39 +02:00 committed by GitHub
commit dcf9242795
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1104 additions and 30 deletions

View file

@ -12,6 +12,7 @@ use crate::html::render::Context;
use std::collections::VecDeque;
use std::fmt::{Display, Write};
use rustc_data_structures::fx::FxHashMap;
use rustc_lexer::{LiteralKind, TokenKind};
use rustc_span::edition::Edition;
use rustc_span::symbol::Symbol;
@ -30,6 +31,10 @@ crate struct ContextInfo<'a, 'b, 'c> {
crate root_path: &'c str,
}
/// 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.
crate struct DecorationInfo(crate FxHashMap<&'static str, Vec<(u32, u32)>>);
/// Highlights `src`, returning the HTML output.
crate fn render_with_highlighting(
src: &str,
@ -40,6 +45,7 @@ crate fn render_with_highlighting(
edition: Edition,
extra_content: Option<Buffer>,
context_info: Option<ContextInfo<'_, '_, '_>>,
decoration_info: Option<DecorationInfo>,
) {
debug!("highlighting: ================\n{}\n==============", src);
if let Some((edition_info, class)) = tooltip {
@ -56,7 +62,7 @@ crate fn render_with_highlighting(
}
write_header(out, class, extra_content);
write_code(out, &src, edition, context_info);
write_code(out, &src, edition, context_info, decoration_info);
write_footer(out, playground_button);
}
@ -89,17 +95,23 @@ fn write_code(
src: &str,
edition: Edition,
context_info: Option<ContextInfo<'_, '_, '_>>,
decoration_info: Option<DecorationInfo>,
) {
// This replace allows to fix how the code source with DOS backline characters is displayed.
let src = src.replace("\r\n", "\n");
Classifier::new(&src, edition, context_info.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP))
.highlight(&mut |highlight| {
match highlight {
Highlight::Token { text, class } => string(out, Escape(text), class, &context_info),
Highlight::EnterSpan { class } => enter_span(out, class),
Highlight::ExitSpan => exit_span(out),
};
});
Classifier::new(
&src,
edition,
context_info.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
decoration_info,
)
.highlight(&mut |highlight| {
match highlight {
Highlight::Token { text, class } => string(out, Escape(text), class, &context_info),
Highlight::EnterSpan { class } => enter_span(out, class),
Highlight::ExitSpan => exit_span(out),
};
});
}
fn write_footer(out: &mut Buffer, playground_button: Option<&str>) {
@ -127,6 +139,7 @@ enum Class {
PreludeTy,
PreludeVal,
QuestionMark,
Decoration(&'static str),
}
impl Class {
@ -150,6 +163,7 @@ impl Class {
Class::PreludeTy => "prelude-ty",
Class::PreludeVal => "prelude-val",
Class::QuestionMark => "question-mark",
Class::Decoration(kind) => kind,
}
}
@ -248,6 +262,24 @@ impl Iterator for PeekIter<'a> {
}
}
/// 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 {
let (starts, ends) = info
.0
.into_iter()
.map(|(kind, ranges)| ranges.into_iter().map(move |(lo, hi)| ((lo, kind), hi)))
.flatten()
.unzip();
Decorations { starts, ends }
}
}
/// Processes program tokens, classifying strings of text by highlighting
/// category (`Class`).
struct Classifier<'a> {
@ -259,13 +291,20 @@ struct Classifier<'a> {
byte_pos: u32,
file_span: Span,
src: &'a str,
decorations: Option<Decorations>,
}
impl<'a> Classifier<'a> {
/// Takes as argument the source code to HTML-ify, the rust edition to use and the source code
/// file span which will be used later on by the `span_correspondance_map`.
fn new(src: &str, edition: Edition, file_span: Span) -> Classifier<'_> {
fn new(
src: &str,
edition: Edition,
file_span: Span,
decoration_info: Option<DecorationInfo>,
) -> Classifier<'_> {
let tokens = PeekIter::new(TokenIter { src });
let decorations = decoration_info.map(Decorations::new);
Classifier {
tokens,
in_attribute: false,
@ -275,6 +314,7 @@ impl<'a> Classifier<'a> {
byte_pos: 0,
file_span,
src,
decorations,
}
}
@ -356,6 +396,19 @@ impl<'a> Classifier<'a> {
/// token is used.
fn highlight(mut self, sink: &mut dyn FnMut(Highlight<'a>)) {
loop {
if let Some(decs) = self.decorations.as_mut() {
let byte_pos = self.byte_pos;
let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count();
for (_, kind) in decs.starts.drain(0..n_starts) {
sink(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(Highlight::ExitSpan);
}
}
if self
.tokens
.peek()
@ -657,7 +710,7 @@ fn string<T: Display>(
// 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(*span)
.href_from_span(*span, true)
.map(|s| format!("{}{}", context_info.root_path, s)),
LinkFromSrc::External(def_id) => {
format::href_with_root_path(*def_id, context, Some(context_info.root_path))

View file

@ -0,0 +1,2 @@
<span class="example"><span class="kw">let</span> <span class="ident">x</span> <span class="op">=</span> <span class="number">1</span>;</span>
<span class="kw">let</span> <span class="ident">y</span> <span class="op">=</span> <span class="number">2</span>;

View file

@ -1,6 +1,7 @@
use super::write_code;
use super::{write_code, DecorationInfo};
use crate::html::format::Buffer;
use expect_test::expect_file;
use rustc_data_structures::fx::FxHashMap;
use rustc_span::create_default_session_globals_then;
use rustc_span::edition::Edition;
@ -22,7 +23,7 @@ fn test_html_highlighting() {
let src = include_str!("fixtures/sample.rs");
let html = {
let mut out = Buffer::new();
write_code(&mut out, src, Edition::Edition2018, None);
write_code(&mut out, src, Edition::Edition2018, None, None);
format!("{}<pre><code>{}</code></pre>\n", STYLE, out.into_inner())
};
expect_file!["fixtures/sample.html"].assert_eq(&html);
@ -36,7 +37,7 @@ fn test_dos_backline() {
println!(\"foo\");\r\n\
}\r\n";
let mut html = Buffer::new();
write_code(&mut html, src, Edition::Edition2018, None);
write_code(&mut html, src, Edition::Edition2018, None, None);
expect_file!["fixtures/dos_line.html"].assert_eq(&html.into_inner());
});
}
@ -50,7 +51,7 @@ let x = super::b::foo;
let y = Self::whatever;";
let mut html = Buffer::new();
write_code(&mut html, src, Edition::Edition2018, None);
write_code(&mut html, src, Edition::Edition2018, None, None);
expect_file!["fixtures/highlight.html"].assert_eq(&html.into_inner());
});
}
@ -60,7 +61,21 @@ fn test_union_highlighting() {
create_default_session_globals_then(|| {
let src = include_str!("fixtures/union.rs");
let mut html = Buffer::new();
write_code(&mut html, src, Edition::Edition2018, None);
write_code(&mut html, src, Edition::Edition2018, None, None);
expect_file!["fixtures/union.html"].assert_eq(&html.into_inner());
});
}
#[test]
fn test_decorations() {
create_default_session_globals_then(|| {
let src = "let x = 1;
let y = 2;";
let mut decorations = FxHashMap::default();
decorations.insert("example", vec![(0, 10)]);
let mut html = Buffer::new();
write_code(&mut html, src, Edition::Edition2018, None, Some(DecorationInfo(decorations)));
expect_file!["fixtures/decorations.html"].assert_eq(&html.into_inner());
});
}

View file

@ -22,6 +22,8 @@ crate struct Layout {
/// If false, the `select` element to have search filtering by crates on rendered docs
/// won't be generated.
crate generate_search_filter: bool,
/// If true, then scrape-examples.js will be included in the output HTML file
crate scrape_examples_extension: bool,
}
#[derive(Serialize)]

View file

@ -360,6 +360,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
edition,
None,
None,
None,
);
Some(Event::Html(s.into_inner().into()))
}

View file

@ -34,6 +34,7 @@ use crate::html::escape::Escape;
use crate::html::format::Buffer;
use crate::html::markdown::{self, plain_text_summary, ErrorCodes, IdMap};
use crate::html::{layout, sources};
use crate::scrape_examples::AllCallLocations;
/// Major driving force in all rustdoc rendering. This contains information
/// about where in the tree-like hierarchy rendering is occurring and controls
@ -123,6 +124,8 @@ crate struct SharedContext<'tcx> {
crate span_correspondance_map: FxHashMap<rustc_span::Span, LinkFromSrc>,
/// The [`Cache`] used during rendering.
crate cache: Cache,
crate call_locations: AllCallLocations,
}
impl SharedContext<'_> {
@ -291,10 +294,10 @@ impl<'tcx> Context<'tcx> {
/// may happen, for example, with externally inlined items where the source
/// of their crate documentation isn't known.
pub(super) fn src_href(&self, item: &clean::Item) -> Option<String> {
self.href_from_span(item.span(self.tcx()))
self.href_from_span(item.span(self.tcx()), true)
}
crate fn href_from_span(&self, span: clean::Span) -> Option<String> {
crate fn href_from_span(&self, span: clean::Span, with_lines: bool) -> Option<String> {
if span.is_dummy() {
return None;
}
@ -341,16 +344,26 @@ impl<'tcx> Context<'tcx> {
(&*symbol, &path)
};
let loline = span.lo(self.sess()).line;
let hiline = span.hi(self.sess()).line;
let lines =
if loline == hiline { loline.to_string() } else { format!("{}-{}", loline, hiline) };
let anchor = if with_lines {
let loline = span.lo(self.sess()).line;
let hiline = span.hi(self.sess()).line;
format!(
"#{}",
if loline == hiline {
loline.to_string()
} else {
format!("{}-{}", loline, hiline)
}
)
} else {
"".to_string()
};
Some(format!(
"{root}src/{krate}/{path}#{lines}",
"{root}src/{krate}/{path}{anchor}",
root = Escape(&root),
krate = krate,
path = path,
lines = lines
anchor = anchor
))
}
}
@ -388,6 +401,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
generate_redirect_map,
show_type_layout,
generate_link_to_definition,
call_locations,
..
} = options;
@ -412,6 +426,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
krate: krate.name.to_string(),
css_file_extension: extension_css,
generate_search_filter,
scrape_examples_extension: !call_locations.is_empty(),
};
let mut issue_tracker_base_url = None;
let mut include_sources = true;
@ -474,6 +489,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
templates,
span_correspondance_map: matches,
cache,
call_locations,
};
// Add the default themes to the `Vec` of stylepaths

View file

@ -40,20 +40,25 @@ crate use span_map::{collect_spans_and_sources, LinkFromSrc};
use std::collections::VecDeque;
use std::default::Default;
use std::fmt;
use std::fs;
use std::iter::Peekable;
use std::path::PathBuf;
use std::str;
use std::string::ToString;
use rustc_ast_pretty::pprust;
use rustc_attr::{ConstStability, Deprecation, StabilityLevel};
use rustc_data_structures::fx::FxHashSet;
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_hir as hir;
use rustc_hir::def::CtorKind;
use rustc_hir::def_id::DefId;
use rustc_hir::Mutability;
use rustc_middle::middle::stability;
use rustc_middle::ty::TyCtxt;
use rustc_span::symbol::{kw, sym, Symbol};
use rustc_span::{
symbol::{kw, sym, Symbol},
BytePos, FileName, RealFileName,
};
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
@ -68,7 +73,10 @@ use crate::html::format::{
href, print_abi_with_space, print_constness_with_space, print_default_space,
print_generic_bounds, print_where_clause, Buffer, HrefError, PrintWithSpace,
};
use crate::html::highlight;
use crate::html::markdown::{HeadingOffset, Markdown, MarkdownHtml, MarkdownSummaryLine};
use crate::html::sources;
use crate::scrape_examples::CallData;
/// A pair of name and its optional document.
crate type NameDoc = (String, Option<String>);
@ -585,6 +593,14 @@ fn document_full_inner(
render_markdown(w, cx, &s, item.links(cx), heading_offset);
}
}
let kind = match &*item.kind {
clean::ItemKind::StrippedItem(box kind) | kind => kind,
};
if let clean::ItemKind::FunctionItem(..) | clean::ItemKind::MethodItem(..) = kind {
render_call_locations(w, cx, item);
}
}
/// Add extra information about an item such as:
@ -2490,3 +2506,221 @@ fn collect_paths_for_type(first_ty: clean::Type, cache: &Cache) -> Vec<String> {
}
out
}
const MAX_FULL_EXAMPLES: usize = 5;
const NUM_VISIBLE_LINES: usize = 10;
/// Generates the HTML for example call locations generated via the --scrape-examples flag.
fn render_call_locations(w: &mut Buffer, cx: &Context<'_>, item: &clean::Item) {
let tcx = cx.tcx();
let def_id = item.def_id.expect_def_id();
let key = tcx.def_path_hash(def_id);
let call_locations = match cx.shared.call_locations.get(&key) {
Some(call_locations) => call_locations,
_ => {
return;
}
};
// Generate a unique ID so users can link to this section for a given method
let id = cx.id_map.borrow_mut().derive("scraped-examples");
write!(
w,
"<div class=\"docblock scraped-example-list\">\
<span></span>\
<h5 id=\"{id}\" class=\"section-header\">\
<a href=\"#{id}\">Examples found in repository</a>\
</h5>",
id = id
);
// Generate the HTML for a single example, being the title and code block
let write_example = |w: &mut Buffer, (path, call_data): (&PathBuf, &CallData)| -> bool {
let contents = match fs::read_to_string(&path) {
Ok(contents) => contents,
Err(err) => {
let span = item.span(tcx).inner();
tcx.sess
.span_err(span, &format!("failed to read file {}: {}", path.display(), err));
return false;
}
};
// To reduce file sizes, we only want to embed the source code needed to understand the example, not
// the entire file. So we find the smallest byte range that covers all items enclosing examples.
assert!(!call_data.locations.is_empty());
let min_loc =
call_data.locations.iter().min_by_key(|loc| loc.enclosing_item.byte_span.0).unwrap();
let byte_min = min_loc.enclosing_item.byte_span.0;
let line_min = min_loc.enclosing_item.line_span.0;
let max_loc =
call_data.locations.iter().max_by_key(|loc| loc.enclosing_item.byte_span.1).unwrap();
let byte_max = max_loc.enclosing_item.byte_span.1;
let line_max = max_loc.enclosing_item.line_span.1;
// The output code is limited to that byte range.
let contents_subset = &contents[(byte_min as usize)..(byte_max as usize)];
// The call locations need to be updated to reflect that the size of the program has changed.
// Specifically, the ranges are all subtracted by `byte_min` since that's the new zero point.
let (mut byte_ranges, line_ranges): (Vec<_>, Vec<_>) = call_data
.locations
.iter()
.map(|loc| {
let (byte_lo, byte_hi) = loc.call_expr.byte_span;
let (line_lo, line_hi) = loc.call_expr.line_span;
let byte_range = (byte_lo - byte_min, byte_hi - byte_min);
let line_range = (line_lo - line_min, line_hi - line_min);
let (anchor, line_title) = if line_lo == line_hi {
(format!("{}", line_lo + 1), format!("line {}", line_lo + 1))
} else {
(
format!("{}-{}", line_lo + 1, line_hi + 1),
format!("lines {}-{}", line_lo + 1, line_hi + 1),
)
};
let line_url = format!("{}{}#{}", cx.root_path(), call_data.url, anchor);
(byte_range, (line_range, line_url, line_title))
})
.unzip();
let (_, init_url, init_title) = &line_ranges[0];
let needs_expansion = line_max - line_min > NUM_VISIBLE_LINES;
let locations_encoded = serde_json::to_string(&line_ranges).unwrap();
write!(
w,
"<div class=\"scraped-example {expanded_cls}\" data-locs=\"{locations}\">\
<div class=\"scraped-example-title\">\
{name} (<a href=\"{url}\">{title}</a>)\
</div>\
<div class=\"code-wrapper\">",
expanded_cls = if needs_expansion { "" } else { "expanded" },
name = call_data.display_name,
url = init_url,
title = init_title,
// The locations are encoded as a data attribute, so they can be read
// later by the JS for interactions.
locations = Escape(&locations_encoded)
);
if line_ranges.len() > 1 {
write!(w, r#"<span class="prev">&pr;</span> <span class="next">&sc;</span>"#);
}
if needs_expansion {
write!(w, r#"<span class="expand">&varr;</span>"#);
}
// Look for the example file in the source map if it exists, otherwise return a dummy span
let file_span = (|| {
let source_map = tcx.sess.source_map();
let crate_src = tcx.sess.local_crate_source_file.as_ref()?;
let abs_crate_src = crate_src.canonicalize().ok()?;
let crate_root = abs_crate_src.parent()?.parent()?;
let rel_path = path.strip_prefix(crate_root).ok()?;
let files = source_map.files();
let file = files.iter().find(|file| match &file.name {
FileName::Real(RealFileName::LocalPath(other_path)) => rel_path == other_path,
_ => false,
})?;
Some(rustc_span::Span::with_root_ctxt(
file.start_pos + BytePos(byte_min),
file.start_pos + BytePos(byte_max),
))
})()
.unwrap_or(rustc_span::DUMMY_SP);
// The root path is the inverse of Context::current
let root_path = vec!["../"; cx.current.len() - 1].join("");
let mut decoration_info = FxHashMap::default();
decoration_info.insert("highlight focus", vec![byte_ranges.remove(0)]);
decoration_info.insert("highlight", byte_ranges);
sources::print_src(
w,
contents_subset,
call_data.edition,
file_span,
cx,
&root_path,
Some(highlight::DecorationInfo(decoration_info)),
sources::SourceContext::Embedded { offset: line_min },
);
write!(w, "</div></div>");
true
};
// The call locations are output in sequence, so that sequence needs to be determined.
// Ideally the most "relevant" examples would be shown first, but there's no general algorithm
// for determining relevance. Instead, we prefer the smallest examples being likely the easiest to
// understand at a glance.
let ordered_locations = {
let sort_criterion = |(_, call_data): &(_, &CallData)| {
// Use the first location because that's what the user will see initially
let (lo, hi) = call_data.locations[0].enclosing_item.byte_span;
hi - lo
};
let mut locs = call_locations.into_iter().collect::<Vec<_>>();
locs.sort_by_key(sort_criterion);
locs
};
let mut it = ordered_locations.into_iter().peekable();
// An example may fail to write if its source can't be read for some reason, so this method
// continues iterating until a write suceeds
let write_and_skip_failure = |w: &mut Buffer, it: &mut Peekable<_>| {
while let Some(example) = it.next() {
if write_example(&mut *w, example) {
break;
}
}
};
// Write just one example that's visible by default in the method's description.
write_and_skip_failure(w, &mut it);
// Then add the remaining examples in a hidden section.
if it.peek().is_some() {
write!(
w,
"<details class=\"rustdoc-toggle more-examples-toggle\">\
<summary class=\"hideme\">\
<span>More examples</span>\
</summary>\
<div class=\"more-scraped-examples\">\
<div class=\"toggle-line\"><div class=\"toggle-line-inner\"></div></div>\
<div class=\"more-scraped-examples-inner\">"
);
// Only generate inline code for MAX_FULL_EXAMPLES number of examples. Otherwise we could
// make the page arbitrarily huge!
for _ in 0..MAX_FULL_EXAMPLES {
write_and_skip_failure(w, &mut it);
}
// For the remaining examples, generate a <ul> containing links to the source files.
if it.peek().is_some() {
write!(w, r#"<div class="example-links">Additional examples can be found in:<br><ul>"#);
it.for_each(|(_, call_data)| {
write!(
w,
r#"<li><a href="{root}{url}">{name}</a></li>"#,
root = cx.root_path(),
url = call_data.url,
name = call_data.display_name
);
});
write!(w, "</ul></div>");
}
write!(w, "</div></div></details>");
}
write!(w, "</div>");
}

View file

@ -1159,6 +1159,7 @@ fn item_macro(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Mac
it.span(cx.tcx()).inner().edition(),
None,
None,
None,
);
});
document(w, cx, it, None, HeadingOffset::H2)

View file

@ -304,6 +304,15 @@ pub(super) fn write_shared(
)?;
}
if cx.shared.layout.scrape_examples_extension {
cx.write_minify(
SharedResource::InvocationSpecific { basename: "scrape-examples.js" },
static_files::SCRAPE_EXAMPLES_JS,
options.enable_minification,
&options.emit,
)?;
}
if let Some(ref css) = cx.shared.layout.css_file_extension {
let buffer = try_err!(fs::read_to_string(css), css);
// This varies based on the invocation, so it can't go through the write_minify wrapper.

View file

@ -204,7 +204,16 @@ impl SourceCollector<'_, 'tcx> {
&page,
"",
|buf: &mut _| {
print_src(buf, contents, self.cx.shared.edition(), file_span, &self.cx, &root_path)
print_src(
buf,
contents,
self.cx.shared.edition(),
file_span,
&self.cx,
&root_path,
None,
SourceContext::Standalone,
)
},
&self.cx.shared.style_files,
);
@ -241,15 +250,22 @@ where
}
}
crate enum SourceContext {
Standalone,
Embedded { offset: usize },
}
/// Wrapper struct to render the source code of a file. This will do things like
/// adding line numbers to the left-hand side.
fn print_src(
crate fn print_src(
buf: &mut Buffer,
s: &str,
edition: Edition,
file_span: rustc_span::Span,
context: &Context<'_>,
root_path: &str,
decoration_info: Option<highlight::DecorationInfo>,
source_context: SourceContext,
) {
let lines = s.lines().count();
let mut line_numbers = Buffer::empty_from(buf);
@ -261,7 +277,14 @@ fn print_src(
}
line_numbers.write_str("<pre class=\"line-numbers\">");
for i in 1..=lines {
writeln!(line_numbers, "<span id=\"{0}\">{0:1$}</span>", i, cols);
match source_context {
SourceContext::Standalone => {
writeln!(line_numbers, "<span id=\"{0}\">{0:1$}</span>", i, cols)
}
SourceContext::Embedded { offset } => {
writeln!(line_numbers, "<span>{0:1$}</span>", i + offset, cols)
}
}
}
line_numbers.write_str("</pre>");
highlight::render_with_highlighting(
@ -273,5 +296,6 @@ fn print_src(
edition,
Some(line_numbers),
Some(highlight::ContextInfo { context, file_span, root_path }),
decoration_info,
);
}

View file

@ -467,6 +467,11 @@ nav.sub {
overflow-x: auto;
}
.rustdoc:not(.source) .example-wrap > pre.line-numbers {
width: auto;
overflow-x: visible;
}
.rustdoc .example-wrap > pre {
margin: 0;
}
@ -1980,3 +1985,166 @@ details.undocumented[open] > summary::before {
overflow-wrap: anywhere;
}
}
/* Begin: styles for --scrape-examples feature */
.scraped-example-title {
font-family: 'Fira Sans';
}
.scraped-example:not(.expanded) .code-wrapper pre.line-numbers {
overflow: hidden;
max-height: 240px;
}
.scraped-example:not(.expanded) .code-wrapper .example-wrap pre.rust {
overflow-y: hidden;
max-height: 240px;
padding-bottom: 0;
}
.scraped-example .code-wrapper .prev {
position: absolute;
top: 0.25em;
right: 2.25em;
z-index: 100;
cursor: pointer;
}
.scraped-example .code-wrapper .next {
position: absolute;
top: 0.25em;
right: 1.25em;
z-index: 100;
cursor: pointer;
}
.scraped-example .code-wrapper .expand {
position: absolute;
top: 0.25em;
right: 0.25em;
z-index: 100;
cursor: pointer;
}
.scraped-example .code-wrapper {
position: relative;
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
}
.scraped-example:not(.expanded) .code-wrapper:before {
content: " ";
width: 100%;
height: 5px;
position: absolute;
z-index: 100;
top: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
}
.scraped-example:not(.expanded) .code-wrapper:after {
content: " ";
width: 100%;
height: 5px;
position: absolute;
z-index: 100;
bottom: 0;
background: linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
}
.scraped-example:not(.expanded) .code-wrapper {
overflow: hidden;
max-height: 240px;
}
.scraped-example .code-wrapper .line-numbers {
margin: 0;
padding: 14px 0;
}
.scraped-example .code-wrapper .line-numbers span {
padding: 0 14px;
}
.scraped-example .code-wrapper .example-wrap {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
margin-bottom: 0;
}
.scraped-example .code-wrapper .example-wrap pre.rust {
overflow-x: inherit;
width: inherit;
overflow-y: hidden;
}
.scraped-example .example-wrap .rust span.highlight {
background: #fcffd6;
}
.scraped-example .example-wrap .rust span.highlight.focus {
background: #f6fdb0;
}
.more-examples-toggle {
margin-top: 10px;
}
.more-examples-toggle summary {
color: #999;
font-family: 'Fira Sans';
}
.more-scraped-examples {
margin-left: 25px;
display: flex;
flex-direction: row;
width: calc(100% - 25px);
}
.more-scraped-examples-inner {
/* 20px is width of toggle-line + toggle-line-inner */
width: calc(100% - 20px);
}
.toggle-line {
align-self: stretch;
margin-right: 10px;
margin-top: 5px;
padding: 0 4px;
cursor: pointer;
}
.toggle-line:hover .toggle-line-inner {
background: #aaa;
}
.toggle-line-inner {
min-width: 2px;
background: #ddd;
height: 100%;
}
.more-scraped-examples .scraped-example {
margin-bottom: 20px;
}
.more-scraped-examples .scraped-example:last-child {
margin-bottom: 0;
}
.example-links a {
margin-top: 20px;
font-family: 'Fira Sans';
}
.example-links ul {
margin-bottom: 0;
}
/* End: styles for --scrape-examples feature */

View file

@ -613,3 +613,22 @@ div.files > .selected {
input:checked + .slider {
background-color: #ffb454 !important;
}
.scraped-example .example-wrap .rust span.highlight {
background: rgb(91, 59, 1);
}
.scraped-example .example-wrap .rust span.highlight.focus {
background: rgb(124, 75, 15);
}
.scraped-example:not(.expanded) .code-wrapper:before {
background: linear-gradient(to bottom, rgba(15, 20, 25, 1), rgba(15, 20, 25, 0));
}
.scraped-example:not(.expanded) .code-wrapper:after {
background: linear-gradient(to top, rgba(15, 20, 25, 1), rgba(15, 20, 25, 0));
}
.toggle-line-inner {
background: #616161;
}
.toggle-line:hover .toggle-line-inner {
background: ##898989;
}

View file

@ -485,3 +485,22 @@ div.files > .selected {
.setting-line > .title {
border-bottom-color: #ddd;
}
.scraped-example .example-wrap .rust span.highlight {
background: rgb(91, 59, 1);
}
.scraped-example .example-wrap .rust span.highlight.focus {
background: rgb(124, 75, 15);
}
.scraped-example:not(.expanded) .code-wrapper:before {
background: linear-gradient(to bottom, rgba(53, 53, 53, 1), rgba(53, 53, 53, 0));
}
.scraped-example:not(.expanded) .code-wrapper:after {
background: linear-gradient(to top, rgba(53, 53, 53, 1), rgba(53, 53, 53, 0));
}
.toggle-line-inner {
background: #616161;
}
.toggle-line:hover .toggle-line-inner {
background: ##898989;
}

View file

@ -0,0 +1,86 @@
/* global addClass, hasClass, removeClass, onEach */
(function () {
// Scroll code block to put the given code location in the middle of the viewer
function scrollToLoc(elt, loc) {
var wrapper = elt.querySelector(".code-wrapper");
var halfHeight = wrapper.offsetHeight / 2;
var lines = elt.querySelector('.line-numbers');
var offsetMid = (lines.children[loc[0]].offsetTop
+ lines.children[loc[1]].offsetTop) / 2;
var scrollOffset = offsetMid - halfHeight;
lines.scrollTo(0, scrollOffset);
elt.querySelector(".rust").scrollTo(0, scrollOffset);
}
function updateScrapedExample(example) {
var locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent);
var locIndex = 0;
var highlights = example.querySelectorAll('.highlight');
var link = example.querySelector('.scraped-example-title a');
if (locs.length > 1) {
// Toggle through list of examples in a given file
var onChangeLoc = function(changeIndex) {
removeClass(highlights[locIndex], 'focus');
changeIndex();
scrollToLoc(example, locs[locIndex][0]);
addClass(highlights[locIndex], 'focus');
var url = locs[locIndex][1];
var title = locs[locIndex][2];
link.href = url;
link.innerHTML = title;
};
example.querySelector('.prev')
.addEventListener('click', function() {
onChangeLoc(function() {
locIndex = (locIndex - 1 + locs.length) % locs.length;
});
});
example.querySelector('.next')
.addEventListener('click', function() {
onChangeLoc(function() {
locIndex = (locIndex + 1) % locs.length;
});
});
}
var expandButton = example.querySelector('.expand');
if (expandButton) {
expandButton.addEventListener('click', function () {
if (hasClass(example, "expanded")) {
removeClass(example, "expanded");
scrollToLoc(example, locs[0][0]);
} else {
addClass(example, "expanded");
}
});
}
// Start with the first example in view
scrollToLoc(example, locs[0][0]);
}
var firstExamples = document.querySelectorAll('.scraped-example-list > .scraped-example');
onEach(firstExamples, updateScrapedExample);
onEach(document.querySelectorAll('.more-examples-toggle'), function(toggle) {
// Allow users to click the left border of the <details> section to close it,
// since the section can be large and finding the [+] button is annoying.
toggle.querySelector('.toggle-line').addEventListener('click', function() {
toggle.open = false;
});
var moreExamples = toggle.querySelectorAll('.scraped-example');
toggle.querySelector('summary').addEventListener('click', function() {
// Wrapping in setTimeout ensures the update happens after the elements are actually
// visible. This is necessary since updateScrapedExample calls scrollToLoc which
// depends on offsetHeight, a property that requires an element to be visible to
// compute correctly.
setTimeout(function() { onEach(moreExamples, updateScrapedExample); });
}, {once: true});
});
})();

View file

@ -35,6 +35,10 @@ crate static SETTINGS_JS: &str = include_str!("static/js/settings.js");
/// Storage, used to store documentation settings.
crate static STORAGE_JS: &str = include_str!("static/js/storage.js");
/// The file contents of `scraped-examples.js`, which contains functionality related to the
/// --scrape-examples flag that inserts automatically-found examples of usages of items.
crate static SCRAPE_EXAMPLES_JS: &str = include_str!("static/js/scrape-examples.js");
/// The file contents of `brush.svg`, the icon used for the theme-switch button.
crate static BRUSH_SVG: &[u8] = include_bytes!("static/images/brush.svg");

View file

@ -109,6 +109,9 @@
data-search-js="{{static_root_path | safe}}search{{page.resource_suffix}}.js"> {#- -#}
</div>
<script src="{{static_root_path | safe}}main{{page.resource_suffix}}.js"></script> {#- -#}
{%- if layout.scrape_examples_extension -%}
<script src="{{static_root_path | safe}}scrape-examples{{page.resource_suffix}}.js"></script> {#- -#}
{%- endif -%}
{%- for script in page.static_extra_scripts -%}
<script src="{{static_root_path | safe}}{{script}}.js"></script> {#- -#}
{% endfor %}