Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6dab6dc

Browse files
committedJul 5, 2023
Auto merge of #112697 - tgross35:explain-markdown, r=oli-obk
Add simple markdown formatting to `rustc --explain` output This is a second attempt at #104540, which is #63128 without dependencies. This PR adds basic markdown formatting to `rustc --explain` output when available. Currently, the output just displays raw markdown: this works of course, but it really doesn't look very elegant. (output is `rustc --explain E0038`) <img width="583" alt="image" src="https://github.com/rust-lang/rust/assets/13724985/ea418117-47af-455b-83c0-6fc59276efee"> After this patch, sample output from the same file: <img width="693" alt="image" src="https://github.com/rust-lang/rust/assets/13724985/12f7bf9b-a3fe-4104-b74b-c3e5227f3de9"> This also obeys the `--color always/auto/never` command option. Behavior: - If pager is available and supports color, print with formatting to the pager - If pager is not available or fails print with formatting to stdout - otherwise without formatting - Follow `--color always/never` if suppied - If everything fails, just print plain text to stdout r? `@oli-obk` cc `@estebank` (since the two of you were involved in the previous discussion)
2 parents 9227ff2 + 6a1c10b commit 6dab6dc

File tree

14 files changed

+1406
-17
lines changed

14 files changed

+1406
-17
lines changed
 

‎compiler/rustc_driver_impl/src/lib.rs‎

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use rustc_data_structures::profiling::{
2424
};
2525
use rustc_data_structures::sync::SeqCst;
2626
use rustc_errors::registry::{InvalidErrorCode, Registry};
27+
use rustc_errors::{markdown, ColorConfig};
2728
use rustc_errors::{
2829
DiagnosticMessage, ErrorGuaranteed, Handler, PResult, SubdiagnosticMessage, TerminalUrl,
2930
};
@@ -282,7 +283,7 @@ fn run_compiler(
282283
interface::set_thread_safe_mode(&sopts.unstable_opts);
283284

284285
if let Some(ref code) = matches.opt_str("explain") {
285-
handle_explain(&early_error_handler, diagnostics_registry(), code);
286+
handle_explain(&early_error_handler, diagnostics_registry(), code, sopts.color);
286287
return Ok(());
287288
}
288289

@@ -540,7 +541,7 @@ impl Compilation {
540541
}
541542
}
542543

543-
fn handle_explain(handler: &EarlyErrorHandler, registry: Registry, code: &str) {
544+
fn handle_explain(handler: &EarlyErrorHandler, registry: Registry, code: &str, color: ColorConfig) {
544545
let upper_cased_code = code.to_ascii_uppercase();
545546
let normalised =
546547
if upper_cased_code.starts_with('E') { upper_cased_code } else { format!("E{code:0>4}") };
@@ -564,7 +565,7 @@ fn handle_explain(handler: &EarlyErrorHandler, registry: Registry, code: &str) {
564565
text.push('\n');
565566
}
566567
if io::stdout().is_terminal() {
567-
show_content_with_pager(&text);
568+
show_md_content_with_pager(&text, color);
568569
} else {
569570
safe_print!("{text}");
570571
}
@@ -575,34 +576,72 @@ fn handle_explain(handler: &EarlyErrorHandler, registry: Registry, code: &str) {
575576
}
576577
}
577578

578-
fn show_content_with_pager(content: &str) {
579+
/// If color is always or auto, print formatted & colorized markdown. If color is never or
580+
/// if formatted printing fails, print the raw text.
581+
///
582+
/// Prefers a pager, falls back standard print
583+
fn show_md_content_with_pager(content: &str, color: ColorConfig) {
584+
let mut fallback_to_println = false;
579585
let pager_name = env::var_os("PAGER").unwrap_or_else(|| {
580586
if cfg!(windows) { OsString::from("more.com") } else { OsString::from("less") }
581587
});
582588

583-
let mut fallback_to_println = false;
589+
let mut cmd = Command::new(&pager_name);
590+
// FIXME: find if other pagers accept color options
591+
let mut print_formatted = if pager_name == "less" {
592+
cmd.arg("-r");
593+
true
594+
} else if ["bat", "catbat", "delta"].iter().any(|v| *v == pager_name) {
595+
true
596+
} else {
597+
false
598+
};
584599

585-
match Command::new(pager_name).stdin(Stdio::piped()).spawn() {
586-
Ok(mut pager) => {
587-
if let Some(pipe) = pager.stdin.as_mut() {
588-
if pipe.write_all(content.as_bytes()).is_err() {
589-
fallback_to_println = true;
590-
}
591-
}
600+
if color == ColorConfig::Never {
601+
print_formatted = false;
602+
} else if color == ColorConfig::Always {
603+
print_formatted = true;
604+
}
605+
606+
let mdstream = markdown::MdStream::parse_str(content);
607+
let bufwtr = markdown::create_stdout_bufwtr();
608+
let mut mdbuf = bufwtr.buffer();
609+
if mdstream.write_termcolor_buf(&mut mdbuf).is_err() {
610+
print_formatted = false;
611+
}
592612

593-
if pager.wait().is_err() {
613+
if let Ok(mut pager) = cmd.stdin(Stdio::piped()).spawn() {
614+
if let Some(pipe) = pager.stdin.as_mut() {
615+
let res = if print_formatted {
616+
pipe.write_all(mdbuf.as_slice())
617+
} else {
618+
pipe.write_all(content.as_bytes())
619+
};
620+
621+
if res.is_err() {
594622
fallback_to_println = true;
595623
}
596624
}
597-
Err(_) => {
625+
626+
if pager.wait().is_err() {
598627
fallback_to_println = true;
599628
}
629+
} else {
630+
fallback_to_println = true;
600631
}
601632

602633
// If pager fails for whatever reason, we should still print the content
603634
// to standard output
604635
if fallback_to_println {
605-
safe_print!("{content}");
636+
let fmt_success = match color {
637+
ColorConfig::Auto => io::stdout().is_terminal() && bufwtr.print(&mdbuf).is_ok(),
638+
ColorConfig::Always => bufwtr.print(&mdbuf).is_ok(),
639+
ColorConfig::Never => false,
640+
};
641+
642+
if !fmt_success {
643+
safe_print!("{content}");
644+
}
606645
}
607646
}
608647

‎compiler/rustc_errors/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ rustc_hir = { path = "../rustc_hir" }
2020
rustc_lint_defs = { path = "../rustc_lint_defs" }
2121
rustc_type_ir = { path = "../rustc_type_ir" }
2222
unicode-width = "0.1.4"
23-
termcolor = "1.0"
23+
termcolor = "1.2.0"
2424
annotate-snippets = "0.9"
2525
termize = "0.1.1"
2626
serde = { version = "1.0.125", features = [ "derive" ] }

‎compiler/rustc_errors/src/emitter.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ pub enum ColorConfig {
616616
}
617617

618618
impl ColorConfig {
619-
fn to_color_choice(self) -> ColorChoice {
619+
pub fn to_color_choice(self) -> ColorChoice {
620620
match self {
621621
ColorConfig::Always => {
622622
if io::stderr().is_terminal() {

‎compiler/rustc_errors/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub mod emitter;
6161
pub mod error;
6262
pub mod json;
6363
mod lock;
64+
pub mod markdown;
6465
pub mod registry;
6566
mod snippet;
6667
mod styled_buffer;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! A simple markdown parser that can write formatted text to the terminal
2+
//!
3+
//! Entrypoint is `MdStream::parse_str(...)`
4+
use std::io;
5+
6+
use termcolor::{Buffer, BufferWriter, ColorChoice};
7+
mod parse;
8+
mod term;
9+
10+
/// An AST representation of a Markdown document
11+
#[derive(Clone, Debug, Default, PartialEq)]
12+
pub struct MdStream<'a>(Vec<MdTree<'a>>);
13+
14+
impl<'a> MdStream<'a> {
15+
/// Parse a markdown string to a tokenstream
16+
#[must_use]
17+
pub fn parse_str(s: &str) -> MdStream<'_> {
18+
parse::entrypoint(s)
19+
}
20+
21+
/// Write formatted output to a termcolor buffer
22+
pub fn write_termcolor_buf(&self, buf: &mut Buffer) -> io::Result<()> {
23+
term::entrypoint(self, buf)
24+
}
25+
}
26+
27+
/// Create a termcolor buffer with the `Always` color choice
28+
pub fn create_stdout_bufwtr() -> BufferWriter {
29+
BufferWriter::stdout(ColorChoice::Always)
30+
}
31+
32+
/// A single tokentree within a Markdown document
33+
#[derive(Clone, Debug, PartialEq)]
34+
pub enum MdTree<'a> {
35+
/// Leaf types
36+
Comment(&'a str),
37+
CodeBlock {
38+
txt: &'a str,
39+
lang: Option<&'a str>,
40+
},
41+
CodeInline(&'a str),
42+
Strong(&'a str),
43+
Emphasis(&'a str),
44+
Strikethrough(&'a str),
45+
PlainText(&'a str),
46+
/// [Foo](www.foo.com) or simple anchor <www.foo.com>
47+
Link {
48+
disp: &'a str,
49+
link: &'a str,
50+
},
51+
/// `[Foo link][ref]`
52+
RefLink {
53+
disp: &'a str,
54+
id: Option<&'a str>,
55+
},
56+
/// [ref]: www.foo.com
57+
LinkDef {
58+
id: &'a str,
59+
link: &'a str,
60+
},
61+
/// Break bewtween two paragraphs (double `\n`), not directly parsed but
62+
/// added later
63+
ParagraphBreak,
64+
/// Break bewtween two lines (single `\n`)
65+
LineBreak,
66+
HorizontalRule,
67+
Heading(u8, MdStream<'a>),
68+
OrderedListItem(u16, MdStream<'a>),
69+
UnorderedListItem(MdStream<'a>),
70+
}
71+
72+
impl<'a> From<Vec<MdTree<'a>>> for MdStream<'a> {
73+
fn from(value: Vec<MdTree<'a>>) -> Self {
74+
Self(value)
75+
}
76+
}

‎compiler/rustc_errors/src/markdown/parse.rs‎

Lines changed: 588 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use std::cell::Cell;
2+
use std::io::{self, Write};
3+
4+
use termcolor::{Buffer, Color, ColorSpec, WriteColor};
5+
6+
use crate::markdown::{MdStream, MdTree};
7+
8+
const DEFAULT_COLUMN_WIDTH: usize = 140;
9+
10+
thread_local! {
11+
/// Track the position of viewable characters in our buffer
12+
static CURSOR: Cell<usize> = Cell::new(0);
13+
/// Width of the terminal
14+
static WIDTH: Cell<usize> = Cell::new(DEFAULT_COLUMN_WIDTH);
15+
}
16+
17+
/// Print to terminal output to a buffer
18+
pub fn entrypoint(stream: &MdStream<'_>, buf: &mut Buffer) -> io::Result<()> {
19+
#[cfg(not(test))]
20+
if let Some((w, _)) = termize::dimensions() {
21+
WIDTH.with(|c| c.set(std::cmp::min(w, DEFAULT_COLUMN_WIDTH)));
22+
}
23+
write_stream(stream, buf, None, 0)?;
24+
buf.write_all(b"\n")
25+
}
26+
27+
/// Write the buffer, reset to the default style after each
28+
fn write_stream(
29+
MdStream(stream): &MdStream<'_>,
30+
buf: &mut Buffer,
31+
default: Option<&ColorSpec>,
32+
indent: usize,
33+
) -> io::Result<()> {
34+
match default {
35+
Some(c) => buf.set_color(c)?,
36+
None => buf.reset()?,
37+
}
38+
39+
for tt in stream {
40+
write_tt(tt, buf, indent)?;
41+
if let Some(c) = default {
42+
buf.set_color(c)?;
43+
}
44+
}
45+
46+
buf.reset()?;
47+
Ok(())
48+
}
49+
50+
pub fn write_tt(tt: &MdTree<'_>, buf: &mut Buffer, indent: usize) -> io::Result<()> {
51+
match tt {
52+
MdTree::CodeBlock { txt, lang: _ } => {
53+
buf.set_color(ColorSpec::new().set_dimmed(true))?;
54+
buf.write_all(txt.as_bytes())?;
55+
}
56+
MdTree::CodeInline(txt) => {
57+
buf.set_color(ColorSpec::new().set_dimmed(true))?;
58+
write_wrapping(buf, txt, indent, None)?;
59+
}
60+
MdTree::Strong(txt) => {
61+
buf.set_color(ColorSpec::new().set_bold(true))?;
62+
write_wrapping(buf, txt, indent, None)?;
63+
}
64+
MdTree::Emphasis(txt) => {
65+
buf.set_color(ColorSpec::new().set_italic(true))?;
66+
write_wrapping(buf, txt, indent, None)?;
67+
}
68+
MdTree::Strikethrough(txt) => {
69+
buf.set_color(ColorSpec::new().set_strikethrough(true))?;
70+
write_wrapping(buf, txt, indent, None)?;
71+
}
72+
MdTree::PlainText(txt) => {
73+
write_wrapping(buf, txt, indent, None)?;
74+
}
75+
MdTree::Link { disp, link } => {
76+
write_wrapping(buf, disp, indent, Some(link))?;
77+
}
78+
MdTree::ParagraphBreak => {
79+
buf.write_all(b"\n\n")?;
80+
reset_cursor();
81+
}
82+
MdTree::LineBreak => {
83+
buf.write_all(b"\n")?;
84+
reset_cursor();
85+
}
86+
MdTree::HorizontalRule => {
87+
(0..WIDTH.with(Cell::get)).for_each(|_| buf.write_all(b"-").unwrap());
88+
reset_cursor();
89+
}
90+
MdTree::Heading(n, stream) => {
91+
let mut cs = ColorSpec::new();
92+
cs.set_fg(Some(Color::Cyan));
93+
match n {
94+
1 => cs.set_intense(true).set_bold(true).set_underline(true),
95+
2 => cs.set_intense(true).set_underline(true),
96+
3 => cs.set_intense(true).set_italic(true),
97+
4.. => cs.set_underline(true).set_italic(true),
98+
0 => unreachable!(),
99+
};
100+
write_stream(stream, buf, Some(&cs), 0)?;
101+
buf.write_all(b"\n")?;
102+
}
103+
MdTree::OrderedListItem(n, stream) => {
104+
let base = format!("{n}. ");
105+
write_wrapping(buf, &format!("{base:<4}"), indent, None)?;
106+
write_stream(stream, buf, None, indent + 4)?;
107+
}
108+
MdTree::UnorderedListItem(stream) => {
109+
let base = "* ";
110+
write_wrapping(buf, &format!("{base:<4}"), indent, None)?;
111+
write_stream(stream, buf, None, indent + 4)?;
112+
}
113+
// Patterns popped in previous step
114+
MdTree::Comment(_) | MdTree::LinkDef { .. } | MdTree::RefLink { .. } => unreachable!(),
115+
}
116+
117+
buf.reset()?;
118+
119+
Ok(())
120+
}
121+
122+
/// End of that block, just wrap the line
123+
fn reset_cursor() {
124+
CURSOR.with(|cur| cur.set(0));
125+
}
126+
127+
/// Change to be generic on Write for testing. If we have a link URL, we don't
128+
/// count the extra tokens to make it clickable.
129+
fn write_wrapping<B: io::Write>(
130+
buf: &mut B,
131+
text: &str,
132+
indent: usize,
133+
link_url: Option<&str>,
134+
) -> io::Result<()> {
135+
let ind_ws = &b" "[..indent];
136+
let mut to_write = text;
137+
if let Some(url) = link_url {
138+
// This is a nonprinting prefix so we don't increment our cursor
139+
write!(buf, "\x1b]8;;{url}\x1b\\")?;
140+
}
141+
CURSOR.with(|cur| {
142+
loop {
143+
if cur.get() == 0 {
144+
buf.write_all(ind_ws)?;
145+
cur.set(indent);
146+
}
147+
let ch_count = WIDTH.with(Cell::get) - cur.get();
148+
let mut iter = to_write.char_indices();
149+
let Some((end_idx, _ch)) = iter.nth(ch_count) else {
150+
// Write entire line
151+
buf.write_all(to_write.as_bytes())?;
152+
cur.set(cur.get()+to_write.chars().count());
153+
break;
154+
};
155+
156+
if let Some((break_idx, ch)) = to_write[..end_idx]
157+
.char_indices()
158+
.rev()
159+
.find(|(_idx, ch)| ch.is_whitespace() || ['_', '-'].contains(ch))
160+
{
161+
// Found whitespace to break at
162+
if ch.is_whitespace() {
163+
writeln!(buf, "{}", &to_write[..break_idx])?;
164+
to_write = to_write[break_idx..].trim_start();
165+
} else {
166+
// Break at a `-` or `_` separator
167+
writeln!(buf, "{}", &to_write.get(..break_idx + 1).unwrap_or(to_write))?;
168+
to_write = to_write.get(break_idx + 1..).unwrap_or_default().trim_start();
169+
}
170+
} else {
171+
// No whitespace, we need to just split
172+
let ws_idx =
173+
iter.find(|(_, ch)| ch.is_whitespace()).map_or(to_write.len(), |(idx, _)| idx);
174+
writeln!(buf, "{}", &to_write[..ws_idx])?;
175+
to_write = to_write.get(ws_idx + 1..).map_or("", str::trim_start);
176+
}
177+
cur.set(0);
178+
}
179+
if link_url.is_some() {
180+
buf.write_all(b"\x1b]8;;\x1b\\")?;
181+
}
182+
183+
Ok(())
184+
})
185+
}
186+
187+
#[cfg(test)]
188+
#[path = "tests/term.rs"]
189+
mod tests;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# H1 Heading [with a link][remote-link]
2+
3+
H1 content: **some words in bold** and `so does inline code`
4+
5+
## H2 Heading
6+
7+
H2 content: _some words in italic_
8+
9+
### H3 Heading
10+
11+
H3 content: ~~strikethrough~~ text
12+
13+
#### H4 Heading
14+
15+
H4 content: A [simple link](https://docs.rs) and a [remote-link].
16+
17+
---
18+
19+
A section break was above. We can also do paragraph breaks:
20+
21+
(new paragraph) and unordered lists:
22+
23+
- Item 1 in `code`
24+
- Item 2 in _italics_
25+
26+
Or ordered:
27+
28+
1. Item 1 in **bold**
29+
2. Item 2 with some long lines that should wrap: Lorem ipsum dolor sit amet,
30+
consectetur adipiscing elit. Aenean ac mattis nunc. Phasellus elit quam,
31+
pulvinar ac risus in, dictum vehicula turpis. Vestibulum neque est, accumsan
32+
in cursus sit amet, dictum a nunc. Suspendisse aliquet, lorem eu eleifend
33+
accumsan, magna neque sodales nisi, a aliquet lectus leo eu sem.
34+
35+
---
36+
37+
## Code
38+
39+
Both `inline code` and code blocks are supported:
40+
41+
```rust
42+
/// A rust enum
43+
#[derive(Debug, PartialEq, Clone)]
44+
enum Foo {
45+
/// Start of line
46+
Bar
47+
}
48+
```
49+
50+
[remote-link]: http://docs.rs
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
H1 Heading ]8;;http://docs.rs\with a link]8;;\
2+
H1 content: some words in bold and so does inline code
3+
4+
H2 Heading
5+
H2 content: some words in italic
6+
7+
H3 Heading
8+
H3 content: strikethrough text
9+
10+
H4 Heading
11+
H4 content: A ]8;;https://docs.rs\simple link]8;;\ and a ]8;;http://docs.rs\remote-link]8;;\.
12+
--------------------------------------------------------------------------------------------------------------------------------------------
13+
A section break was above. We can also do paragraph breaks:
14+
15+
(new paragraph) and unordered lists:
16+
17+
* Item 1 in code
18+
* Item 2 in italics
19+
20+
Or ordered:
21+
22+
1. Item 1 in bold
23+
2. Item 2 with some long lines that should wrap: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ac mattis nunc. Phasellus
24+
elit quam, pulvinar ac risus in, dictum vehicula turpis. Vestibulum neque est, accumsan in cursus sit amet, dictum a nunc. Suspendisse
25+
aliquet, lorem eu eleifend accumsan, magna neque sodales nisi, a aliquet lectus leo eu sem.
26+
--------------------------------------------------------------------------------------------------------------------------------------------
27+
Code
28+
Both inline code and code blocks are supported:
29+
30+
/// A rust enum
31+
#[derive(Debug, PartialEq, Clone)]
32+
enum Foo {
33+
/// Start of line
34+
Bar
35+
}
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
use super::*;
2+
use ParseOpt as PO;
3+
4+
#[test]
5+
fn test_parse_simple() {
6+
let buf = "**abcd** rest";
7+
let (t, r) = parse_simple_pat(buf.as_bytes(), STG, STG, PO::None, MdTree::Strong).unwrap();
8+
assert_eq!(t, MdTree::Strong("abcd"));
9+
assert_eq!(r, b" rest");
10+
11+
// Escaping should fail
12+
let buf = r"**abcd\** rest";
13+
let res = parse_simple_pat(buf.as_bytes(), STG, STG, PO::None, MdTree::Strong);
14+
assert!(res.is_none());
15+
}
16+
17+
#[test]
18+
fn test_parse_comment() {
19+
let opt = PO::TrimNoEsc;
20+
let buf = "<!-- foobar! -->rest";
21+
let (t, r) = parse_simple_pat(buf.as_bytes(), CMT_S, CMT_E, opt, MdTree::Comment).unwrap();
22+
assert_eq!(t, MdTree::Comment("foobar!"));
23+
assert_eq!(r, b"rest");
24+
25+
let buf = r"<!-- foobar! \-->rest";
26+
let (t, r) = parse_simple_pat(buf.as_bytes(), CMT_S, CMT_E, opt, MdTree::Comment).unwrap();
27+
assert_eq!(t, MdTree::Comment(r"foobar! \"));
28+
assert_eq!(r, b"rest");
29+
}
30+
31+
#[test]
32+
fn test_parse_heading() {
33+
let buf1 = "# Top level\nrest";
34+
let (t, r) = parse_heading(buf1.as_bytes()).unwrap();
35+
assert_eq!(t, MdTree::Heading(1, vec![MdTree::PlainText("Top level")].into()));
36+
assert_eq!(r, b"\nrest");
37+
38+
let buf1 = "# Empty";
39+
let (t, r) = parse_heading(buf1.as_bytes()).unwrap();
40+
assert_eq!(t, MdTree::Heading(1, vec![MdTree::PlainText("Empty")].into()));
41+
assert_eq!(r, b"");
42+
43+
// Combo
44+
let buf2 = "### Top `level` _woo_\nrest";
45+
let (t, r) = parse_heading(buf2.as_bytes()).unwrap();
46+
assert_eq!(
47+
t,
48+
MdTree::Heading(
49+
3,
50+
vec![
51+
MdTree::PlainText("Top "),
52+
MdTree::CodeInline("level"),
53+
MdTree::PlainText(" "),
54+
MdTree::Emphasis("woo"),
55+
]
56+
.into()
57+
)
58+
);
59+
assert_eq!(r, b"\nrest");
60+
}
61+
62+
#[test]
63+
fn test_parse_code_inline() {
64+
let buf1 = "`abcd` rest";
65+
let (t, r) = parse_codeinline(buf1.as_bytes()).unwrap();
66+
assert_eq!(t, MdTree::CodeInline("abcd"));
67+
assert_eq!(r, b" rest");
68+
69+
// extra backticks, newline
70+
let buf2 = "```ab\ncd``` rest";
71+
let (t, r) = parse_codeinline(buf2.as_bytes()).unwrap();
72+
assert_eq!(t, MdTree::CodeInline("ab\ncd"));
73+
assert_eq!(r, b" rest");
74+
75+
// test no escaping
76+
let buf3 = r"`abcd\` rest";
77+
let (t, r) = parse_codeinline(buf3.as_bytes()).unwrap();
78+
assert_eq!(t, MdTree::CodeInline(r"abcd\"));
79+
assert_eq!(r, b" rest");
80+
}
81+
82+
#[test]
83+
fn test_parse_code_block() {
84+
let buf1 = "```rust\ncode\ncode\n```\nleftovers";
85+
let (t, r) = parse_codeblock(buf1.as_bytes());
86+
assert_eq!(t, MdTree::CodeBlock { txt: "code\ncode", lang: Some("rust") });
87+
assert_eq!(r, b"\nleftovers");
88+
89+
let buf2 = "`````\ncode\ncode````\n`````\nleftovers";
90+
let (t, r) = parse_codeblock(buf2.as_bytes());
91+
assert_eq!(t, MdTree::CodeBlock { txt: "code\ncode````", lang: None });
92+
assert_eq!(r, b"\nleftovers");
93+
}
94+
95+
#[test]
96+
fn test_parse_link() {
97+
let simple = "[see here](docs.rs) other";
98+
let (t, r) = parse_any_link(simple.as_bytes(), false).unwrap();
99+
assert_eq!(t, MdTree::Link { disp: "see here", link: "docs.rs" });
100+
assert_eq!(r, b" other");
101+
102+
let simple_toplevel = "[see here](docs.rs) other";
103+
let (t, r) = parse_any_link(simple_toplevel.as_bytes(), true).unwrap();
104+
assert_eq!(t, MdTree::Link { disp: "see here", link: "docs.rs" });
105+
assert_eq!(r, b" other");
106+
107+
let reference = "[see here] other";
108+
let (t, r) = parse_any_link(reference.as_bytes(), true).unwrap();
109+
assert_eq!(t, MdTree::RefLink { disp: "see here", id: None });
110+
assert_eq!(r, b" other");
111+
112+
let reference_full = "[see here][docs-rs] other";
113+
let (t, r) = parse_any_link(reference_full.as_bytes(), false).unwrap();
114+
assert_eq!(t, MdTree::RefLink { disp: "see here", id: Some("docs-rs") });
115+
assert_eq!(r, b" other");
116+
117+
let reference_def = "[see here]: docs.rs\nother";
118+
let (t, r) = parse_any_link(reference_def.as_bytes(), true).unwrap();
119+
assert_eq!(t, MdTree::LinkDef { id: "see here", link: "docs.rs" });
120+
assert_eq!(r, b"\nother");
121+
}
122+
123+
const IND1: &str = r"test standard
124+
ind
125+
ind2
126+
not ind";
127+
const IND2: &str = r"test end of stream
128+
1
129+
2
130+
";
131+
const IND3: &str = r"test empty lines
132+
1
133+
2
134+
135+
not ind";
136+
137+
#[test]
138+
fn test_indented_section() {
139+
let (t, r) = get_indented_section(IND1.as_bytes());
140+
assert_eq!(str::from_utf8(t).unwrap(), "test standard\n ind\n ind2");
141+
assert_eq!(str::from_utf8(r).unwrap(), "\nnot ind");
142+
143+
let (txt, rest) = get_indented_section(IND2.as_bytes());
144+
assert_eq!(str::from_utf8(txt).unwrap(), "test end of stream\n 1\n 2");
145+
assert_eq!(str::from_utf8(rest).unwrap(), "\n");
146+
147+
let (txt, rest) = get_indented_section(IND3.as_bytes());
148+
assert_eq!(str::from_utf8(txt).unwrap(), "test empty lines\n 1\n 2");
149+
assert_eq!(str::from_utf8(rest).unwrap(), "\n\nnot ind");
150+
}
151+
152+
const HBT: &str = r"# Heading
153+
154+
content";
155+
156+
#[test]
157+
fn test_heading_breaks() {
158+
let expected = vec![
159+
MdTree::Heading(1, vec![MdTree::PlainText("Heading")].into()),
160+
MdTree::PlainText("content"),
161+
]
162+
.into();
163+
let res = entrypoint(HBT);
164+
assert_eq!(res, expected);
165+
}
166+
167+
const NL1: &str = r"start
168+
169+
end";
170+
const NL2: &str = r"start
171+
172+
173+
end";
174+
const NL3: &str = r"start
175+
176+
177+
178+
end";
179+
180+
#[test]
181+
fn test_newline_breaks() {
182+
let expected =
183+
vec![MdTree::PlainText("start"), MdTree::ParagraphBreak, MdTree::PlainText("end")].into();
184+
for (idx, check) in [NL1, NL2, NL3].iter().enumerate() {
185+
let res = entrypoint(check);
186+
assert_eq!(res, expected, "failed {idx}");
187+
}
188+
}
189+
190+
const WRAP: &str = "plain _italics
191+
italics_";
192+
193+
#[test]
194+
fn test_wrap_pattern() {
195+
let expected = vec![
196+
MdTree::PlainText("plain "),
197+
MdTree::Emphasis("italics"),
198+
MdTree::Emphasis(" "),
199+
MdTree::Emphasis("italics"),
200+
]
201+
.into();
202+
let res = entrypoint(WRAP);
203+
assert_eq!(res, expected);
204+
}
205+
206+
const WRAP_NOTXT: &str = r"_italics_
207+
**bold**";
208+
209+
#[test]
210+
fn test_wrap_notxt() {
211+
let expected =
212+
vec![MdTree::Emphasis("italics"), MdTree::PlainText(" "), MdTree::Strong("bold")].into();
213+
let res = entrypoint(WRAP_NOTXT);
214+
assert_eq!(res, expected);
215+
}
216+
217+
const MIXED_LIST: &str = r"start
218+
- _italics item_
219+
<!-- comment -->
220+
- **bold item**
221+
second line [link1](foobar1)
222+
third line [link2][link-foo]
223+
- :crab:
224+
extra indent
225+
end
226+
[link-foo]: foobar2
227+
";
228+
229+
#[test]
230+
fn test_list() {
231+
let expected = vec![
232+
MdTree::PlainText("start"),
233+
MdTree::ParagraphBreak,
234+
MdTree::UnorderedListItem(vec![MdTree::Emphasis("italics item")].into()),
235+
MdTree::LineBreak,
236+
MdTree::UnorderedListItem(
237+
vec![
238+
MdTree::Strong("bold item"),
239+
MdTree::PlainText(" second line "),
240+
MdTree::Link { disp: "link1", link: "foobar1" },
241+
MdTree::PlainText(" third line "),
242+
MdTree::Link { disp: "link2", link: "foobar2" },
243+
]
244+
.into(),
245+
),
246+
MdTree::LineBreak,
247+
MdTree::UnorderedListItem(
248+
vec![MdTree::PlainText("🦀"), MdTree::PlainText(" extra indent")].into(),
249+
),
250+
MdTree::ParagraphBreak,
251+
MdTree::PlainText("end"),
252+
]
253+
.into();
254+
let res = entrypoint(MIXED_LIST);
255+
assert_eq!(res, expected);
256+
}
257+
258+
const SMOOSHED: &str = r#"
259+
start
260+
### heading
261+
1. ordered item
262+
```rust
263+
println!("Hello, world!");
264+
```
265+
`inline`
266+
``end``
267+
"#;
268+
269+
#[test]
270+
fn test_without_breaks() {
271+
let expected = vec![
272+
MdTree::PlainText("start"),
273+
MdTree::ParagraphBreak,
274+
MdTree::Heading(3, vec![MdTree::PlainText("heading")].into()),
275+
MdTree::OrderedListItem(1, vec![MdTree::PlainText("ordered item")].into()),
276+
MdTree::ParagraphBreak,
277+
MdTree::CodeBlock { txt: r#"println!("Hello, world!");"#, lang: Some("rust") },
278+
MdTree::ParagraphBreak,
279+
MdTree::CodeInline("inline"),
280+
MdTree::PlainText(" "),
281+
MdTree::CodeInline("end"),
282+
]
283+
.into();
284+
let res = entrypoint(SMOOSHED);
285+
assert_eq!(res, expected);
286+
}
287+
288+
const CODE_STARTLINE: &str = r#"
289+
start
290+
`code`
291+
middle
292+
`more code`
293+
end
294+
"#;
295+
296+
#[test]
297+
fn test_code_at_start() {
298+
let expected = vec![
299+
MdTree::PlainText("start"),
300+
MdTree::PlainText(" "),
301+
MdTree::CodeInline("code"),
302+
MdTree::PlainText(" "),
303+
MdTree::PlainText("middle"),
304+
MdTree::PlainText(" "),
305+
MdTree::CodeInline("more code"),
306+
MdTree::PlainText(" "),
307+
MdTree::PlainText("end"),
308+
]
309+
.into();
310+
let res = entrypoint(CODE_STARTLINE);
311+
assert_eq!(res, expected);
312+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use std::io::BufWriter;
2+
use std::path::PathBuf;
3+
use termcolor::{BufferWriter, ColorChoice};
4+
5+
use super::*;
6+
use crate::markdown::MdStream;
7+
8+
const INPUT: &str = include_str!("input.md");
9+
const OUTPUT_PATH: &[&str] = &[env!("CARGO_MANIFEST_DIR"), "src","markdown","tests","output.stdout"];
10+
11+
const TEST_WIDTH: usize = 80;
12+
13+
// We try to make some words long to create corner cases
14+
const TXT: &str = r"Lorem ipsum dolor sit amet, consecteturadipiscingelit.
15+
Fusce-id-urna-sollicitudin, pharetra nisl nec, lobortis tellus. In at
16+
metus hendrerit, tincidunteratvel, ultrices turpis. Curabitur_risus_sapien,
17+
porta-sed-nunc-sed, ultricesposuerelacus. Sed porttitor quis
18+
dolor non venenatis. Aliquam ut. ";
19+
20+
const WRAPPED: &str = r"Lorem ipsum dolor sit amet, consecteturadipiscingelit. Fusce-id-urna-
21+
sollicitudin, pharetra nisl nec, lobortis tellus. In at metus hendrerit,
22+
tincidunteratvel, ultrices turpis. Curabitur_risus_sapien, porta-sed-nunc-sed,
23+
ultricesposuerelacus. Sed porttitor quis dolor non venenatis. Aliquam ut. Lorem
24+
ipsum dolor sit amet, consecteturadipiscingelit. Fusce-id-urna-
25+
sollicitudin, pharetra nisl nec, lobortis tellus. In at metus hendrerit,
26+
tincidunteratvel, ultrices turpis. Curabitur_risus_sapien, porta-sed-nunc-
27+
sed, ultricesposuerelacus. Sed porttitor quis dolor non venenatis. Aliquam
28+
ut. Sample link lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
29+
consecteturadipiscingelit. Fusce-id-urna-sollicitudin, pharetra nisl nec,
30+
lobortis tellus. In at metus hendrerit, tincidunteratvel, ultrices turpis.
31+
Curabitur_risus_sapien, porta-sed-nunc-sed, ultricesposuerelacus. Sed porttitor
32+
quis dolor non venenatis. Aliquam ut. ";
33+
34+
#[test]
35+
fn test_wrapping_write() {
36+
WIDTH.with(|w| w.set(TEST_WIDTH));
37+
let mut buf = BufWriter::new(Vec::new());
38+
let txt = TXT.replace("-\n","-").replace("_\n","_").replace('\n', " ").replace(" ", "");
39+
write_wrapping(&mut buf, &txt, 0, None).unwrap();
40+
write_wrapping(&mut buf, &txt, 4, None).unwrap();
41+
write_wrapping(
42+
&mut buf,
43+
"Sample link lorem ipsum dolor sit amet. ",
44+
4,
45+
Some("link-address-placeholder"),
46+
)
47+
.unwrap();
48+
write_wrapping(&mut buf, &txt, 0, None).unwrap();
49+
let out = String::from_utf8(buf.into_inner().unwrap()).unwrap();
50+
let out = out
51+
.replace("\x1b\\", "")
52+
.replace('\x1b', "")
53+
.replace("]8;;", "")
54+
.replace("link-address-placeholder", "");
55+
56+
for line in out.lines() {
57+
assert!(line.len() <= TEST_WIDTH, "line length\n'{line}'")
58+
}
59+
60+
assert_eq!(out, WRAPPED);
61+
}
62+
63+
#[test]
64+
fn test_output() {
65+
// Capture `--bless` when run via ./x
66+
let bless = std::env::var("RUSTC_BLESS").unwrap_or_default() == "1";
67+
let ast = MdStream::parse_str(INPUT);
68+
let bufwtr = BufferWriter::stderr(ColorChoice::Always);
69+
let mut buffer = bufwtr.buffer();
70+
ast.write_termcolor_buf(&mut buffer).unwrap();
71+
72+
let mut blessed = PathBuf::new();
73+
blessed.extend(OUTPUT_PATH);
74+
75+
if bless {
76+
std::fs::write(&blessed, buffer.into_inner()).unwrap();
77+
eprintln!("blessed output at {}", blessed.display());
78+
} else {
79+
let output = buffer.into_inner();
80+
if std::fs::read(blessed).unwrap() != output {
81+
// hack: I don't know any way to write bytes to the captured stdout
82+
// that cargo test uses
83+
let mut out = std::io::stdout();
84+
out.write_all(b"\n\nMarkdown output did not match. Expected:\n").unwrap();
85+
out.write_all(&output).unwrap();
86+
out.write_all(b"\n\n").unwrap();
87+
panic!("markdown output mismatch");
88+
}
89+
}
90+
}

‎compiler/rustc_session/src/config.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,7 @@ impl Default for Options {
10271027
json_future_incompat: false,
10281028
pretty: None,
10291029
working_dir: RealFileName::LocalPath(std::env::current_dir().unwrap()),
1030+
color: ColorConfig::Auto,
10301031
}
10311032
}
10321033
}
@@ -2807,6 +2808,7 @@ pub fn build_session_options(
28072808
json_future_incompat,
28082809
pretty,
28092810
working_dir,
2811+
color,
28102812
}
28112813
}
28122814

‎compiler/rustc_session/src/options.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::search_paths::SearchPath;
44
use crate::utils::NativeLib;
55
use crate::{lint, EarlyErrorHandler};
66
use rustc_data_structures::profiling::TimePassesFormat;
7+
use rustc_errors::ColorConfig;
78
use rustc_errors::{LanguageIdentifier, TerminalUrl};
89
use rustc_target::spec::{CodeModel, LinkerFlavorCli, MergeFunctions, PanicStrategy, SanitizerSet};
910
use rustc_target::spec::{
@@ -212,6 +213,7 @@ top_level_options!(
212213

213214
/// The (potentially remapped) working directory
214215
working_dir: RealFileName [TRACKED],
216+
color: ColorConfig [UNTRACKED],
215217
}
216218
);
217219

‎src/bootstrap/test.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,6 +2219,11 @@ fn prepare_cargo_test(
22192219
) -> Command {
22202220
let mut cargo = cargo.into();
22212221

2222+
// If bless is passed, give downstream crates a way to use it
2223+
if builder.config.cmd.bless() {
2224+
cargo.env("RUSTC_BLESS", "1");
2225+
}
2226+
22222227
// Pass in some standard flags then iterate over the graph we've discovered
22232228
// in `cargo metadata` with the maps above and figure out what `-p`
22242229
// arguments need to get passed.

0 commit comments

Comments
 (0)
Please sign in to comment.