33 Commits

Author SHA1 Message Date
1bb50c09bf elaborated on the usage 2025-11-22 18:45:21 +01:00
1995e5310d conditional workflow is activated again, also removed note at the top of the readme 2025-11-15 15:23:27 +01:00
379a337611 updated flake, workflow, and cargo lock/toml file 2025-11-14 12:36:07 +01:00
bc3283eb29 Merge pull request #11 from quirinecker/develop
Release 1.3
2025-11-14 11:27:50 +01:00
89c4d48c1c incremented version to 1.3 2025-11-14 11:22:11 +01:00
bf5782fc5e Merge pull request #10 from quirinecker/feature/1-specify-docs-dir
New config file that can be used
2025-11-13 12:38:25 +01:00
b0617efe93 adjusted tests to changes 2025-11-13 12:35:50 +01:00
f4d91aabe6 updated readme 2025-11-13 12:30:31 +01:00
904d9ccb28 added util binary for generating default config file. also adding generated config file 2025-11-13 12:24:02 +01:00
1f2d9e4363 made offline-reveal a global argument (easier for me)* 2025-11-13 09:35:36 +01:00
3ae1b2cc6e finished rewrite. custon folders work now 2025-11-13 09:24:46 +01:00
f6940f1db8 nearly finished rewrite 2025-11-13 09:10:08 +01:00
c33428f3e1 builder rewrite wip 2025-11-12 22:49:59 +01:00
fc98b48601 setting output and input is now global 2025-11-11 13:03:26 +01:00
bf30e8eeb6 updated readme 2025-11-10 17:58:20 +01:00
fa9f282194 updated some things like help text, fixing custom docs dir 2025-11-10 17:54:09 +01:00
64400c07c7 updated .gitignore with asciidocs directory 2025-11-10 17:30:47 +01:00
b3d6d21ea5 added asciidocs directory to the resources for testing 2025-11-10 17:30:15 +01:00
a0bdc34792 reverted test resource 2025-11-10 16:59:58 +01:00
659bb5da63 wip docs dir 2025-11-10 16:50:04 +01:00
24e1ade129 docs dir wip 2025-11-10 16:50:04 +01:00
88dc0e47b1 added config file for some options 2025-11-10 16:50:04 +01:00
0ae02c8950 Merge pull request #9 from quirinecker/fix/8-progress-jumps
Fix/8 progress jumps
2025-11-10 14:28:49 +01:00
e053f51bf3 removed old useless code for not building the reveal js things 2025-11-10 14:24:42 +01:00
f3bfd8c114 filtering out paths before building process 2025-11-10 14:23:15 +01:00
2ed789aa46 Merge pull request #7 from quirinecker/feature/2-when-ascidoctor-reveal-js-is-missing-it-should-say-so-instead-of-saying-asciidoctor-itself-is-missing
When ascidoctor reveal js is missing it should say so instead of saying asciidoctor itself is missing

Closes #2
2025-11-10 13:49:15 +01:00
131bf15240 now displaying missing binary instead of allways asciidoctro. Tested 2025-11-10 13:43:07 +01:00
fd1fe0d896 printing missing binary in the case something is missing. testing later 2025-11-09 22:11:19 +01:00
5996db8d80 Merge pull request #6 from quirinecker/4-make-serving-link-a-clickable-link
Serving link is now clickable
2025-11-09 22:05:32 +01:00
2df54930bb making it allways run on pull requests for now 2025-11-09 22:03:57 +01:00
f7a1f84730 removed colored library, using nu-ansi-term now 2025-11-09 21:54:22 +01:00
bb82dad4cf added paths to triggers 2025-11-09 21:09:37 +01:00
303309fff9 renamed existing deploy workflows. added testing workflow 2025-11-09 21:07:59 +01:00
36 changed files with 2510 additions and 1359 deletions

BIN
.github/assets/directory_structure.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

@@ -1,4 +1,4 @@
name: Docker
name: Deploy Docker Image to Github Container Registry
on:
push:

View File

@@ -1,10 +1,10 @@
name: Rust
name: Deploy Rust to Crates.io
on:
push:
branches: ["main"]
paths:
- "Cargo.toml"
- "./Cargo.toml"
env:
CARGO_TERM_COLOR: always
@@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
run: cargo build --release --bin docki
- name: Run tests
run: cargo test --verbose
- name: Publish to crates.io

24
.github/workflows/rust_test.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Test Rust
on:
push:
branches: ["main", "develop"]
paths:
- "./src/**"
- "./Cargo.toml"
pull_request:
branches: ["main", "develop"]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
/target
dist
/docs
res/test/docs/slides/revealjs
res/test/asciidocs/slides/revealjs
res/test/result
result

2977
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,26 @@
[package]
name = "docki"
version = "1.2.3"
version = "1.3.0"
edition = "2021"
description = "cli for building and publishing documentation using asciidoctor"
license-file = "LICENSE.txt"
authors = ["Quirin Ecker"]
exclude = [".gitlab", ".github"]
default-run = "docki"
[[bin]]
name = "docki"
path = "src/main.rs"
[[bin]]
name = "util"
path = "src/util.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bytes = "1.4.0"
clap = { version = "4.1.8", features = ["derive"] }
colored = "2.0.0"
futures = "0.3.26"
home = "0.5.4"
live-server = "0.6.0"
@@ -23,3 +31,7 @@ text_io = "0.1.12"
tokio = { version = "1.26.0", features = ["full"] }
zip-extract = "0.1.1"
clap_complete = "4.1.4"
nu-ansi-term = "0.50.3"
config = { version = "0.15.18", features = ["toml"] }
serde = "1.0.228"
toml = "0.9.6"

View File

@@ -1,9 +1,5 @@
# Docki
> [!NOTE]
> This project is under renovations. It can be used, but it might not be completely stable yet.
## Preview
![screencast](.github/assets/screencast.gif)
@@ -12,8 +8,15 @@ Docki is cli for converting asciidoctor files into html files.
## Usage
Docki must be run in a directory with the following structure
![docki directory structure](.github/assets/directory_structure.png)
1. Will be converted into asciidoc format
2. Will be converted into asciidocs slides
> [!NOTE]
> This works in a directory with a directory called `docs`, which contains the asciidoctor documentation.
> The input directory can be defined either with a cli argument or the config file. Default is `./docs`
### Building the documentation
@@ -21,7 +24,7 @@ Docki is cli for converting asciidoctor files into html files.
docki build
```
### Serving the documentation on a live server
### Serving the documentation with a live server
```shell
docki serve
@@ -112,6 +115,10 @@ home.packages = with pkgs; [
]
```
## Configuration
You can configure some of the cli arguments with a config file. It is always located relatively to the current directory under `./docki.config.toml`. Available options and their defaults can be found in the [docki.config.toml](config/docki.config.toml) file. CLI arguments with the same names as in the config usually have also the same default values. As a fallback you can also use the [config.rs](src/app/config/config.rs) file for reference.
## Development
### Running it

4
config/docki.config.toml Normal file
View File

@@ -0,0 +1,4 @@
port = 8080
input_dir = "./docs"
offline_reveal = false
output_dir = "./dist"

View File

@@ -64,6 +64,11 @@
libiconv
makeWrapper
];
binaries = [ "docki" ];
# only including the main binary in build
# There is no better option at the time of writing this
# https://github.com/nix-community/naersk/issues/127
copyBinsFilter = ''select(.reason == "compiler-artifact" and .executable != null and .profile.test == false and .target.name == "docki")'';
postInstall = ''
mkdir -p $out/share/bash-completion/completions
mkdir -p $out/share/zsh/site-functions

View File

@@ -0,0 +1,16 @@
= My Project
:toc:
:toc-title:
:toclevels: 2
== Introduction
This is my project.
== Slides
* link:slides/index.html[Presentation]
== License
This project is licensed under the MIT License - see the link:LICENSE[LICENSE] file for details.

View File

@@ -0,0 +1,19 @@
# Slides
## Slide 1
* First
* Second
* Third
## Slide 2
* First
* Second
* Third
### Slide 3
* First
* Second
* Third

View File

@@ -0,0 +1,3 @@
docs_dir = "./asciidocs"
port = 6969
offline_reveal = false

View File

View File

@@ -0,0 +1,16 @@
= My Project
:toc:
:toc-title:
:toclevels: 2
== Introduction
This is my project.
== Slides
* link:slides/index.html[Presentation]
== License
This project is licensed under the MIT License - see the link:LICENSE[LICENSE] file for details.

View File

@@ -1,11 +1,19 @@
= My cool presentation
:author: John Doe
:email: john@doe.com
# Slides
== First slide
## Slide 1
Content
* First
* Second
* Third
== Second slide
## Slide 2
Content
* First
* Second
* Third
### Slide 3
* First
* Second
* Third

View File

@@ -1,40 +0,0 @@
use clap::{Parser, Subcommand};
#[derive(Parser)]
pub struct Args {
#[command(subcommand)]
pub command: CommandArg,
}
#[derive(Subcommand)]
pub enum ShellArg {
Bash,
Fish,
Zsh,
}
#[derive(Subcommand)]
pub enum CommandArg {
/// Builds the documentation into a dist folder
Build {
/// When set to true, docki will download revealjs before building the documentation.
/// Otherwise it will use the cdn for revealjs
#[arg(short, long)]
offline_reveal: bool,
},
/// Checks if everything required for docki is installed
Health,
/// Deprecated: Helper command for installing asciidoctor-reveal-js
InstallReveal,
/// Starts a Webserver with the live preview of the Documentation
Serve {
/// Port for the Live Server
#[arg(short, long)]
port: Option<u16>,
},
/// Generates completions for the desired shell
Completions {
#[command(subcommand)]
shell: ShellArg,
},
}

View File

@@ -1,6 +1,8 @@
use regex::Regex;
use std::process;
use crate::app::config::config::Config;
fn exec_command(command: &mut process::Command) -> Result<(), String> {
let result = command.output();
@@ -12,9 +14,13 @@ fn exec_command(command: &mut process::Command) -> Result<(), String> {
}
} else {
println!("{}", result.unwrap_err());
return Err(
"asciidoctor not installed. For more information run docki health!".to_string(),
);
let binary_name = command.get_program().to_str().unwrap_or("Something is");
return Err(format!(
"{} not installed. For more information run docki health!",
binary_name
));
}
}
@@ -28,14 +34,17 @@ fn asciidoctor_docs(in_path: &str, out_path: &str) -> process::Command {
return command;
}
fn asciidoctor_slides(in_path: &str, out_path: &str, offline_reveal: bool) -> process::Command {
fn asciidoctor_slides(in_path: &str, out_path: &str, config: &Config) -> process::Command {
let mut command = process::Command::new(format!("asciidoctor-revealjs"));
let out_dir = parent_path(out_path);
let revealjs_path = if offline_reveal {
path_between(out_dir.to_string(), "./dist/slides/revealjs".to_string())
} else {
"https://cdn.jsdelivr.net/npm/reveal.js@5.2.1".to_string()
};
let revealjs_path = if config.offline_reveal {
path_between(
out_dir.to_string(),
format!("{}/slides/revealjs", config.output_dir),
)
} else {
"https://cdn.jsdelivr.net/npm/reveal.js@5.2.1".to_string()
};
command
.arg(format!("{in_path}"))
@@ -100,13 +109,13 @@ pub fn matching_from_start(from_segments: &Vec<String>, to_segments: &Vec<String
return from_segments.len();
}
pub fn build_doc(in_path: &str, out_path: &str) -> Result<(), String> {
pub fn build_doc(in_path: &str, out_path: &str, _: &Config) -> Result<(), String> {
let mut command = asciidoctor_docs(in_path, out_path);
return exec_command(&mut command);
}
pub fn build_slide(in_path: &str, out_path: &str, offline_reveal: bool) -> Result<(), String> {
let mut command = asciidoctor_slides(in_path, out_path, offline_reveal);
pub fn build_slide(in_path: &str, out_path: &str, config: &Config) -> Result<(), String> {
let mut command = asciidoctor_slides(in_path, out_path, config);
return exec_command(&mut command);
}

View File

@@ -1,54 +1,150 @@
use std::fs;
use self::asciidoctor::{build_doc, build_slide};
use super::fs_util;
pub mod asciidoctor;
pub fn docki_build(in_path: &str, offline_reveal: bool) -> DockiBuildResult {
let out_path = in_path.replace("/docs/", "/dist/");
let convert_out_path = out_path.replace(".adoc", ".html");
use std::{
fs, io::Cursor, path::PathBuf
};
if in_path.starts_with("./docs/slides/") && in_path.ends_with(".adoc") {
if let Err(err) = build_slide(&in_path, &convert_out_path, offline_reveal) {
return DockiBuildResult::Err(err);
use crate::app::{
build::{asciidoctor::build_slide, asciidoctor::build_doc}, config::config::Config, fs_util::{self, create_dir_recursive}, log::display_status
};
pub struct DockiBuilder<'a> {
progress: usize,
goal: usize,
config: &'a Config
}
impl <'a> DockiBuilder <'a> {
pub fn new(config: &'a Config) -> Self {
return Self {
progress: 0,
goal: 0,
config: config
};
}
///
/// Prepares everything for building the documentation
///
/// 1. Checks if the input directory exists and if not, returns an error
/// 2. When offline_reveal is set to true, it downloads revealjs. When it fails, it returns an error
pub async fn prepare(&self) -> Result<(), String> {
if !fs_util::directory_exists(&self.config.input_dir) {
return Err(
"docs directory does not exist it. Create it or use the template".to_string(),
);
}
DockiBuildResult::Slide(convert_out_path)
} else if in_path.ends_with(".adoc") {
if let Err(err) = build_doc(&in_path, &convert_out_path) {
return DockiBuildResult::Err(err);
}
DockiBuildResult::Doc(convert_out_path)
} else {
if in_path.starts_with("./docs/slides/revealjs") && !offline_reveal {
return DockiBuildResult::Silent;
if !self.config.offline_reveal {
return Ok(())
}
if let Err(err) = copy(&in_path, &out_path) {
return DockiBuildResult::Err(err);
let reveal_version = "5.2.1";
let target = format!("https://github.com/hakimel/reveal.js/archive/{reveal_version}.zip");
create_dir_recursive(format!("{}/slides", self.config.input_dir).as_str());
reqwest::get(target.clone()).await.unwrap();
let Ok(response) = reqwest::get(target).await else {
return Err("could not downlaod revealjs".to_string())
};
let Ok(bytes) = response.bytes().await else {
return Err("could not extract bytes".to_string())
};
let out = PathBuf::from(format!("{}/slides/revealjs", self.config.input_dir));
if zip_extract::extract(Cursor::new(bytes), &out, true).is_err() {
return Err("could not write extracted archive to disk".to_string());
}
DockiBuildResult::Copy(out_path)
}
}
fn copy(in_path: &str, out_path: &str) -> Result<(), String> {
fs_util::create_parent_dir_recursive(out_path);
if let Err(err) = fs::copy(in_path, out_path) {
return Err(err.to_string());
return Ok(());
}
/// Builds all files in the input directory with a pretty output
pub fn build_docs(&mut self) -> Result<(), String> {
let result = fs_util::fetch_paths_recursive(&self.config.input_dir);
let Ok(paths) = result else {
return Err(result.unwrap_err())
};
let reveal_dir = format!("{}/slides/revealjs", self.config.input_dir);
let paths = paths.into_iter()
.filter(|path| self.config.offline_reveal || !path.starts_with(reveal_dir.as_str()))
.collect::<Vec<String>>();
self.goal = paths.len();
for (index, in_path) in paths.iter().enumerate() {
self.progress = index + 1;
let result = self.build_file(&in_path);
match result {
DockiBuildResult::Err(err) => {
self.display_building_status("Error", &in_path, "");
println!("{}", err)
},
DockiBuildResult::Copy(out_path) => self.display_building_status("Copy", &in_path, &out_path),
DockiBuildResult::Slide(out_path) => self.display_building_status("Slide", &in_path, &out_path),
DockiBuildResult::Doc(out_path) => self.display_building_status("Doc", &in_path, &out_path),
}
}
return Ok(());
}
/// Builds a single file without a pretty output
pub fn build_file(&self, path: &str) -> DockiBuildResult {
let out_path = path.replace(&self.config.input_dir, &self.config.output_dir);
let convert_out_path = out_path.replace(".adoc", ".html");
if path.starts_with(format!("{}/slides/", &self.config.input_dir).as_str()) && path.ends_with(".adoc") {
if let Err(err) = build_slide(&path, &convert_out_path, self.config) {
return DockiBuildResult::Err(err);
}
DockiBuildResult::Slide(convert_out_path)
} else if path.ends_with(".adoc") {
if let Err(err) = build_doc(&path, &convert_out_path, self.config) {
return DockiBuildResult::Err(err);
}
DockiBuildResult::Doc(convert_out_path)
} else {
if let Err(err) = Self::copy(&path, &out_path) {
return DockiBuildResult::Err(err);
}
DockiBuildResult::Copy(out_path)
}
}
fn copy(in_path: &str, out_path: &str) -> Result<(), String> {
fs_util::create_parent_dir_recursive(out_path);
if let Err(err) = fs::copy(in_path, out_path) {
return Err(err.to_string());
}
Ok(())
}
fn display_building_status(&self, status_type: &str, in_path: &str, out_path: &str) -> () {
let progress_str = format!("{} / {}", self.progress, self.goal);
display_status(&progress_str, status_type, in_path, out_path);
}
Ok(())
}
/// Used for the Result of build_file. This way it is known what the builder did
pub enum DockiBuildResult {
Slide(String),
Doc(String),
Copy(String),
Err(String),
Silent,
}

View File

@@ -1,6 +1,10 @@
use super::executions::build_execution::BuildExecution;
use crate::app::config::config::Config;
pub async fn build(offline_reveal: bool) -> () {
let mut build_execution = BuildExecution::new();
build_execution.execute(offline_reveal).await.expect("build failed")
use crate::app::build::DockiBuilder;
pub async fn build(config: &Config) -> () {
let mut builder = DockiBuilder::new(config);
builder.prepare().await.expect("could not prepare for build");
builder.build_docs().expect("build failed")
}

View File

@@ -3,7 +3,7 @@ use std::io;
use clap::CommandFactory;
use clap_complete::{generate, shells::{Bash, Fish, Zsh}};
use crate::app::args::structure::{Args, ShellArg};
use crate::app::config::arguments::{Args, ShellArg};
pub fn completions(shell: ShellArg) {
let mut command = Args::command();

View File

@@ -1,101 +0,0 @@
use std::{
io::Cursor,
path::PathBuf
};
use crate::app::{
build::{docki_build, DockiBuildResult},
fs_util::{self, create_dir_recursive}, log::display_status,
};
pub struct BuildExecution {
progress: usize,
goal: usize,
}
impl BuildExecution {
pub fn new() -> Self {
return BuildExecution {
progress: 0,
goal: 0,
};
}
pub async fn execute(&mut self, offline_reveal: bool) -> Result<(), String> {
let path = "./docs/".to_string();
if !fs_util::directory_exists(&path) {
return Err(
"docs directory does not exist it. Create it or use the template".to_string(),
);
}
if let Err(error) = Self::prepare(offline_reveal).await {
return Err(error);
}
return self.build_dir(&path, offline_reveal);
}
async fn prepare(offline_reveal: bool) -> Result<(), String> {
if !offline_reveal {
return Ok(())
}
let reveal_version = "5.2.1";
let target = format!("https://github.com/hakimel/reveal.js/archive/{reveal_version}.zip");
create_dir_recursive("./docs/slides");
reqwest::get(target.clone()).await.unwrap();
let Ok(response) = reqwest::get(target).await else {
return Err("could not downlaod revealjs".to_string())
};
let Ok(bytes) = response.bytes().await else {
return Err("could not extract bytes".to_string())
};
let out = PathBuf::from("./docs/slides/revealjs");
if zip_extract::extract(Cursor::new(bytes), &out, true).is_err() {
return Err("could not write extracted archive to disk".to_string());
}
return Ok(());
}
fn build_dir(&mut self, path: &str, offline_reveal: bool) -> Result<(), String> {
let result = fs_util::fetch_paths_recursive(&path);
let Ok(paths) = result else {
return Err(result.unwrap_err())
};
for (index, in_path) in paths.iter().enumerate() {
self.progress = index + 1;
self.goal = paths.len();
let result = docki_build(&in_path, offline_reveal);
match result {
DockiBuildResult::Err(err) => {
self.display_building_status("Error", in_path, "");
println!("{}", err)
},
DockiBuildResult::Copy(out_path) => self.display_building_status("Copy", &in_path, &out_path),
DockiBuildResult::Slide(out_path) => self.display_building_status("Slide", &in_path, &out_path),
DockiBuildResult::Doc(out_path) => self.display_building_status("Doc", &in_path, &out_path),
DockiBuildResult::Silent => ()
}
}
return Ok(());
}
fn display_building_status(&self, status_type: &str, in_path: &str, out_path: &str) -> () {
let progress_str = format!("{} / {}", self.progress, self.goal);
display_status(&progress_str, status_type, in_path, out_path);
}
}

View File

@@ -1 +0,0 @@
pub mod build_execution;

View File

@@ -1,5 +1,5 @@
use std::{process::Command, io::ErrorKind};
use colored::Colorize;
use nu_ansi_term::Color::{LightGray, LightGreen, LightRed};
use std::{io::ErrorKind, process::Command};
const INFO_ASCIIDOC: &str = "
Install the binary with your package manager!
@@ -37,7 +37,7 @@ fn check_reveal() -> () {
}
fn reveal_is_installed() -> bool {
return check_command("asciidoctor-revealjs")
return check_command("asciidoctor-revealjs");
}
fn check_asciidoc() -> () {
@@ -49,23 +49,21 @@ fn check_asciidoc() -> () {
}
fn asciidoc_is_installed() -> bool {
return check_command("asciidoctor")
return check_command("asciidoctor");
}
fn check_command(command: &str) -> bool {
return match Command::new(command)
.output() {
return match Command::new(command).output() {
Ok(_) => true,
Err(e) => ErrorKind::NotFound != e.kind()
}
Err(e) => ErrorKind::NotFound != e.kind(),
};
}
fn print_health_ok(name: &str) {
println!("- ✔️ {}", name.bright_green());
println!("- ✔️ {}", LightGreen.paint(name));
}
fn print_health_not_ok(name: &str, info: &str) {
println!("- ❗{}", name.bright_red());
println!("{}", info.bright_black())
println!("- ❗{}", LightRed.paint(name));
println!("{}", LightGray.paint(info))
}

View File

@@ -1,6 +1,5 @@
pub mod build;
pub mod completions;
pub mod executions;
pub mod health;
pub mod install_reveal;
pub mod serve;

View File

@@ -1,4 +1,4 @@
use colored::Colorize;
use nu_ansi_term::Color::Green;
use futures::StreamExt;
use live_server::listen;
use notify::{
@@ -7,89 +7,115 @@ use notify::{
};
use std::{env, path::Path};
use crate::app::{ watcher::watcher, build::{docki_build, DockiBuildResult}, commands::build::build, log::display_status};
use crate::app::watcher::watcher;
use crate::app::log::display_status;
use crate::app::config::config::Config;
use crate::app::build::{DockiBuildResult, DockiBuilder};
pub async fn serve(port: Option<u16>) {
build(false).await;
tokio::join!(watch_and_build(), start_server(port));
pub async fn serve(config: &Config) {
let builder = DockiBuilder::new(config);
let mut server = Server::new(builder, config);
server.serve().await;
}
async fn watch_and_build() {
watch(Path::new("./docs"))
.await
.expect("something went wrong")
struct Server<'a> {
builder: DockiBuilder<'a>,
config: &'a Config,
}
async fn start_server(port: Option<u16>) {
let unwrapped_port = port.unwrap_or(8080);
println!(
"\nServing at {}{} ",
"http://localhost:".bold(),
unwrapped_port.to_string().bold()
);
impl <'a> Server <'a> {
fn new(builder: DockiBuilder<'a>, config: &'a Config) -> Self {
return Self {
builder,
config: config
}
}
let Ok(()) = listen("localhost", port.unwrap_or(8080), "./dist").await else {
panic!("could not start server")
};
async fn serve(&mut self) {
self.builder.prepare().await.expect("could not prepare for build");
self.builder.build_docs().expect("build failed");
tokio::join!(self.start_server(), self.watch_and_build());
}
async fn start_server(&self) {
let link = &format!("http://localhost:{}", self.config.port);
let hyperlink = Green.paint(link).hyperlink(link);
println!(
"\nServing at {}",
hyperlink
);
let Ok(()) = listen("localhost", self.config.port, self.config.output_dir.clone()).await else {
panic!("could not start server")
};
}
async fn watch_and_build(&self) {
self.watch()
.await
.expect("something went wrong")
}
async fn watch(&self) -> notify::Result<()> {
let path = Path::new(&self.config.input_dir);
let (mut watcher, mut rx) = watcher()?;
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
while let Some(res) = rx.next().await {
let event = res.expect("watching failed");
self.file_change(event)
}
Ok(())
}
fn file_change(&self, event: Event) {
match event.kind {
EventKind::Modify(ModifyKind::Data(_)) => self.build_valid_files(event.paths),
_ => (),
}
}
fn build_valid_files(&self, paths: Vec<std::path::PathBuf>) {
let invalid_path_message = "changed path is invalid";
let in_path = paths
.first()
.expect(invalid_path_message)
.strip_prefix(&Self::current_dir())
.expect(invalid_path_message)
.to_str()
.expect(invalid_path_message);
let in_path = format!("./{}", in_path);
let result = self.builder.build_file(&in_path);
match result {
DockiBuildResult::Slide(out_path) => Self::display_rebuilding_status("Slide", &in_path, &out_path),
DockiBuildResult::Doc(out_path) => Self::display_rebuilding_status("Doc", &in_path, &out_path),
DockiBuildResult::Copy(out_path) => Self::display_rebuilding_status("Copy", &in_path, &out_path),
DockiBuildResult::Err(err) => {
Self::display_rebuilding_status("Error", &in_path, "");
println!("{}", err);
},
}
}
fn display_rebuilding_status(context: &str, in_path: &str, out_path: &str) {
display_status("Rebuildng", context, in_path, out_path)
}
fn current_dir() -> String {
let err_message = "something went wrong";
return String::from(
env::current_dir()
.expect(err_message)
.to_str()
.expect(err_message),
);
}
}
async fn watch(path: &Path) -> notify::Result<()> {
let (mut watcher, mut rx) = watcher()?;
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
while let Some(res) = rx.next().await {
let event = res.expect("watching failed");
file_change(event)
}
Ok(())
}
fn file_change(event: Event) {
match event.kind {
EventKind::Modify(ModifyKind::Data(_)) => build_file(event.paths),
_ => (),
}
}
fn build_file(paths: Vec<std::path::PathBuf>) {
let invalid_path_message = "changed path is invalid";
let in_path = paths
.first()
.expect(invalid_path_message)
.strip_prefix(&current_dir())
.expect(invalid_path_message)
.to_str()
.expect(invalid_path_message);
let in_path = format!("./{}", in_path);
let result = docki_build(&in_path, false);
match result {
DockiBuildResult::Slide(out_path) => display_rebuilding_status("Slide", &in_path, &out_path),
DockiBuildResult::Doc(out_path) => display_rebuilding_status("Doc", &in_path, &out_path),
DockiBuildResult::Copy(out_path) => display_rebuilding_status("Copy", &in_path, &out_path),
DockiBuildResult::Err(err) => {
display_rebuilding_status("Error", &in_path, "");
println!("{}", err);
},
DockiBuildResult::Silent => ()
}
}
fn display_rebuilding_status(context: &str, in_path: &str, out_path: &str) {
display_status("Rebuildng", context, in_path, out_path)
}
fn current_dir() -> String {
let err_message = "something went wrong";
return String::from(
env::current_dir()
.expect(err_message)
.to_str()
.expect(err_message),
);
}

View File

@@ -0,0 +1,60 @@
use clap::{Parser, Subcommand};
use nu_ansi_term::{AnsiGenericString, Style};
fn github_hyperlink() -> AnsiGenericString<'static, str> {
return Style::new()
.bold()
.underline()
.paint("https://github.com/quirinecker/docki")
.hyperlink("https://github.com/quirinecker/docki");
}
#[derive(Parser)]
#[command(after_help = format!("More information like defaults can be found at {}", github_hyperlink()))]
pub struct Args {
#[command(subcommand)]
pub command: CommandArg,
/// The directory where the documentation is located
#[arg(short, long, global = true)]
pub input_dir: Option<String>,
/// The directory where the documentation will be built
#[arg(short, long, global = true)]
pub output_dir: Option<String>,
/// When set to true, docki will download revealjs before building the documentation.
/// Otherwise it will use the cdn for revealjs
#[arg(long, global = true)]
pub offline_reveal: bool,
}
#[derive(Subcommand)]
pub enum ShellArg {
Bash,
Fish,
Zsh,
}
#[derive(Subcommand)]
pub enum CommandArg {
/// Builds the documentation into the specified output_dir
Build,
/// Checks if everything required for docki is installed
Health,
/// Deprecated: Helper command for installing asciidoctor-reveal-js
InstallReveal,
/// Starts a Webserver with the live preview of the Documentation
Serve {
/// Port for the Live Server
#[arg(short, long)]
port: Option<u16>,
},
/// Generates completions for the desired shell
Completions {
#[command(subcommand)]
shell: ShellArg,
},
}
impl Args {}

44
src/app/config/config.rs Normal file
View File

@@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
use crate::app::config::arguments::CommandArg;
#[derive(Deserialize, Debug, Serialize)]
#[serde(default)]
pub struct Config {
pub port: u16,
pub input_dir: String,
pub offline_reveal: bool,
pub output_dir: String,
}
impl Config {
pub fn load() -> Result<Self, config::ConfigError> {
let s = config::Config::builder()
.add_source(config::File::with_name("./docki.config.toml"))
.build()?;
s.try_deserialize()
}
pub fn merge_with_args(self, args: &super::arguments::Args) -> Self {
Self {
port: match args.command {
CommandArg::Serve { port } => port.unwrap_or(self.port),
_ => self.port,
},
input_dir: args.input_dir.clone().unwrap_or(self.input_dir),
output_dir: args.output_dir.clone().unwrap_or(self.output_dir),
offline_reveal: args.offline_reveal || self.offline_reveal,
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
port: 8080,
input_dir: "./docs".to_string(),
output_dir: "./dist".to_string(),
offline_reveal: false,
}
}
}

View File

@@ -1,10 +1,10 @@
use clap::Parser;
use self::structure::Args;
use self::arguments::Args;
pub mod structure;
pub mod arguments;
pub mod config;
pub fn args() -> Args {
return Args::parse();
}

View File

@@ -1,20 +1,19 @@
use colored::Colorize;
use nu_ansi_term::Color::LightGreen;
use nu_ansi_term::Color::LightRed;
use nu_ansi_term::Style;
pub fn display_status(context1: &str, context2: &str, in_path: &str, out_path: &str) {
let colored_context = color_context(context2);
let colored_context = if context2 == "Error" {
LightRed.paint(context2)
} else {
LightGreen.paint(context2)
};
println!(
"({}) [{}] {} -> {}",
context1.bold(),
Style::new().paint(context1),
colored_context,
in_path,
out_path
);
}
fn color_context(context: &str) -> colored::ColoredString {
if context == "Error" {
return context.bright_red()
} else {
return context.bright_green()
}
}

View File

@@ -3,11 +3,13 @@ pub mod build;
pub mod fs_util;
pub mod watcher;
pub mod log;
mod args;
pub mod config;
use std::env;
use self::args::{args, structure::CommandArg};
use crate::app::config::config::Config;
use self::config::{args, arguments::CommandArg};
use self::commands::build::build;
use self::commands::completions::completions;
use self::commands::health::health;
@@ -20,13 +22,14 @@ impl App {
pub async fn start(&self) {
let args = args();
let config = Config::load().unwrap_or(Config::default()).merge_with_args(&args);
Self::setup_environment_variables();
match args.command {
CommandArg::Build { offline_reveal } => build(offline_reveal).await,
CommandArg::Build { .. } => build(&config).await,
CommandArg::Health => health(),
CommandArg::InstallReveal => install_reveal().await,
CommandArg::Serve { port } => serve(port).await,
CommandArg::Serve { .. } => serve(&config).await,
CommandArg::Completions { shell } => completions(shell)
};
}

View File

@@ -5,5 +5,5 @@ fn test_fetch_asciidoctor_paths_recursive() {
let paths = fs_util::fetch_paths_recursive("res/test/docs").unwrap();
let len = paths.len();
dbg!(paths);
assert_eq!(len, 6);
assert_eq!(len, 2);
}

34
src/util.rs Normal file
View File

@@ -0,0 +1,34 @@
mod app;
use std::{fs::File, io::Write};
use app::config::config::Config;
use clap::{Parser, Subcommand};
#[derive(Parser)]
pub struct Args {
#[command(subcommand)]
pub command: CommandArg,
}
#[derive(Subcommand)]
pub enum CommandArg {
/// Generates a default docki.config.toml
GenerateDefaultConfig,
}
fn main() {
let args = Args::parse();
match args.command {
CommandArg::GenerateDefaultConfig => generate_default_config(),
}
}
fn generate_default_config() {
let default_config = Config::default();
let target_file = "config/docki.config.toml";
let mut file = File::create(target_file).unwrap();
let output = toml::to_string_pretty(&default_config).unwrap();
file.write_all(output.as_bytes()).unwrap();
}