summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Hummer <jcbhmr@outlook.com>2024-02-26 09:56:19 -0600
committerGitHub <noreply@github.com>2024-02-26 15:56:19 +0000
commit7ed257a3c7d9aa69e9f127e78a918b862c4d4f74 (patch)
tree9edbd4b9089e91b6d583799ff543fe7fa60d18b5
parent85db05727b1cfab18480fd4296bd054cec786646 (diff)
Add basic typst-docs CLI that spits out json (#3429)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
-rw-r--r--Cargo.lock3
-rw-r--r--crates/typst-docs/Cargo.toml10
-rw-r--r--crates/typst-docs/src/main.rs148
3 files changed, 161 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a4669ba8..a1193fe0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2660,6 +2660,7 @@ dependencies = [
name = "typst-docs"
version = "0.10.0"
dependencies = [
+ "clap",
"comemo",
"ecow",
"heck",
@@ -2667,10 +2668,12 @@ dependencies = [
"once_cell",
"pulldown-cmark",
"serde",
+ "serde_json",
"serde_yaml 0.9.27",
"syntect",
"typed-arena",
"typst",
+ "typst-render",
"unicode_names2",
"unscanny",
"yaml-front-matter",
diff --git a/crates/typst-docs/Cargo.toml b/crates/typst-docs/Cargo.toml
index 7b444b9b..bb32aaf2 100644
--- a/crates/typst-docs/Cargo.toml
+++ b/crates/typst-docs/Cargo.toml
@@ -10,6 +10,13 @@ publish = false
doctest = false
bench = false
+[[bin]]
+name = "typst-docs"
+required-features = ["cli"]
+
+[features]
+cli = ["clap", "typst-render", "serde_json"]
+
[dependencies]
typst = { workspace = true }
comemo = { workspace = true }
@@ -25,6 +32,9 @@ typed-arena = { workspace = true }
unicode_names2 = { workspace = true }
unscanny = { workspace = true }
yaml-front-matter = { workspace = true }
+clap = { workspace = true, optional = true }
+typst-render = { workspace = true, optional = true }
+serde_json = { workspace = true, optional = true }
[lints]
workspace = true
diff --git a/crates/typst-docs/src/main.rs b/crates/typst-docs/src/main.rs
new file mode 100644
index 00000000..f4414b10
--- /dev/null
+++ b/crates/typst-docs/src/main.rs
@@ -0,0 +1,148 @@
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use clap::Parser;
+use typst::model::Document;
+use typst::visualize::Color;
+use typst_docs::{provide, Html, Resolver};
+use typst_render::render;
+
+#[derive(Debug)]
+struct CliResolver<'a> {
+ assets_dir: &'a Path,
+ verbose: bool,
+ base: &'a str,
+}
+
+impl<'a> Resolver for CliResolver<'a> {
+ fn commits(&self, from: &str, to: &str) -> Vec<typst_docs::Commit> {
+ if self.verbose {
+ eprintln!("commits({from}, {to})");
+ }
+ vec![]
+ }
+
+ fn example(
+ &self,
+ hash: u128,
+ source: Option<Html>,
+ document: &Document,
+ ) -> typst_docs::Html {
+ if self.verbose {
+ eprintln!(
+ "example(0x{hash:x}, {:?} chars, Document)",
+ source.as_ref().map(|s| s.as_str().len())
+ );
+ }
+
+ let frame = &document.pages.first().expect("page 0").frame;
+ let pixmap = render(frame, 2.0, Color::WHITE);
+ let filename = format!("{hash:x}.png");
+ let path = self.assets_dir.join(&filename);
+ fs::create_dir_all(path.parent().expect("parent")).expect("create dir");
+ pixmap.save_png(path.as_path()).expect("save png");
+ let src = format!("{}assets/{filename}", self.base);
+ eprintln!("Generated example image {path:?}");
+
+ if let Some(code) = source {
+ let code_safe = code.as_str();
+ Html::new(format!(
+ r#"<div class="previewed-code"><pre>{code_safe}</pre><div class="preview"><img src="{src}" alt="Preview" /></div></div>"#
+ ))
+ } else {
+ Html::new(format!(
+ r#"<div class="preview"><img src="{src}" alt="Preview" /></div>"#
+ ))
+ }
+ }
+
+ fn image(&self, filename: &str, data: &[u8]) -> String {
+ if self.verbose {
+ eprintln!("image({filename}, {} bytes)", data.len());
+ }
+
+ let path = self.assets_dir.join(filename);
+ fs::create_dir_all(path.parent().expect("parent")).expect("create dir");
+ fs::write(&path, data).expect("write image");
+ eprintln!("Created {} byte image at {path:?}", data.len());
+
+ format!("{}assets/{filename}", self.base)
+ }
+
+ fn link(&self, link: &str) -> Option<String> {
+ if self.verbose {
+ eprintln!("link({link})");
+ }
+ None
+ }
+
+ fn base(&self) -> &str {
+ self.base
+ }
+}
+
+/// Generates the JSON representation of the documentation. This can be used to
+/// generate the HTML yourself. Be warned: the JSON structure is not stable and
+/// may change at any time.
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+struct Args {
+ /// The generation process can produce additional assets. Namely images.
+ /// This option controls where to spit them out. The HTML generation will
+ /// assume that this output directory is served at `${base_url}/assets/*`.
+ /// The default is `assets`. For example, if the base URL is `/docs/` then
+ /// the gemerated HTML might look like `<img src="/docs/assets/foo.png">`
+ /// even though the `--assets-dir` was set to `/tmp/images` or something.
+ #[arg(long, default_value = "assets")]
+ assets_dir: PathBuf,
+
+ /// Write the JSON output to this file. The default is `-` which is a
+ /// special value that means "write to standard output". If you want to
+ /// write to a file named `-` then use `./-`.
+ #[arg(long, default_value = "-")]
+ out_file: PathBuf,
+
+ /// The base URL for the documentation. This can be an absolute URL like
+ /// `https://example.com/docs/` or a relative URL like `/docs/`. This is
+ /// used as the base URL for the generated page's `.route` properties as
+ /// well as cross-page links. The default is `/`. If a `/` trailing slash is
+ /// not present then it will be added. This option also affects the HTML
+ /// asset references. For example: `--base /docs/` will generate
+ /// `<img src="/docs/assets/foo.png">`.
+ #[arg(long, default_value = "/")]
+ base: String,
+
+ /// Enable verbose logging. This will print out all the calls to the
+ /// resolver and the paths of the generated assets.
+ #[arg(long)]
+ verbose: bool,
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let args = Args::parse();
+ let mut base = args.base.clone();
+ if !base.ends_with('/') {
+ base.push('/');
+ }
+
+ let resolver = CliResolver {
+ assets_dir: &args.assets_dir,
+ verbose: args.verbose,
+ base: &base,
+ };
+ if args.verbose {
+ eprintln!("resolver: {resolver:?}");
+ }
+ let pages = provide(&resolver);
+
+ eprintln!("Be warned: the JSON structure is not stable and may change at any time.");
+ let json = serde_json::to_string_pretty(&pages)?;
+
+ if args.out_file.to_string_lossy() == "-" {
+ println!("{json}");
+ } else {
+ fs::write(&args.out_file, &*json)?;
+ }
+
+ Ok(())
+}