Skip to content

Commit 3326af2

Browse files
committed
Accept additional custom classes in fenced code blocks
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.
1 parent 32dce35 commit 3326af2

File tree

14 files changed

+230
-29
lines changed

14 files changed

+230
-29
lines changed

compiler/rustc_feature/src/active.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ declare_features! (
126126
/// macros disappear).
127127
(active, allow_internal_unsafe, "1.0.0", None, None),
128128

129-
/// no-tracking-issue-end
129+
// no-tracking-issue-end
130130

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

647+
/// Allows users to provide classes for fenced code block using `class:classname`.
648+
(active, custom_code_classes_in_docs, "1.52.0", Some(79483), None),
649+
647650
// -------------------------------------------------------------------------
648651
// feature-group-end: actual feature gates
649652
// -------------------------------------------------------------------------

compiler/rustc_mir/src/borrow_check/region_infer/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ impl<'tcx> RegionInferenceContext<'tcx> {
12411241
/// it. However, it works pretty well in practice. In particular,
12421242
/// this is needed to deal with projection outlives bounds like
12431243
///
1244-
/// ```ignore (internal compiler representation so lifetime syntax is invalid)
1244+
/// ```text (internal compiler representation so lifetime syntax is invalid)
12451245
/// <T as Foo<'0>>::Item: '1
12461246
/// ```
12471247
///

compiler/rustc_span/src/symbol.rs

+1
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ symbols! {
434434
cttz,
435435
cttz_nonzero,
436436
custom_attribute,
437+
custom_code_classes_in_docs,
437438
custom_derive,
438439
custom_inner_attributes,
439440
custom_test_frameworks,

compiler/rustc_trait_selection/src/opaque_types.rs

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub struct OpaqueTypeDecl<'tcx> {
4646
/// type Foo = impl Baz;
4747
/// fn bar() -> Foo {
4848
/// // ^^^ This is the span we are looking for!
49+
/// }
4950
/// ```
5051
///
5152
/// In cases where the fn returns `(impl Trait, impl Trait)` or

src/doc/rustdoc/src/unstable-features.md

+14
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ future.
3838
Attempting to use these error numbers on stable will result in the code sample being interpreted as
3939
plain text.
4040

41+
### Custom CSS classes for code blocks
42+
43+
```rust
44+
#![feature(custom_code_classes_in_docs)]
45+
46+
/// ```class:language-c
47+
/// int main(void) { return 0; }
48+
/// ```
49+
fn main() {}
50+
```
51+
52+
The text `int main(void) { return 0; }` is rendered without highlighting in a code block with the class
53+
`language-c`. This can be used to highlight other languages through JavaScript libraries for example.
54+
4155
## Extensions to the `#[doc]` attribute
4256

4357
These features operate by extending the `#[doc]` attribute, and thus can be caught by the compiler

src/librustdoc/html/highlight.rs

+29-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ crate fn render_with_highlighting(
2727
edition: Edition,
2828
) {
2929
debug!("highlighting: ================\n{}\n==============", src);
30+
31+
write_tooltip(out, tooltip);
32+
write_header(out, class);
33+
write_highlighted_code(out, src, edition);
34+
write_footer(out, playground_button);
35+
}
36+
37+
/// Does not highlight `src` because additional classes have been provided with
38+
/// the `class:` modifier.
39+
crate fn render_with_added_classes(
40+
src: &str,
41+
out: &mut Buffer,
42+
classes: String,
43+
tooltip: Option<(Option<Edition>, &str)>,
44+
) {
45+
debug!("*not* highlighting: ================\n{}\n==============", src);
46+
47+
write_tooltip(out, tooltip);
48+
write_header(out, Some(&classes));
49+
write_raw_code(out, src);
50+
write_footer(out, None);
51+
}
52+
53+
fn write_tooltip(out: &mut Buffer, tooltip: Option<(Option<Edition>, &str)>) {
3054
if let Some((edition_info, class)) = tooltip {
3155
write!(
3256
out,
@@ -39,17 +63,13 @@ crate fn render_with_highlighting(
3963
},
4064
);
4165
}
42-
43-
write_header(out, class);
44-
write_code(out, &src, edition);
45-
write_footer(out, playground_button);
4666
}
4767

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

52-
fn write_code(out: &mut Buffer, src: &str, edition: Edition) {
72+
fn write_highlighted_code(out: &mut Buffer, src: &str, edition: Edition) {
5373
// This replace allows to fix how the code source with DOS backline characters is displayed.
5474
let src = src.replace("\r\n", "\n");
5575
Classifier::new(&src, edition).highlight(&mut |highlight| {
@@ -65,6 +85,10 @@ fn write_footer(out: &mut Buffer, playground_button: Option<&str>) {
6585
write!(out, "</pre>{}</div>\n", playground_button.unwrap_or_default());
6686
}
6787

88+
fn write_raw_code(out: &mut Buffer, src: &str) {
89+
write!(out, "{}", src.replace("\r\n", "\n"));
90+
}
91+
6892
/// How a span of text is classified. Mostly corresponds to token kinds.
6993
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7094
enum Class {

src/librustdoc/html/highlight/tests.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use super::write_code;
1+
use super::write_highlighted_code;
2+
23
use crate::html::format::Buffer;
4+
35
use expect_test::expect_file;
46
use rustc_span::edition::Edition;
57

@@ -20,7 +22,7 @@ fn test_html_highlighting() {
2022
let src = include_str!("fixtures/sample.rs");
2123
let html = {
2224
let mut out = Buffer::new();
23-
write_code(&mut out, src, Edition::Edition2018);
25+
write_highlighted_code(&mut out, src, Edition::Edition2018);
2426
format!("{}<pre><code>{}</code></pre>\n", STYLE, out.into_inner())
2527
};
2628
expect_file!["fixtures/sample.html"].assert_eq(&html);
@@ -31,7 +33,8 @@ fn test_dos_backline() {
3133
let src = "pub fn foo() {\r\n\
3234
println!(\"foo\");\r\n\
3335
}\r\n";
36+
3437
let mut html = Buffer::new();
35-
write_code(&mut html, src, Edition::Edition2018);
38+
write_highlighted_code(&mut html, src, Edition::Edition2018);
3639
expect_file!["fixtures/dos_line.html"].assert_eq(&html.into_inner());
3740
}

src/librustdoc/html/markdown.rs

+40-19
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
207207
let should_panic;
208208
let ignore;
209209
let edition;
210+
let added_classes;
210211
if let Some(Event::Start(Tag::CodeBlock(kind))) = event {
211212
let parse_result = match kind {
212213
CodeBlockKind::Fenced(ref lang) => {
@@ -221,6 +222,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
221222
should_panic = parse_result.should_panic;
222223
ignore = parse_result.ignore;
223224
edition = parse_result.edition;
225+
added_classes = parse_result.added_classes;
224226
} else {
225227
return event;
226228
}
@@ -289,33 +291,42 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
289291
))
290292
});
291293

292-
let tooltip = if ignore != Ignore::None {
293-
Some((None, "ignore"))
294+
let (tooltip, special_class) = if ignore != Ignore::None {
295+
(Some((None, "ignore")), " ignore")
294296
} else if compile_fail {
295-
Some((None, "compile_fail"))
297+
(Some((None, "compile_fail")), " compile_fail")
296298
} else if should_panic {
297-
Some((None, "should_panic"))
299+
(Some((None, "should_panic")), " should_panic")
298300
} else if explicit_edition {
299-
Some((Some(edition), "edition"))
301+
(Some((Some(edition), "edition")), " edition")
300302
} else {
301-
None
303+
(None, "")
302304
};
303305

304306
// insert newline to clearly separate it from the
305307
// previous block so we can shorten the html output
306308
let mut s = Buffer::new();
307309
s.push_str("\n");
308-
highlight::render_with_highlighting(
309-
&text,
310-
&mut s,
311-
Some(&format!(
312-
"rust-example-rendered{}",
313-
if let Some((_, class)) = tooltip { format!(" {}", class) } else { String::new() }
314-
)),
315-
playground_button.as_deref(),
316-
tooltip,
317-
edition,
318-
);
310+
311+
if added_classes.is_empty() {
312+
highlight::render_with_highlighting(
313+
&text,
314+
&mut s,
315+
Some(&format!("rust-example-rendered{}", special_class)),
316+
playground_button.as_deref(),
317+
tooltip,
318+
edition,
319+
);
320+
} else {
321+
let classes = if special_class.is_empty() {
322+
added_classes.join(" ")
323+
} else {
324+
format!("{} {}", special_class.trim(), added_classes.join(" "))
325+
};
326+
327+
highlight::render_with_added_classes(&text, &mut s, classes, tooltip);
328+
}
329+
319330
Some(Event::Html(s.into_inner().into()))
320331
}
321332
}
@@ -744,6 +755,7 @@ crate struct LangString {
744755
crate error_codes: Vec<String>,
745756
crate allow_fail: bool,
746757
crate edition: Option<Edition>,
758+
crate added_classes: Vec<String>,
747759
}
748760

749761
#[derive(Eq, PartialEq, Clone, Debug)]
@@ -766,6 +778,7 @@ impl Default for LangString {
766778
error_codes: Vec::new(),
767779
allow_fail: false,
768780
edition: None,
781+
added_classes: Vec::new(),
769782
}
770783
}
771784
}
@@ -775,7 +788,7 @@ impl LangString {
775788
string: &str,
776789
allow_error_code_check: ErrorCodes,
777790
enable_per_target_ignores: bool,
778-
) -> LangString {
791+
) -> Self {
779792
Self::parse(string, allow_error_code_check, enable_per_target_ignores, None)
780793
}
781794

@@ -809,7 +822,7 @@ impl LangString {
809822
allow_error_code_check: ErrorCodes,
810823
enable_per_target_ignores: bool,
811824
extra: Option<&ExtraInfo<'_>>,
812-
) -> LangString {
825+
) -> Self {
813826
let allow_error_code_check = allow_error_code_check.as_bool();
814827
let mut seen_rust_tags = false;
815828
let mut seen_other_tags = false;
@@ -868,6 +881,14 @@ impl LangString {
868881
seen_other_tags = true;
869882
}
870883
}
884+
x if x.starts_with("class:") => {
885+
let class = x.trim_start_matches("class:");
886+
if class.is_empty() {
887+
seen_other_tags = true;
888+
} else {
889+
data.added_classes.push(class.to_owned());
890+
}
891+
}
871892
x if extra.is_some() => {
872893
let s = x.to_lowercase();
873894
match if s == "compile-fail" || s == "compile_fail" || s == "compilefail" {

src/librustdoc/html/markdown/tests.rs

+20
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ fn test_lang_string_parse() {
118118
edition: Some(Edition::Edition2018),
119119
..Default::default()
120120
});
121+
t(LangString {
122+
original: "class:test".into(),
123+
added_classes: vec!["test".into()],
124+
..Default::default()
125+
});
126+
t(LangString {
127+
original: "rust,class:test".into(),
128+
added_classes: vec!["test".into()],
129+
..Default::default()
130+
});
131+
t(LangString {
132+
original: "class:test:with:colon".into(),
133+
added_classes: vec!["test:with:colon".into()],
134+
..Default::default()
135+
});
136+
t(LangString {
137+
original: "class:first,class:second".into(),
138+
added_classes: vec!["first".into(), "second".into()],
139+
..Default::default()
140+
});
121141
}
122142

123143
#[test]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! NIGHTLY & UNSTABLE CHECK: custom_code_classes_in_docs
2+
//!
3+
//! This pass will produce errors when finding custom classes outside of
4+
//! nightly + relevant feature active.
5+
6+
use super::{span_of_attrs, Pass};
7+
use crate::clean::{Crate, Item};
8+
use crate::core::DocContext;
9+
use crate::fold::DocFolder;
10+
use crate::html::markdown::{find_testable_code, ErrorCodes, LangString};
11+
12+
use rustc_session::parse::feature_err;
13+
use rustc_span::symbol::sym;
14+
15+
crate const CHECK_CUSTOM_CODE_CLASSES: Pass = Pass {
16+
name: "check-custom-code-classes",
17+
run: check_custom_code_classes,
18+
description: "check for custom code classes while not in nightly",
19+
};
20+
21+
crate fn check_custom_code_classes(krate: Crate, cx: &mut DocContext<'_>) -> Crate {
22+
let mut coll = CustomCodeClassLinter { cx };
23+
24+
coll.fold_crate(krate)
25+
}
26+
27+
struct CustomCodeClassLinter<'a, 'tcx> {
28+
cx: &'a DocContext<'tcx>,
29+
}
30+
31+
impl<'a, 'tcx> DocFolder for CustomCodeClassLinter<'a, 'tcx> {
32+
fn fold_item(&mut self, item: Item) -> Option<Item> {
33+
let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
34+
35+
look_for_custom_classes(&self.cx, &dox, &item);
36+
37+
Some(self.fold_item_recur(item))
38+
}
39+
}
40+
41+
struct TestsWithCustomClasses {
42+
custom_classes_found: Vec<String>,
43+
}
44+
45+
impl crate::doctest::Tester for TestsWithCustomClasses {
46+
fn add_test(&mut self, _: String, config: LangString, _: usize) {
47+
self.custom_classes_found.extend(config.added_classes.into_iter());
48+
}
49+
}
50+
51+
crate fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, dox: &str, item: &Item) {
52+
let hir_id = match DocContext::as_local_hir_id(cx.tcx, item.def_id) {
53+
Some(hir_id) => hir_id,
54+
None => {
55+
// If non-local, no need to check anything.
56+
return;
57+
}
58+
};
59+
60+
let mut tests = TestsWithCustomClasses { custom_classes_found: vec![] };
61+
62+
find_testable_code(&dox, &mut tests, ErrorCodes::No, false, None);
63+
64+
if !tests.custom_classes_found.is_empty() && !cx.tcx.features().custom_code_classes_in_docs {
65+
debug!("reporting error for {:?} (hid_id={:?})", item, hir_id);
66+
let sp = span_of_attrs(&item.attrs).unwrap_or(item.source.span());
67+
feature_err(
68+
&cx.tcx.sess.parse_sess,
69+
sym::custom_code_classes_in_docs,
70+
sp,
71+
"custom classes in code blocks are unstable",
72+
)
73+
.note(
74+
// This will list the wrong items to make them more easily searchable.
75+
// To ensure the most correct hits, it adds back the 'class:' that was stripped.
76+
&format!(
77+
"found these custom classes: class:{}",
78+
tests.custom_classes_found.join(", class:")
79+
),
80+
)
81+
.emit();
82+
}
83+
}

0 commit comments

Comments
 (0)