Skip to content

Commit

Permalink
Accept additional custom classes in fenced code blocks
Browse files Browse the repository at this point in the history
This allow users to write the following:

```
/// Some code block with `rust,class:test:name` as the language string:
///
/// ```rust,class:test:name
/// int main(void) {
///     return 0;
/// }
/// ```
fn main() {}
```

This block will be rendered without any highlighting and will have a class
`test:name` in the resulting CSS.
  • Loading branch information
poliorcetics committed Mar 13, 2021
1 parent 32dce35 commit 3326af2
Show file tree
Hide file tree
Showing 14 changed files with 230 additions and 29 deletions.
5 changes: 4 additions & 1 deletion compiler/rustc_feature/src/active.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ declare_features! (
/// macros disappear).
(active, allow_internal_unsafe, "1.0.0", None, None),

/// no-tracking-issue-end
// no-tracking-issue-end

/// Allows using `#[link_name="llvm.*"]`.
(active, link_llvm_intrinsics, "1.0.0", Some(29602), None),
Expand Down Expand Up @@ -644,6 +644,9 @@ declare_features! (
/// Allows `extern "C-unwind" fn` to enable unwinding across ABI boundaries.
(active, c_unwind, "1.52.0", Some(74990), None),

/// Allows users to provide classes for fenced code block using `class:classname`.
(active, custom_code_classes_in_docs, "1.52.0", Some(79483), None),

// -------------------------------------------------------------------------
// feature-group-end: actual feature gates
// -------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_mir/src/borrow_check/region_infer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1241,7 +1241,7 @@ impl<'tcx> RegionInferenceContext<'tcx> {
/// it. However, it works pretty well in practice. In particular,
/// this is needed to deal with projection outlives bounds like
///
/// ```ignore (internal compiler representation so lifetime syntax is invalid)
/// ```text (internal compiler representation so lifetime syntax is invalid)
/// <T as Foo<'0>>::Item: '1
/// ```
///
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ symbols! {
cttz,
cttz_nonzero,
custom_attribute,
custom_code_classes_in_docs,
custom_derive,
custom_inner_attributes,
custom_test_frameworks,
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_trait_selection/src/opaque_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub struct OpaqueTypeDecl<'tcx> {
/// type Foo = impl Baz;
/// fn bar() -> Foo {
/// // ^^^ This is the span we are looking for!
/// }
/// ```
///
/// In cases where the fn returns `(impl Trait, impl Trait)` or
Expand Down
14 changes: 14 additions & 0 deletions src/doc/rustdoc/src/unstable-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ future.
Attempting to use these error numbers on stable will result in the code sample being interpreted as
plain text.

### Custom CSS classes for code blocks

```rust
#![feature(custom_code_classes_in_docs)]

/// ```class:language-c
/// int main(void) { return 0; }
/// ```
fn main() {}
```

The text `int main(void) { return 0; }` is rendered without highlighting in a code block with the class
`language-c`. This can be used to highlight other languages through JavaScript libraries for example.

## Extensions to the `#[doc]` attribute

These features operate by extending the `#[doc]` attribute, and thus can be caught by the compiler
Expand Down
34 changes: 29 additions & 5 deletions src/librustdoc/html/highlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ crate fn render_with_highlighting(
edition: Edition,
) {
debug!("highlighting: ================\n{}\n==============", src);

write_tooltip(out, tooltip);
write_header(out, class);
write_highlighted_code(out, src, edition);
write_footer(out, playground_button);
}

/// Does not highlight `src` because additional classes have been provided with
/// the `class:` modifier.
crate fn render_with_added_classes(
src: &str,
out: &mut Buffer,
classes: String,
tooltip: Option<(Option<Edition>, &str)>,
) {
debug!("*not* highlighting: ================\n{}\n==============", src);

write_tooltip(out, tooltip);
write_header(out, Some(&classes));
write_raw_code(out, src);
write_footer(out, None);
}

fn write_tooltip(out: &mut Buffer, tooltip: Option<(Option<Edition>, &str)>) {
if let Some((edition_info, class)) = tooltip {
write!(
out,
Expand All @@ -39,17 +63,13 @@ crate fn render_with_highlighting(
},
);
}

write_header(out, class);
write_code(out, &src, edition);
write_footer(out, playground_button);
}

fn write_header(out: &mut Buffer, class: Option<&str>) {
write!(out, "<div class=\"example-wrap\"><pre class=\"rust {}\">\n", class.unwrap_or_default());
}

fn write_code(out: &mut Buffer, src: &str, edition: Edition) {
fn write_highlighted_code(out: &mut Buffer, src: &str, edition: Edition) {
// 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).highlight(&mut |highlight| {
Expand All @@ -65,6 +85,10 @@ fn write_footer(out: &mut Buffer, playground_button: Option<&str>) {
write!(out, "</pre>{}</div>\n", playground_button.unwrap_or_default());
}

fn write_raw_code(out: &mut Buffer, src: &str) {
write!(out, "{}", src.replace("\r\n", "\n"));
}

/// How a span of text is classified. Mostly corresponds to token kinds.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Class {
Expand Down
9 changes: 6 additions & 3 deletions src/librustdoc/html/highlight/tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::write_code;
use super::write_highlighted_code;

use crate::html::format::Buffer;

use expect_test::expect_file;
use rustc_span::edition::Edition;

Expand All @@ -20,7 +22,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);
write_highlighted_code(&mut out, src, Edition::Edition2018);
format!("{}<pre><code>{}</code></pre>\n", STYLE, out.into_inner())
};
expect_file!["fixtures/sample.html"].assert_eq(&html);
Expand All @@ -31,7 +33,8 @@ fn test_dos_backline() {
let src = "pub fn foo() {\r\n\
println!(\"foo\");\r\n\
}\r\n";

let mut html = Buffer::new();
write_code(&mut html, src, Edition::Edition2018);
write_highlighted_code(&mut html, src, Edition::Edition2018);
expect_file!["fixtures/dos_line.html"].assert_eq(&html.into_inner());
}
59 changes: 40 additions & 19 deletions src/librustdoc/html/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
let should_panic;
let ignore;
let edition;
let added_classes;
if let Some(Event::Start(Tag::CodeBlock(kind))) = event {
let parse_result = match kind {
CodeBlockKind::Fenced(ref lang) => {
Expand All @@ -221,6 +222,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
should_panic = parse_result.should_panic;
ignore = parse_result.ignore;
edition = parse_result.edition;
added_classes = parse_result.added_classes;
} else {
return event;
}
Expand Down Expand Up @@ -289,33 +291,42 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
))
});

let tooltip = if ignore != Ignore::None {
Some((None, "ignore"))
let (tooltip, special_class) = if ignore != Ignore::None {
(Some((None, "ignore")), " ignore")
} else if compile_fail {
Some((None, "compile_fail"))
(Some((None, "compile_fail")), " compile_fail")
} else if should_panic {
Some((None, "should_panic"))
(Some((None, "should_panic")), " should_panic")
} else if explicit_edition {
Some((Some(edition), "edition"))
(Some((Some(edition), "edition")), " edition")
} else {
None
(None, "")
};

// insert newline to clearly separate it from the
// previous block so we can shorten the html output
let mut s = Buffer::new();
s.push_str("\n");
highlight::render_with_highlighting(
&text,
&mut s,
Some(&format!(
"rust-example-rendered{}",
if let Some((_, class)) = tooltip { format!(" {}", class) } else { String::new() }
)),
playground_button.as_deref(),
tooltip,
edition,
);

if added_classes.is_empty() {
highlight::render_with_highlighting(
&text,
&mut s,
Some(&format!("rust-example-rendered{}", special_class)),
playground_button.as_deref(),
tooltip,
edition,
);
} else {
let classes = if special_class.is_empty() {
added_classes.join(" ")
} else {
format!("{} {}", special_class.trim(), added_classes.join(" "))
};

highlight::render_with_added_classes(&text, &mut s, classes, tooltip);
}

Some(Event::Html(s.into_inner().into()))
}
}
Expand Down Expand Up @@ -744,6 +755,7 @@ crate struct LangString {
crate error_codes: Vec<String>,
crate allow_fail: bool,
crate edition: Option<Edition>,
crate added_classes: Vec<String>,
}

#[derive(Eq, PartialEq, Clone, Debug)]
Expand All @@ -766,6 +778,7 @@ impl Default for LangString {
error_codes: Vec::new(),
allow_fail: false,
edition: None,
added_classes: Vec::new(),
}
}
}
Expand All @@ -775,7 +788,7 @@ impl LangString {
string: &str,
allow_error_code_check: ErrorCodes,
enable_per_target_ignores: bool,
) -> LangString {
) -> Self {
Self::parse(string, allow_error_code_check, enable_per_target_ignores, None)
}

Expand Down Expand Up @@ -809,7 +822,7 @@ impl LangString {
allow_error_code_check: ErrorCodes,
enable_per_target_ignores: bool,
extra: Option<&ExtraInfo<'_>>,
) -> LangString {
) -> Self {
let allow_error_code_check = allow_error_code_check.as_bool();
let mut seen_rust_tags = false;
let mut seen_other_tags = false;
Expand Down Expand Up @@ -868,6 +881,14 @@ impl LangString {
seen_other_tags = true;
}
}
x if x.starts_with("class:") => {
let class = x.trim_start_matches("class:");
if class.is_empty() {
seen_other_tags = true;
} else {
data.added_classes.push(class.to_owned());
}
}
x if extra.is_some() => {
let s = x.to_lowercase();
match if s == "compile-fail" || s == "compile_fail" || s == "compilefail" {
Expand Down
20 changes: 20 additions & 0 deletions src/librustdoc/html/markdown/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ fn test_lang_string_parse() {
edition: Some(Edition::Edition2018),
..Default::default()
});
t(LangString {
original: "class:test".into(),
added_classes: vec!["test".into()],
..Default::default()
});
t(LangString {
original: "rust,class:test".into(),
added_classes: vec!["test".into()],
..Default::default()
});
t(LangString {
original: "class:test:with:colon".into(),
added_classes: vec!["test:with:colon".into()],
..Default::default()
});
t(LangString {
original: "class:first,class:second".into(),
added_classes: vec!["first".into(), "second".into()],
..Default::default()
});
}

#[test]
Expand Down
83 changes: 83 additions & 0 deletions src/librustdoc/passes/check_custom_code_classes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! NIGHTLY & UNSTABLE CHECK: custom_code_classes_in_docs
//!
//! This pass will produce errors when finding custom classes outside of
//! nightly + relevant feature active.
use super::{span_of_attrs, Pass};
use crate::clean::{Crate, Item};
use crate::core::DocContext;
use crate::fold::DocFolder;
use crate::html::markdown::{find_testable_code, ErrorCodes, LangString};

use rustc_session::parse::feature_err;
use rustc_span::symbol::sym;

crate const CHECK_CUSTOM_CODE_CLASSES: Pass = Pass {
name: "check-custom-code-classes",
run: check_custom_code_classes,
description: "check for custom code classes while not in nightly",
};

crate fn check_custom_code_classes(krate: Crate, cx: &mut DocContext<'_>) -> Crate {
let mut coll = CustomCodeClassLinter { cx };

coll.fold_crate(krate)
}

struct CustomCodeClassLinter<'a, 'tcx> {
cx: &'a DocContext<'tcx>,
}

impl<'a, 'tcx> DocFolder for CustomCodeClassLinter<'a, 'tcx> {
fn fold_item(&mut self, item: Item) -> Option<Item> {
let dox = item.attrs.collapsed_doc_value().unwrap_or_default();

look_for_custom_classes(&self.cx, &dox, &item);

Some(self.fold_item_recur(item))
}
}

struct TestsWithCustomClasses {
custom_classes_found: Vec<String>,
}

impl crate::doctest::Tester for TestsWithCustomClasses {
fn add_test(&mut self, _: String, config: LangString, _: usize) {
self.custom_classes_found.extend(config.added_classes.into_iter());
}
}

crate fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, dox: &str, item: &Item) {
let hir_id = match DocContext::as_local_hir_id(cx.tcx, item.def_id) {
Some(hir_id) => hir_id,
None => {
// If non-local, no need to check anything.
return;
}
};

let mut tests = TestsWithCustomClasses { custom_classes_found: vec![] };

find_testable_code(&dox, &mut tests, ErrorCodes::No, false, None);

if !tests.custom_classes_found.is_empty() && !cx.tcx.features().custom_code_classes_in_docs {
debug!("reporting error for {:?} (hid_id={:?})", item, hir_id);
let sp = span_of_attrs(&item.attrs).unwrap_or(item.source.span());
feature_err(
&cx.tcx.sess.parse_sess,
sym::custom_code_classes_in_docs,
sp,
"custom classes in code blocks are unstable",
)
.note(
// This will list the wrong items to make them more easily searchable.
// To ensure the most correct hits, it adds back the 'class:' that was stripped.
&format!(
"found these custom classes: class:{}",
tests.custom_classes_found.join(", class:")
),
)
.emit();
}
}
Loading

0 comments on commit 3326af2

Please sign in to comment.