Skip to content

Commit 76139cc

Browse files
authored
Merge pull request #46 from ehuss/markdown-style
Add markdown style guide
2 parents 5234021 + ce16497 commit 76139cc

File tree

6 files changed

+233
-1
lines changed

6 files changed

+233
-1
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ jobs:
1818
env:
1919
SPEC_DENY_WARNINGS: 1
2020
run: mdbook build
21+
- name: Run style check
22+
run: (cd style-check && cargo run -- ../spec)

docs/authoring.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,21 @@
22

33
## Markdown formatting
44

5-
* Use ATX-style heading with sentence case.
5+
* Use [ATX-style headings][atx] (not Setext) with [sentence case].
6+
* Do not use tabs, only spaces.
7+
* Files must end with a newline.
8+
* Lines must not end with spaces. Double spaces have semantic meaning, but can be invisible. Use a trailing backslash if you need a hard line break.
9+
* If possible, avoid double blank lines.
10+
* Do not use indented code blocks, use 3+ backticks code blocks instead.
11+
* Code blocks should have an explicit language tag.
12+
* Do not wrap long lines. This helps with reviewing diffs of the source.
13+
* Use [smart punctuation] instead of Unicode characters. For example, use `---` for em-dash instead of the Unicode character. Characters like em-dash can be difficult to see in a fixed-width editor, and some editors may not have easy methods to enter such characters.
14+
15+
There are automated checks for some of these rules. Run `cargo run --manifest-path style-check/Cargo.toml -- spec` to run them locally.
16+
17+
[atx]: https://spec.commonmark.org/0.31.2/#atx-headings
18+
[sentence case]: https://apastyle.apa.org/style-grammar-guidelines/capitalization/sentence-case
19+
[smart punctuation]: https://rust-lang.github.io/mdBook/format/markdown.html#smart-punctuation
620

721
## Special markdown constructs
822

style-check/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
target

style-check/Cargo.lock

Lines changed: 71 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

style-check/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
name = "style-check"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
pulldown-cmark = "0.10.0"

style-check/src/main.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use std::env;
2+
use std::error::Error;
3+
use std::fs;
4+
use std::path::Path;
5+
6+
macro_rules! style_error {
7+
($bad:expr, $path:expr, $($arg:tt)*) => {
8+
*$bad = true;
9+
eprint!("error in {}: ", $path.display());
10+
eprintln!("{}", format_args!($($arg)*));
11+
};
12+
}
13+
14+
fn main() {
15+
let arg = env::args().nth(1).unwrap_or_else(|| {
16+
eprintln!("Please pass a src directory as the first argument");
17+
std::process::exit(1);
18+
});
19+
20+
let mut bad = false;
21+
if let Err(e) = check_directory(&Path::new(&arg), &mut bad) {
22+
eprintln!("error: {}", e);
23+
std::process::exit(1);
24+
}
25+
if bad {
26+
eprintln!("some style checks failed");
27+
std::process::exit(1);
28+
}
29+
eprintln!("passed!");
30+
}
31+
32+
fn check_directory(dir: &Path, bad: &mut bool) -> Result<(), Box<dyn Error>> {
33+
for entry in fs::read_dir(dir)? {
34+
let entry = entry?;
35+
let path = entry.path();
36+
37+
if path.is_dir() {
38+
check_directory(&path, bad)?;
39+
continue;
40+
}
41+
42+
if !matches!(
43+
path.extension().and_then(|p| p.to_str()),
44+
Some("md") | Some("html")
45+
) {
46+
// This may be extended in the future if other file types are needed.
47+
style_error!(bad, path, "expected only md or html in src");
48+
}
49+
50+
let contents = fs::read_to_string(&path)?;
51+
if contents.contains("#![feature") {
52+
style_error!(bad, path, "#![feature] attributes are not allowed");
53+
}
54+
if !cfg!(windows) && contents.contains('\r') {
55+
style_error!(
56+
bad,
57+
path,
58+
"CR characters not allowed, must use LF line endings"
59+
);
60+
}
61+
if contents.contains('\t') {
62+
style_error!(bad, path, "tab characters not allowed, use spaces");
63+
}
64+
if contents.contains('\u{2013}') {
65+
style_error!(bad, path, "en-dash not allowed, use two dashes like --");
66+
}
67+
if contents.contains('\u{2014}') {
68+
style_error!(bad, path, "em-dash not allowed, use three dashes like ---");
69+
}
70+
if !contents.ends_with('\n') {
71+
style_error!(bad, path, "file must end with a newline");
72+
}
73+
for line in contents.lines() {
74+
if line.ends_with(' ') {
75+
style_error!(bad, path, "lines must not end with spaces");
76+
}
77+
}
78+
cmark_check(&path, bad, &contents)?;
79+
}
80+
Ok(())
81+
}
82+
83+
fn cmark_check(path: &Path, bad: &mut bool, contents: &str) -> Result<(), Box<dyn Error>> {
84+
use pulldown_cmark::{BrokenLink, CodeBlockKind, Event, Options, Parser, Tag};
85+
86+
macro_rules! cmark_error {
87+
($bad:expr, $path:expr, $range:expr, $($arg:tt)*) => {
88+
*$bad = true;
89+
let lineno = contents[..$range.start].chars().filter(|&ch| ch == '\n').count() + 1;
90+
eprint!("error in {} (line {}): ", $path.display(), lineno);
91+
eprintln!("{}", format_args!($($arg)*));
92+
}
93+
}
94+
95+
let options = Options::all();
96+
// Can't use `bad` because it would get captured in closure.
97+
let mut link_err = false;
98+
let mut cb = |link: BrokenLink<'_>| {
99+
cmark_error!(
100+
&mut link_err,
101+
path,
102+
link.span,
103+
"broken {:?} link (reference `{}`)",
104+
link.link_type,
105+
link.reference
106+
);
107+
None
108+
};
109+
let parser = Parser::new_with_broken_link_callback(contents, options, Some(&mut cb));
110+
111+
for (event, range) in parser.into_offset_iter() {
112+
match event {
113+
Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) => {
114+
cmark_error!(
115+
bad,
116+
path,
117+
range,
118+
"indented code blocks should use triple backtick-style \
119+
with a language identifier"
120+
);
121+
}
122+
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(languages))) => {
123+
if languages.is_empty() {
124+
cmark_error!(
125+
bad,
126+
path,
127+
range,
128+
"code block should include an explicit language",
129+
);
130+
}
131+
}
132+
_ => {}
133+
}
134+
}
135+
*bad |= link_err;
136+
Ok(())
137+
}

0 commit comments

Comments
 (0)