Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept additional user-defined classes in fenced code blocks #79454

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
poliorcetics marked this conversation as resolved.
Show resolved Hide resolved

/// 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;
Comment on lines +886 to +887
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, what does this do? I would expect class: to give an error (or at least a warning).

} 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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: "check for custom code classes while not in nightly",
description: "check for custom code classes without a feature-gate enabled",

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, since it has to be a specific feature gate:

Suggested change
description: "check for custom code classes while not in nightly",
description: "check for custom code classes without the feature-gate enabled",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm suddenly wondering if the negative here is correct. Maybe it should be with the feature-gate enabled ? I don't remember in which context this message pops up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't pop up anywhere except -Z time-passes I think. Since this pass is explicitly only emitting lints and not doing anything else, I think @camelid's suggestion is fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@poliorcetics this is still waiting on you to accept the change I think.

};

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",
)
poliorcetics marked this conversation as resolved.
Show resolved Hide resolved
.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