Skip to content
This repository was archived by the owner on May 20, 2020. It is now read-only.

Support alternative frontends #191

Merged
merged 5 commits into from
Oct 17, 2017
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
55 changes: 39 additions & 16 deletions Cargo.lock

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

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ doctest = false

[dependencies]
clap = "2.24.2"
env_logger = "0.4"
error-chain = "0.11"
indicatif = "0.6.0"
indoc = "0.2"
lazy_static = "0.2"
log = "0.3"
open = "1.2.0"
pulldown-cmark = { version = "0.1", default-features = false }
quote = "0.3"
Expand All @@ -29,9 +32,7 @@ syn = { version = "0.11", features = ["full"] }
tempdir = "0.3"

[build-dependencies]
error-chain = "0.11"
quote = "0.3"
glob = "0.2.11"

[dev-dependencies]
error-chain = "0.11"
Expand All @@ -40,3 +41,7 @@ lazy_static = "0.2"
regex = "0.2"
serde_json = "1.0.2"
shlex = "0.1"

[workspace]
members = ["rustdoc-ember"]
exclude = ["example"]
52 changes: 34 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ for more information about the purpose of this project.

There are a few top-level directories that are important:

- `src` contains the main source code for `rustdoc`. It will embed the HTML, CSS,
and JS from the `frontend` directory, in the final binary.
- Any of the presentation of the documentation such as layout and how it works is
found in that `frontend` directory.
- `src` contains the source code for `rustdoc`. It includes two binaries: the
backend (`rustdoc`) and the bundled frontend (`rustdoc-ember`).
`rustdoc-ember` will embed the HTML, CSS, and JS from the `frontend` directory
in the final binary.
- Any presentation of the documentation (such as layout and how it works) is
found in the `frontend` directory.
- The `tests` directory contains tests for things we've added support for in
`rustdoc`. It tests that the output of the JSON generated by rustdoc for the
frontend is what it expects to get using comment annotations to test various
assertions.
- The `example` directory contains a sample crate that you can use to try out with
`rustdoc`; we add stuff to it as we add support for various things into `rustdoc`
itself.
`rustdoc`. It tests the JSON generated by `rustdoc` for consumption by
frontend binaries. Comment annotations are used to test various assertions.
- The `example` directory contains a sample crate that you can try out `rustdoc`
with.

### The backend

Expand All @@ -59,10 +59,11 @@ Here's how it does it:
4. These two pieces are turned into a `Documentation` struct which is immediately serialized
to JSON, specifically, a subset of [JSON API][json_api] as we don't need all of the items
used in the spec.
5. It writes out this JSON to the `target/doc` directory of the crate that it's documenting.
6. It writes out some HTML/CSS/JS from the frontend to `target/doc` as well.
5. It shells out to a frontend binary, writing the documentation JSON to stdin.
The frontend is expected to write out HTML, CSS, JS, etc. to `target/doc`.

You can also request it to only write out some of this information through the `--emit` flag.
You can also request it write out some or all of these artifacts through the
`--emit` flag.

### The frontend

Expand All @@ -77,18 +78,33 @@ drives the rest of the site.
One other slightly unusual aspect of the frontend: normally, you'd have the `dist`
directory ignored, as you don't want to commit generated files. In this case, though,
we don't want `ember` to be a dependency of installing `rustdoc`, and so we do commit
those generated files.
those generated files. They are bundled in the `rustdoc-ember` binary.

#### Alternative Frontends

`rustdoc` allows using alternative frontends, provided that the frontend
conforms to a particular interface.

- The frontend must read its input from stdin. `rustdoc` will pipe the
documentation JSON generated by the backend into the frontend as a subprocess.
- The frontend must allow an `--output <path>` argument, for specifying where
the frontend should output its files.
- The frontend is free to generate whatever files it pleases in the output
directory, but typical frontends will at least generate `index.html` at the
root.

To use an alternative frontend, set the `RUSTDOC_FRONTEND` environment variable
to a path to a frontend binary.

## Usage

Currently, it only builds the given example. Do it as follow:
To build and view documentation:

```
cargo run --release -- --manifest-path=example/Cargo.toml
cargo build --all --release
cargo run --release --bin rustdoc -- --manifest-path=example/Cargo.toml open
```

Then open a web browser and open "rustdoc/example/target/doc/index.html".

## Known issues (and their solution)

* "javascript error: data.json isn't found": go to `example/target/doc` and then run
Expand Down
151 changes: 20 additions & 131 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,154 +1,36 @@
//! This build script creates source tests from the files in the `tests/source` directory.

#![recursion_limit = "1024"]

#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate quote;
extern crate glob;

use std::env;
use std::fmt;
use std::fs::File;
use std::fs;
use std::io::{Write, BufWriter, stderr};
use std::io::prelude::*;
use std::io;
use std::path::Path;
use std::process::exit;
use std::thread;
use std::process;

use quote::Ident;
use glob::glob;

error_chain! {
foreign_links {
Io(::std::io::Error);
PatternError(::glob::PatternError);
GlobError(::glob::GlobError);
}
}

const SOURCE_TEST_DIR: &str = "tests/source";

// Location of the dist folder
const DIST: &str = "frontend/dist/";

fn run() -> Result<()> {
let source_thread = thread::spawn(|| generate_source_tests());
let asset_thread = thread::spawn(|| write_asset_file());

source_thread.join().unwrap()?;
asset_thread.join().unwrap()?;

Ok(())
}

fn main() {
if let Err(ref e) = run() {
let stderr = &mut stderr();
let errmsg = "Error writing to stderr";

writeln!(stderr, "Error: {}", e).expect(errmsg);

for e in e.iter().skip(1) {
writeln!(stderr, "Caused by: {}", e).expect(errmsg);
}

// The backtrace is not always generated. Try to run this example
// with `RUST_BACKTRACE=1`.
if let Some(backtrace) = e.backtrace() {
writeln!(stderr, "Backtrace: {:?}", backtrace).expect(errmsg);
}

exit(1);
}
}

/// Start the recursion from the top level of the frontend's dist folder
fn acquire_assets() -> Result<Vec<Asset>> {
let mut output: Vec<Asset> = Vec::new();
let whitelist = vec![
"assets/**/*",
"crossdomain.xml",
"ember-fetch/**/*",
"index.html",
"robots.txt",
];

for w in whitelist {
for entry in glob(&format!("{}{}", DIST, w))? {
let path = entry?;
if path.is_file() {
output.push(Asset {
// If the directory isn't valid this wouldn't have worked.
path: String::from(path.to_str().unwrap()).replace("\\", "/"),
});
}
}
}

Ok(output)
}

/// Write to asset.in which is a vec expression of static assets that will be
/// imported using the include!() macro for the `Config` struct
fn write_asset_file() -> Result<()> {
let assets = acquire_assets()?;
let asset_out = Path::new(&env::var("OUT_DIR").unwrap()).join("asset.in");

let mut writer = BufWriter::new(File::create(asset_out)?);
write!(writer, "vec![")?;
for asset in assets {
write!(writer, "{},", asset)?;
}
write!(writer, "]")?;

Ok(())
}

/// Intermediary data structure used to hold the path of the file
pub struct Asset {
pub path: String,
}

// This gives us the ToString impl as well. We're using this to write out what
// an asset would look like for the actual program into the asset.in file.
impl fmt::Display for Asset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// We know this will give the proper name so we can do this to get the
// proper name for the asset without the rest of the beginning path
let name = self.path
.split(DIST)
.skip(1)
.collect::<Vec<&str>>()
.pop()
.unwrap();

let asset =
format!(
r#"rustdoc::assets::Asset {{
name: "{name}",
contents: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/{path}")),
}}"#,
name = name,
path = self.path,
);

write!(f, "{}", asset)
}
}

/// Create source tests from the files in the `tests/source` directory.
fn generate_source_tests() -> Result<()> {
fn run() -> io::Result<()> {
let out_dir = env::var("OUT_DIR").unwrap();
let mut generated_code = File::create(Path::new(&out_dir).join("source_generated.rs"))?;

let source_dir = Path::new(SOURCE_TEST_DIR);
for source_file in fs::read_dir(&source_dir)? {
let source_file = source_file?.path();

let test_name = Ident::new(source_file
.file_stem()
.and_then(|stem| stem.to_str())
.ok_or_else(|| "Invalid source file stem")?);
let test_name = Ident::new(
source_file
.file_stem()
.and_then(|stem| stem.to_str())
.expect("invalid file stem"),
);

let source_file_path = source_file.to_str().unwrap();

Expand Down Expand Up @@ -178,8 +60,15 @@ fn generate_source_tests() -> Result<()> {
}
};

write!(generated_code, "{}", test.to_string())?;
write!(generated_code, "{}", test.as_str())?;
}

Ok(())
}

fn main() {
if let Err(ref e) = run() {
eprintln!("Error: {}", e);
process::exit(1);
}
}
15 changes: 15 additions & 0 deletions rustdoc-ember/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "rustdoc-ember"
version = "0.1.0"
authors = ["steveklabnik <steve@steveklabnik.com>"]
license = "MIT/Apache-2.0"

[dependencies]
clap = "2"
env_logger = "0.4"
lazy_static = "0.2"
log = "0.3"

[build-dependencies]
glob = "0.2"
quote = "0.3"
Loading