Skip to content

Commit

Permalink
WIP mdbook extension
Browse files Browse the repository at this point in the history
  • Loading branch information
willcrichton committed Apr 6, 2024
1 parent a878474 commit a498482
Show file tree
Hide file tree
Showing 14 changed files with 1,886 additions and 20 deletions.
922 changes: 905 additions & 17 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ members = [ "crates/*" ]
exclude = [ "crates/argus_cli/tests/workspaces", "examples" ]
resolver = "2"

[profile.dev.package.similar]
opt-level = 3
[patch.crates-io]
mdbook-preprocessor-utils = { path = "../mdbook-preprocessor-utils" }
1 change: 1 addition & 0 deletions crates/mdbook-argus/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
js/
20 changes: 20 additions & 0 deletions crates/mdbook-argus/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "mdbook-argus"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.81"
html-escape = "0.2.13"
mdbook-preprocessor-utils = "0.1.6"
nom = "7.1.3"
nom_locate = "4.2.0"
rayon = "1.10.0"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
tempfile = "3.10.1"
toml = "0.8.12"

[build-dependencies]
anyhow = "1.0.81"
mdbook-preprocessor-utils = "0.1.6"
10 changes: 10 additions & 0 deletions crates/mdbook-argus/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use std::path::Path;

use anyhow::Result;

const SRC_DIR: &str = "../../ide/packages/panoptes/dist/";
const DST_DIR: &str = "./js";

fn main() -> Result<()> {
mdbook_preprocessor_utils::copy_assets(Path::new(SRC_DIR), Path::new(DST_DIR))
}
1 change: 1 addition & 0 deletions crates/mdbook-argus/rust-toolchain.toml
80 changes: 80 additions & 0 deletions crates/mdbook-argus/src/block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! Parser for Argus code blocks within Markdown.
use std::{hash::Hash, ops::Range};

use nom::{
branch::alt,
bytes::complete::{tag, take_until},
character::complete::{anychar, char, none_of},
combinator::map,
multi::many0,
sequence::{preceded, separated_pair, tuple},
IResult,
};
use nom_locate::LocatedSpan;

#[derive(PartialEq, Hash, Debug, Clone)]
pub struct ArgusBlock {
pub config: Vec<(String, String)>,
pub code: String,
}

impl ArgusBlock {
fn parse(i: LocatedSpan<&str>) -> IResult<LocatedSpan<&str>, Self> {
fn parse_sym(i: LocatedSpan<&str>) -> IResult<LocatedSpan<&str>, String> {
let (i, v) = many0(none_of(",=\n+"))(i)?;
Ok((i, v.into_iter().collect::<String>()))
}

let mut parser = tuple((
tag("```argus"),
many0(preceded(
char(','),
alt((
separated_pair(parse_sym, char('='), parse_sym),
map(parse_sym, |s| (s, String::from("true"))),
)),
)),
take_until("```"),
tag("```"),
));
let (i, (_, config, code, _)) = parser(i)?;
let code = code.fragment().trim().to_string();

Ok((i, ArgusBlock { config, code }))
}

pub fn parse_all(content: &str) -> Vec<(Range<usize>, Self)> {
let mut content = LocatedSpan::new(content);
let mut to_process = Vec::new();
loop {
if let Ok((next, block)) = ArgusBlock::parse(content) {
let range = content.location_offset() .. next.location_offset();
to_process.push((range, block));
content = next;
} else {
match anychar::<_, nom::error::Error<LocatedSpan<&str>>>(content) {
Ok((next, _)) => {
content = next;
}
Err(_) => break,
}
}
}

to_process
}
}

#[test]
fn test_parse_block() {
let inp = r#"```argus,foo=bar,baz
content!
```"#;
let s = |s: &str| s.to_string();
let blocks = ArgusBlock::parse_all(inp);
assert_eq!(blocks, vec![(0 .. inp.len(), ArgusBlock {
config: vec![(s("foo"), s("bar")), (s("baz"), s("true"))],
code: s("content!"),
})]);
}
169 changes: 169 additions & 0 deletions crates/mdbook-argus/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use std::{
fs,
ops::Range,
path::{Path, PathBuf},
process::{Command, Stdio},
};

use anyhow::{anyhow, ensure, Context, Result};
use block::ArgusBlock;
use mdbook_preprocessor_utils::{
mdbook::preprocess::PreprocessorContext, Asset, HtmlElementBuilder,
SimplePreprocessor,
};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tempfile::tempdir;

mod block;

mdbook_preprocessor_utils::asset_generator!("../js/");

const FRONTEND_ASSETS: [Asset; 2] =
[make_asset!("panoptes.iife.js"), make_asset!("style.css")];

const TOOLCHAIN_TOML: &str = include_str!("../rust-toolchain.toml");

pub fn get_toolchain() -> Result<String> {
let config: toml::Value = toml::from_str(TOOLCHAIN_TOML)?;
Ok(
config
.get("toolchain")
.context("Missing toolchain key")?
.get("channel")
.context("Missing channel key")?
.as_str()
.unwrap()
.to_string(),
)
}

struct ArgusPreprocessor {
toolchain: String,
target_libdir: PathBuf,
}

impl ArgusPreprocessor {
fn new() -> Result<Self> {
let run_and_get_output = |cmd: &mut Command| -> Result<String> {
let output = cmd.output()?;
ensure!(output.status.success(), "Command failed");
let stdout = String::from_utf8(output.stdout)?;
Ok(stdout.trim_end().to_string())
};

let toolchain = get_toolchain()?;
let output = run_and_get_output(Command::new("rustup").args([
"which",
"--toolchain",
&toolchain,
"rustc",
]))?;
let rustc = PathBuf::from(output);

let output = run_and_get_output(
Command::new(rustc).args(["--print", "target-libdir"]),
)?;
let target_libdir = PathBuf::from(output);

Ok(ArgusPreprocessor {
toolchain,
target_libdir,
})
}

fn run_argus(&self, block: &ArgusBlock) -> Result<String> {
let tempdir = tempdir()?;
let root = tempdir.path();
let status = Command::new("cargo")
.args(["new", "--bin", "example"])
.current_dir(root)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
ensure!(status.success(), "Cargo failed");

let crate_root = root.join("example");
fs::write(crate_root.join("src/main.rs"), &block.code)?;

let mut cmd = Command::new("cargo");
cmd
.args([&format!("+{}", self.toolchain), "argus", "bundle"])
.env("DYLD_LIBRARY_PATH", &self.target_libdir)
.env("LD_LIBRARY_PATH", &self.target_libdir)
.current_dir(crate_root);

let child = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
let output = child.wait_with_output()?;
ensure!(
output.status.success(),
"Argus failed for program:\n{}\nwith error:\n{}",
block.code,
String::from_utf8(output.stderr)?
);

let response = String::from_utf8(output.stdout)?;
let response_json_res: Result<Vec<serde_json::Value>, String> =
serde_json::from_str(&response)?;
let response_json = response_json_res.map_err(|e| anyhow!("{e}"))?;

let bodies = response_json
.iter()
.map(|obj| obj.as_object().unwrap().get("body").unwrap().clone())
.collect::<Vec<_>>();
let config = json!({
"type": "WEB_BUNDLE",
"closedSystem": response_json,
"data": [("main.rs", bodies)]
});

Ok(serde_json::to_string(&config)?)
}

fn process_code(&self, block: ArgusBlock) -> Result<String> {
let config = self.run_argus(&block)?;

let mut element = HtmlElementBuilder::new("argus-embed");
element.data("config", config)?;
element.finish()
}
}

impl SimplePreprocessor for ArgusPreprocessor {
fn name() -> &'static str {
"argus"
}

fn build(ctx: &PreprocessorContext) -> Result<Self> {
ArgusPreprocessor::new()
}

fn replacements(
&self,
chapter_dir: &Path,
content: &str,
) -> Result<Vec<(Range<usize>, String)>> {
ArgusBlock::parse_all(content)
.into_par_iter()
.map(|(range, block)| {
let html = self.process_code(block)?;
Ok((range, html))
})
.collect()
}

fn linked_assets(&self) -> Vec<Asset> {
FRONTEND_ASSETS.to_vec()
}

fn all_assets(&self) -> Vec<Asset> {
self.linked_assets()
}

fn finish(self) {}
}

fn main() {
mdbook_preprocessor_utils::main::<ArgusPreprocessor>()
}
2 changes: 2 additions & 0 deletions crates/mdbook-argus/test-book/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
book/
src/argus
10 changes: 10 additions & 0 deletions crates/mdbook-argus/test-book/book.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[book]
authors = ["Will Crichton"]
language = "en"
multilingual = false
src = "src"

[output.html]
additional-css = ["vars.css"]

[preprocessor.argus]
3 changes: 3 additions & 0 deletions crates/mdbook-argus/test-book/src/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Summary

- [Chapter 1](./chapter_1.md)
30 changes: 30 additions & 0 deletions crates/mdbook-argus/test-book/src/chapter_1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Chapter 1

```argus
trait IntoString {
fn to_string(&self) -> String;
}
impl IntoString for (i32, i32) {
fn to_string(&self) -> String {
String::from("(...)")
}
}
impl<T: IntoString> IntoString for Vec<T> {
fn to_string(&self) -> String {
String::from("Vec<T>")
}
}
impl<T: IntoString> IntoString for [T] {
fn to_string(&self) -> String {
String::from("[T]")
}
}
fn main() {
fn is_into_string<T: IntoString>() {}
is_into_string::<Vec<&str>>();
}
```
Loading

0 comments on commit a498482

Please sign in to comment.