Skip to content

Commit

Permalink
Merge pull request #46 from ehuss/markdown-style
Browse files Browse the repository at this point in the history
Add markdown style guide
  • Loading branch information
pnkfelix authored Apr 3, 2024
2 parents 5234021 + ce16497 commit 76139cc
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ jobs:
env:
SPEC_DENY_WARNINGS: 1
run: mdbook build
- name: Run style check
run: (cd style-check && cargo run -- ../spec)
16 changes: 15 additions & 1 deletion docs/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@

## Markdown formatting

* Use ATX-style heading with sentence case.
* Use [ATX-style headings][atx] (not Setext) with [sentence case].
* Do not use tabs, only spaces.
* Files must end with a newline.
* 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.
* If possible, avoid double blank lines.
* Do not use indented code blocks, use 3+ backticks code blocks instead.
* Code blocks should have an explicit language tag.
* Do not wrap long lines. This helps with reviewing diffs of the source.
* 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.

There are automated checks for some of these rules. Run `cargo run --manifest-path style-check/Cargo.toml -- spec` to run them locally.

[atx]: https://spec.commonmark.org/0.31.2/#atx-headings
[sentence case]: https://apastyle.apa.org/style-grammar-guidelines/capitalization/sentence-case
[smart punctuation]: https://rust-lang.github.io/mdBook/format/markdown.html#smart-punctuation

## Special markdown constructs

Expand Down
1 change: 1 addition & 0 deletions style-check/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
71 changes: 71 additions & 0 deletions style-check/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions style-check/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "style-check"
version = "0.1.0"
edition = "2021"

[dependencies]
pulldown-cmark = "0.10.0"
137 changes: 137 additions & 0 deletions style-check/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use std::env;
use std::error::Error;
use std::fs;
use std::path::Path;

macro_rules! style_error {
($bad:expr, $path:expr, $($arg:tt)*) => {
*$bad = true;
eprint!("error in {}: ", $path.display());
eprintln!("{}", format_args!($($arg)*));
};
}

fn main() {
let arg = env::args().nth(1).unwrap_or_else(|| {
eprintln!("Please pass a src directory as the first argument");
std::process::exit(1);
});

let mut bad = false;
if let Err(e) = check_directory(&Path::new(&arg), &mut bad) {
eprintln!("error: {}", e);
std::process::exit(1);
}
if bad {
eprintln!("some style checks failed");
std::process::exit(1);
}
eprintln!("passed!");
}

fn check_directory(dir: &Path, bad: &mut bool) -> Result<(), Box<dyn Error>> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();

if path.is_dir() {
check_directory(&path, bad)?;
continue;
}

if !matches!(
path.extension().and_then(|p| p.to_str()),
Some("md") | Some("html")
) {
// This may be extended in the future if other file types are needed.
style_error!(bad, path, "expected only md or html in src");
}

let contents = fs::read_to_string(&path)?;
if contents.contains("#![feature") {
style_error!(bad, path, "#![feature] attributes are not allowed");
}
if !cfg!(windows) && contents.contains('\r') {
style_error!(
bad,
path,
"CR characters not allowed, must use LF line endings"
);
}
if contents.contains('\t') {
style_error!(bad, path, "tab characters not allowed, use spaces");
}
if contents.contains('\u{2013}') {
style_error!(bad, path, "en-dash not allowed, use two dashes like --");
}
if contents.contains('\u{2014}') {
style_error!(bad, path, "em-dash not allowed, use three dashes like ---");
}
if !contents.ends_with('\n') {
style_error!(bad, path, "file must end with a newline");
}
for line in contents.lines() {
if line.ends_with(' ') {
style_error!(bad, path, "lines must not end with spaces");
}
}
cmark_check(&path, bad, &contents)?;
}
Ok(())
}

fn cmark_check(path: &Path, bad: &mut bool, contents: &str) -> Result<(), Box<dyn Error>> {
use pulldown_cmark::{BrokenLink, CodeBlockKind, Event, Options, Parser, Tag};

macro_rules! cmark_error {
($bad:expr, $path:expr, $range:expr, $($arg:tt)*) => {
*$bad = true;
let lineno = contents[..$range.start].chars().filter(|&ch| ch == '\n').count() + 1;
eprint!("error in {} (line {}): ", $path.display(), lineno);
eprintln!("{}", format_args!($($arg)*));
}
}

let options = Options::all();
// Can't use `bad` because it would get captured in closure.
let mut link_err = false;
let mut cb = |link: BrokenLink<'_>| {
cmark_error!(
&mut link_err,
path,
link.span,
"broken {:?} link (reference `{}`)",
link.link_type,
link.reference
);
None
};
let parser = Parser::new_with_broken_link_callback(contents, options, Some(&mut cb));

for (event, range) in parser.into_offset_iter() {
match event {
Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) => {
cmark_error!(
bad,
path,
range,
"indented code blocks should use triple backtick-style \
with a language identifier"
);
}
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(languages))) => {
if languages.is_empty() {
cmark_error!(
bad,
path,
range,
"code block should include an explicit language",
);
}
}
_ => {}
}
}
*bad |= link_err;
Ok(())
}

0 comments on commit 76139cc

Please sign in to comment.