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

Mdbook spec to lib #65

Merged
merged 3 commits into from
Jul 5, 2024
Merged
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
4 changes: 4 additions & 0 deletions mdbook-spec/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## mdbook-spec 0.1.1

- Moved code to a library to support upstream integration.

## mdbook-spec 0.1.0

- Initial release
2 changes: 1 addition & 1 deletion mdbook-spec/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mdbook-spec"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "An mdBook preprocessor to help with the Rust specification."
Expand Down
172 changes: 172 additions & 0 deletions mdbook-spec/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use mdbook::book::{Book, Chapter};
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use mdbook::BookItem;
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use semver::{Version, VersionReq};
use std::collections::BTreeMap;
use std::io;
use std::path::PathBuf;

mod std_links;

/// The Regex for rules like `r[foo]`.
static RULE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^r\[([^]]+)]$").unwrap());

/// The Regex for the syntax for blockquotes that have a specific CSS class,
/// like `> [!WARNING]`.
static ADMONITION_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *> .*\n)+)").unwrap()
});

pub fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;

let book_version = Version::parse(&ctx.mdbook_version)?;
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;

if !version_req.matches(&book_version) {
eprintln!(
"warning: The {} plugin was built against version {} of mdbook, \
but we're being called from version {}",
pre.name(),
mdbook::MDBOOK_VERSION,
ctx.mdbook_version
);
}

let processed_book = pre.run(&ctx, book)?;
serde_json::to_writer(io::stdout(), &processed_book)?;

Ok(())
}

pub struct Spec {
/// Whether or not warnings should be errors (set by SPEC_DENY_WARNINGS
/// environment variable).
deny_warnings: bool,
}

impl Spec {
pub fn new() -> Spec {
Spec {
deny_warnings: std::env::var("SPEC_DENY_WARNINGS").as_deref() == Ok("1"),
}
}

/// Converts lines that start with `r[…]` into a "rule" which has special
/// styling and can be linked to.
fn rule_definitions(
&self,
chapter: &Chapter,
found_rules: &mut BTreeMap<String, (PathBuf, PathBuf)>,
) -> String {
let source_path = chapter.source_path.clone().unwrap_or_default();
let path = chapter.path.clone().unwrap_or_default();
RULE_RE
.replace_all(&chapter.content, |caps: &Captures| {
let rule_id = &caps[1];
if let Some((old, _)) =
found_rules.insert(rule_id.to_string(), (source_path.clone(), path.clone()))
{
let message = format!(
"rule `{rule_id}` defined multiple times\n\
First location: {old:?}\n\
Second location: {source_path:?}"
);
if self.deny_warnings {
panic!("error: {message}");
} else {
eprintln!("warning: {message}");
}
}
format!(
"<div class=\"rule\" id=\"{rule_id}\">\
<a class=\"rule-link\" href=\"#{rule_id}\">[{rule_id}]</a>\
</div>\n"
)
})
.to_string()
}

/// Generates link references to all rules on all pages, so you can easily
/// refer to rules anywhere in the book.
fn auto_link_references(
&self,
chapter: &Chapter,
found_rules: &BTreeMap<String, (PathBuf, PathBuf)>,
) -> String {
let current_path = chapter.path.as_ref().unwrap().parent().unwrap();
let definitions: String = found_rules
.iter()
.map(|(rule_id, (_, path))| {
let relative = pathdiff::diff_paths(path, current_path).unwrap();
format!("[{rule_id}]: {}#{rule_id}\n", relative.display())
})
.collect();
format!(
"{}\n\
{definitions}",
chapter.content
)
}

/// Converts blockquotes with special headers into admonitions.
///
/// The blockquote should look something like:
///
/// ```
/// > [!WARNING]
/// > ...
/// ```
///
/// This will add a `<div class="warning">` around the blockquote so that
/// it can be styled differently. Any text between the brackets that can
/// be a CSS class is valid. The actual styling needs to be added in a CSS
/// file.
fn admonitions(&self, chapter: &Chapter) -> String {
ADMONITION_RE
.replace_all(&chapter.content, |caps: &Captures| {
let lower = caps["admon"].to_lowercase();
format!(
"<div class=\"{lower}\">\n\n{}\n\n</div>\n",
&caps["blockquote"]
)
})
.to_string()
}
}

impl Preprocessor for Spec {
fn name(&self) -> &str {
"nop-preprocessor"
}

fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
let mut found_rules = BTreeMap::new();
book.for_each_mut(|item| {
let BookItem::Chapter(ch) = item else {
return;
};
if ch.is_draft_chapter() {
return;
}
ch.content = self.rule_definitions(&ch, &mut found_rules);
ch.content = self.admonitions(&ch);
ch.content = std_links::std_links(&ch);
});
// This is a separate pass because it relies on the modifications of
// the previous passes.
book.for_each_mut(|item| {
let BookItem::Chapter(ch) = item else {
return;
};
if ch.is_draft_chapter() {
return;
}
ch.content = self.auto_link_references(&ch, &found_rules);
});
Ok(book)
}
}
180 changes: 3 additions & 177 deletions mdbook-spec/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,3 @@
use mdbook::book::{Book, Chapter};
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use mdbook::BookItem;
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use semver::{Version, VersionReq};
use std::collections::BTreeMap;
use std::io;
use std::path::PathBuf;
use std::process;

mod std_links;

/// The Regex for rules like `r[foo]`.
static RULE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^r\[([^]]+)]$").unwrap());

/// The Regex for the syntax for blockquotes that have a specific CSS class,
/// like `> [!WARNING]`.
static ADMONITION_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *> .*\n)+)").unwrap()
});

fn main() {
let mut args = std::env::args().skip(1);
match args.next().as_deref() {
Expand All @@ -35,161 +12,10 @@ fn main() {
None => {}
}

let preprocessor = Spec::new();
let preprocessor = mdbook_spec::Spec::new();

if let Err(e) = handle_preprocessing(&preprocessor) {
if let Err(e) = mdbook_spec::handle_preprocessing(&preprocessor) {
eprintln!("{}", e);
process::exit(1);
}
}

fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;

let book_version = Version::parse(&ctx.mdbook_version)?;
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;

if !version_req.matches(&book_version) {
eprintln!(
"warning: The {} plugin was built against version {} of mdbook, \
but we're being called from version {}",
pre.name(),
mdbook::MDBOOK_VERSION,
ctx.mdbook_version
);
}

let processed_book = pre.run(&ctx, book)?;
serde_json::to_writer(io::stdout(), &processed_book)?;

Ok(())
}

struct Spec {
/// Whether or not warnings should be errors (set by SPEC_DENY_WARNINGS
/// environment variable).
deny_warnings: bool,
}

impl Spec {
pub fn new() -> Spec {
Spec {
deny_warnings: std::env::var("SPEC_DENY_WARNINGS").as_deref() == Ok("1"),
}
}

/// Converts lines that start with `r[…]` into a "rule" which has special
/// styling and can be linked to.
fn rule_definitions(
&self,
chapter: &Chapter,
found_rules: &mut BTreeMap<String, (PathBuf, PathBuf)>,
) -> String {
let source_path = chapter.source_path.clone().unwrap_or_default();
let path = chapter.path.clone().unwrap_or_default();
RULE_RE
.replace_all(&chapter.content, |caps: &Captures| {
let rule_id = &caps[1];
if let Some((old, _)) =
found_rules.insert(rule_id.to_string(), (source_path.clone(), path.clone()))
{
let message = format!(
"rule `{rule_id}` defined multiple times\n\
First location: {old:?}\n\
Second location: {source_path:?}"
);
if self.deny_warnings {
panic!("error: {message}");
} else {
eprintln!("warning: {message}");
}
}
format!(
"<div class=\"rule\" id=\"{rule_id}\">\
<a class=\"rule-link\" href=\"#{rule_id}\">[{rule_id}]</a>\
</div>\n"
)
})
.to_string()
}

/// Generates link references to all rules on all pages, so you can easily
/// refer to rules anywhere in the book.
fn auto_link_references(
&self,
chapter: &Chapter,
found_rules: &BTreeMap<String, (PathBuf, PathBuf)>,
) -> String {
let current_path = chapter.path.as_ref().unwrap().parent().unwrap();
let definitions: String = found_rules
.iter()
.map(|(rule_id, (_, path))| {
let relative = pathdiff::diff_paths(path, current_path).unwrap();
format!("[{rule_id}]: {}#{rule_id}\n", relative.display())
})
.collect();
format!(
"{}\n\
{definitions}",
chapter.content
)
}

/// Converts blockquotes with special headers into admonitions.
///
/// The blockquote should look something like:
///
/// ```
/// > [!WARNING]
/// > ...
/// ```
///
/// This will add a `<div class="warning">` around the blockquote so that
/// it can be styled differently. Any text between the brackets that can
/// be a CSS class is valid. The actual styling needs to be added in a CSS
/// file.
fn admonitions(&self, chapter: &Chapter) -> String {
ADMONITION_RE
.replace_all(&chapter.content, |caps: &Captures| {
let lower = caps["admon"].to_lowercase();
format!(
"<div class=\"{lower}\">\n\n{}\n\n</div>\n",
&caps["blockquote"]
)
})
.to_string()
}
}

impl Preprocessor for Spec {
fn name(&self) -> &str {
"nop-preprocessor"
}

fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
let mut found_rules = BTreeMap::new();
book.for_each_mut(|item| {
let BookItem::Chapter(ch) = item else {
return;
};
if ch.is_draft_chapter() {
return;
}
ch.content = self.rule_definitions(&ch, &mut found_rules);
ch.content = self.admonitions(&ch);
ch.content = std_links::std_links(&ch);
});
// This is a separate pass because it relies on the modifications of
// the previous passes.
book.for_each_mut(|item| {
let BookItem::Chapter(ch) = item else {
return;
};
if ch.is_draft_chapter() {
return;
}
ch.content = self.auto_link_references(&ch, &found_rules);
});
Ok(book)
std::process::exit(1);
}
}