├── templates ├── tutorial.html ├── tutorial-index.html ├── function.html ├── struct.html ├── class.html ├── file.html ├── page.html ├── nav.html ├── head.html ├── default.css ├── themes.css ├── nav.css ├── content.css └── script.js ├── .gitignore ├── src ├── builder │ ├── mod.rs │ ├── function.rs │ ├── class.rs │ ├── struct_.rs │ ├── namespace.rs │ ├── files.rs │ ├── tutorial.rs │ ├── builder.rs │ ├── markdown.rs │ └── traits.rs ├── normalize.rs ├── lookahead.rs ├── html │ ├── process.rs │ └── mod.rs ├── annotation.rs ├── main.rs ├── cmake.rs ├── analyze.rs ├── config.rs └── url.rs ├── macros ├── Cargo.toml ├── LICENSE └── src │ └── lib.rs ├── Cargo.toml ├── .github └── workflows │ └── build.yml ├── README.md └── LICENSE /templates/tutorial.html: -------------------------------------------------------------------------------- 1 | 2 | {content} 3 | {links} 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | test/ 3 | output/ 4 | geode/ 5 | .vscode/ 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /templates/tutorial-index.html: -------------------------------------------------------------------------------- 1 | 2 |

{title}

3 |
4 | {links} 5 |
6 | -------------------------------------------------------------------------------- /templates/function.html: -------------------------------------------------------------------------------- 1 | 2 |

Function {name}

3 |
4 | {header_link} 5 |
6 |
7 | {description} 8 |
9 |
10 | {examples} 11 |
12 | -------------------------------------------------------------------------------- /src/builder/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::module_inception)] 2 | pub mod builder; 3 | pub mod class; 4 | pub mod comment; 5 | pub mod files; 6 | pub mod function; 7 | pub mod namespace; 8 | pub mod shared; 9 | pub mod struct_; 10 | pub mod tutorial; 11 | pub mod traits; 12 | pub mod markdown; 13 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flash-macros" 3 | version = "0.1.0" 4 | authors = ["HJfod"] 5 | description = "Macros for the Flash documentation generator" 6 | license-file = "LICENSE" 7 | 8 | [lib] 9 | proc-macro = true 10 | 11 | [dependencies] 12 | syn = "1.0" 13 | quote = "1.0" 14 | proc-macro2 = "1.0.47" 15 | convert_case = "0.1.0" 16 | -------------------------------------------------------------------------------- /templates/struct.html: -------------------------------------------------------------------------------- 1 | 2 |

Struct {name}

3 |
4 | {header_link} 5 |
6 |
7 | {description} 8 |
9 |
10 | {public_members} 11 | {examples} 12 | {public_static_functions} 13 | {public_member_functions} 14 |
15 | -------------------------------------------------------------------------------- /templates/class.html: -------------------------------------------------------------------------------- 1 | 2 |

Class {name}

3 |
4 | {header_link} 5 | {base_classes} 6 |
7 |
8 | {description} 9 |
10 |
11 | {examples} 12 | {public_static_functions} 13 | {public_member_functions} 14 | {public_members} 15 | {protected_member_functions} 16 | {protected_members} 17 |
18 | -------------------------------------------------------------------------------- /templates/file.html: -------------------------------------------------------------------------------- 1 | 2 |

File {name}

3 |
4 | 5 | 6 | #include <{file_path}> 7 | 8 | 9 |
10 |
11 | {description} 12 |
13 |
14 | {classes} 15 | {structs} 16 | {functions} 17 |
18 | -------------------------------------------------------------------------------- /src/normalize.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::path::{PathBuf, Component}; 3 | 4 | pub trait Normalize { 5 | fn normalize(&self) -> Self; 6 | } 7 | 8 | impl Normalize for PathBuf { 9 | fn normalize(&self) -> Self { 10 | let mut res = Self::new(); 11 | for comp in self.components() { 12 | if comp == Component::ParentDir { 13 | res.pop(); 14 | } 15 | else { 16 | res.push(comp); 17 | } 18 | } 19 | res 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /templates/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {head_content} 6 | 7 | 8 | 18 |
19 | {main_content} 20 |
21 |
22 | 23 | 24 | 25 |
26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flash" 3 | version = "0.2.10" 4 | edition = "2021" 5 | authors = ["HJfod"] 6 | description = "Documentation generator for C++" 7 | readme = "README.md" 8 | repository = "https://github.com/hjfod/flash" 9 | license-file = "LICENSE" 10 | keywords = ["documentation"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | clang = "1.0.2" 16 | clap = { version = "4.0.29", features = ["derive"] } 17 | glob = "0.3.0" 18 | indicatif = "0.17.2" 19 | serde = { version = "1.0.151", features = ["derive", "rc"] } 20 | serde_json = "1.0.91" 21 | shlex = "1.1.0" 22 | strfmt = "0.2.2" 23 | toml = "0.5.10" 24 | flash-macros = { path = "macros" } 25 | tokio = { version = "1.23.1", features = ["full"] } 26 | futures = "0.3.25" 27 | percent-encoding = "2.2.0" 28 | multipeek = "0.1.2" 29 | pulldown-cmark = { version = "0.9.2", git = "https://github.com/SergioBenitez/pulldown-cmark", branch = "cowstr-heading" } 30 | emojis = "0.5.2" 31 | cached = "0.42.0" 32 | serde_yaml = "0.9.17" 33 | swc = "0.245.5" 34 | swc_common = "0.29.28" 35 | anyhow = "1.0.68" 36 | minify-html = "0.10.8" 37 | lightningcss = "1.0.0-alpha.39" 38 | ico = "0.3.0" 39 | -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 6 | 9 | 12 |
13 | 16 | 19 | 22 | 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Binaries 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | config: 13 | - name: "Windows" 14 | os: windows-latest 15 | out_paths: './build/release/flash.exe' 16 | static: '' 17 | name: ${{ matrix.config.name }} 18 | runs-on: ${{ matrix.config.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | submodules: recursive 24 | 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: nightly 28 | 29 | - uses: Swatinem/rust-cache@v1 30 | with: 31 | key: ${{ matrix.config.name }} 32 | target-dir: ./build 33 | 34 | - name: Build 35 | run: | 36 | ${{ matrix.config.static }} 37 | cargo build --release --target-dir ${{ github.workspace }}/build 38 | 39 | - name: Move to output folder 40 | shell: bash 41 | working-directory: ${{ github.workspace }} 42 | run: | 43 | mkdir ./out 44 | mv ${{ matrix.config.out_paths }} ./out 45 | 46 | - name: Upload Artifacts 47 | uses: actions/upload-artifact@v2 48 | with: 49 | name: ${{ matrix.config.name }} Flash Binary 50 | path: ./out/ 51 | -------------------------------------------------------------------------------- /src/builder/function.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{html::Html, url::UrlPath}; 4 | use clang::Entity; 5 | 6 | use super::{ 7 | traits::{ASTEntry, BuildResult, EntityMethods, Entry, NavItem, OutputEntry}, 8 | builder::Builder, 9 | shared::output_entity, 10 | }; 11 | 12 | pub struct Function<'e> { 13 | entity: Entity<'e>, 14 | } 15 | 16 | impl<'e> Function<'e> { 17 | pub fn new(entity: Entity<'e>) -> Self { 18 | Self { entity } 19 | } 20 | } 21 | 22 | impl<'e> Entry<'e> for Function<'e> { 23 | fn name(&self) -> String { 24 | self.entity 25 | .get_name() 26 | .unwrap_or("`Anonymous function`".into()) 27 | } 28 | 29 | fn url(&self) -> UrlPath { 30 | self.entity.rel_docs_url().expect("Unable to get function URL") 31 | } 32 | 33 | fn build(&self, builder: &Builder<'e>) -> BuildResult { 34 | builder.create_output_for(self) 35 | } 36 | 37 | fn nav(&self) -> NavItem { 38 | NavItem::new_link(&self.name(), self.url(), Some(("code", true)), Vec::new()) 39 | } 40 | } 41 | 42 | impl<'e> ASTEntry<'e> for Function<'e> { 43 | fn entity(&self) -> &Entity<'e> { 44 | &self.entity 45 | } 46 | 47 | fn category(&self) -> &'static str { 48 | "function" 49 | } 50 | } 51 | 52 | impl<'e> OutputEntry<'e> for Function<'e> { 53 | fn output(&self, builder: &Builder<'e>) -> (Arc, Vec<(&'static str, Html)>) { 54 | ( 55 | builder.config.templates.function.clone(), 56 | output_entity(self, builder), 57 | ) 58 | } 59 | 60 | fn description(&self, builder: &'e Builder<'e>) -> String { 61 | self.output_description(builder) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/lookahead.rs: -------------------------------------------------------------------------------- 1 | 2 | // thanks https://stackoverflow.com/questions/74841526/why-does-stditerpeekablepeek-mutably-borrow-the-self-argument 3 | 4 | #![allow(unused)] 5 | 6 | pub struct CachedLookahead { 7 | iter: I, 8 | next_items: [Option; SIZE], 9 | } 10 | 11 | impl CachedLookahead { 12 | pub fn new(mut iter: I) -> Self { 13 | let mut next_items: [Option; SIZE] = [(); SIZE].map(|_| None); 14 | for i in 0..SIZE { 15 | next_items[i] = iter.next(); 16 | } 17 | Self { iter, next_items } 18 | } 19 | 20 | pub fn lookahead(&self) -> &[Option; SIZE] { 21 | &self.next_items 22 | } 23 | 24 | pub fn peek(&self) -> Option<&I::Item> { 25 | self.next_items[0].as_ref() 26 | } 27 | } 28 | 29 | impl Iterator for CachedLookahead { 30 | type Item = I::Item; 31 | 32 | fn next(&mut self) -> Option { 33 | self.next_items.rotate_left(1); 34 | std::mem::replace(&mut self.next_items[SIZE - 1], self.iter.next()) 35 | } 36 | } 37 | 38 | pub trait CreateCachedLookahead: Iterator + Sized { 39 | fn lookahead_cached(self) -> CachedLookahead; 40 | fn peekable_cached(self) -> CachedLookahead; 41 | } 42 | 43 | impl CreateCachedLookahead for pulldown_cmark::Parser<'_, '_> { 44 | fn lookahead_cached(self) -> CachedLookahead { 45 | CachedLookahead::new(self) 46 | } 47 | 48 | fn peekable_cached(self) -> CachedLookahead { 49 | CachedLookahead::new(self) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/builder/class.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{html::Html, url::UrlPath}; 4 | use clang::Entity; 5 | 6 | use super::{ 7 | builder::Builder, 8 | traits::{ASTEntry, BuildResult, EntityMethods, Entry, NavItem, OutputEntry, SubItem}, 9 | shared::output_classlike, 10 | }; 11 | 12 | pub struct Class<'e> { 13 | entity: Entity<'e>, 14 | } 15 | 16 | impl<'e> Class<'e> { 17 | pub fn new(entity: Entity<'e>) -> Self { 18 | Self { entity } 19 | } 20 | } 21 | 22 | impl<'e> Entry<'e> for Class<'e> { 23 | fn name(&self) -> String { 24 | self.entity 25 | .get_display_name() 26 | .unwrap_or("`Anonymous class`".into()) 27 | } 28 | 29 | fn url(&self) -> UrlPath { 30 | self.entity.rel_docs_url().expect("Unable to get class URL") 31 | } 32 | 33 | fn build(&self, builder: &Builder<'e>) -> BuildResult { 34 | builder.create_output_for(self) 35 | } 36 | 37 | fn nav(&self) -> NavItem { 38 | NavItem::new_link( 39 | &self.name(), self.url(), Some(("box", false)), 40 | SubItem::for_classlike(&self.entity) 41 | ) 42 | } 43 | } 44 | 45 | impl<'e> ASTEntry<'e> for Class<'e> { 46 | fn entity(&self) -> &Entity<'e> { 47 | &self.entity 48 | } 49 | 50 | fn category(&self) -> &'static str { 51 | "class" 52 | } 53 | } 54 | 55 | impl<'e> OutputEntry<'e> for Class<'e> { 56 | fn output(&self, builder: &Builder<'e>) -> (Arc, Vec<(&'static str, Html)>) { 57 | ( 58 | builder.config.templates.class.clone(), 59 | output_classlike(self, builder), 60 | ) 61 | } 62 | 63 | fn description(&self, builder: &'e Builder<'e>) -> String { 64 | self.output_description(builder) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/builder/struct_.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::sync::Arc; 3 | use crate::{html::Html, url::UrlPath}; 4 | use clang::Entity; 5 | use super::{ 6 | traits::{ASTEntry, BuildResult, EntityMethods, Entry, NavItem, OutputEntry, SubItem}, 7 | builder::Builder, 8 | shared::output_classlike, 9 | }; 10 | 11 | pub struct Struct<'e> { 12 | entity: Entity<'e>, 13 | } 14 | 15 | impl<'e> Struct<'e> { 16 | pub fn new(entity: Entity<'e>) -> Self { 17 | Self { entity } 18 | } 19 | } 20 | 21 | impl<'e> Entry<'e> for Struct<'e> { 22 | fn name(&self) -> String { 23 | self.entity 24 | .get_display_name() 25 | .unwrap_or("`Anonymous struct`".into()) 26 | } 27 | 28 | fn url(&self) -> UrlPath { 29 | self.entity.rel_docs_url().expect("Unable to get struct URL") 30 | } 31 | 32 | fn build(&self, builder: &Builder<'e>) -> BuildResult { 33 | builder.create_output_for(self) 34 | } 35 | 36 | fn nav(&self) -> NavItem { 37 | NavItem::new_link( 38 | &self.name(), self.url(), Some(("box", true)), 39 | SubItem::for_classlike(&self.entity) 40 | ) 41 | } 42 | } 43 | 44 | impl<'e> ASTEntry<'e> for Struct<'e> { 45 | fn entity(&self) -> &Entity<'e> { 46 | &self.entity 47 | } 48 | 49 | fn category(&self) -> &'static str { 50 | "struct" 51 | } 52 | } 53 | 54 | impl<'e> OutputEntry<'e> for Struct<'e> { 55 | fn output(&self, builder: &Builder<'e>) -> (Arc, Vec<(&'static str, Html)>) { 56 | ( 57 | builder.config.templates.struct_.clone(), 58 | output_classlike(self, builder), 59 | ) 60 | } 61 | 62 | fn description(&self, builder: &'e Builder<'e>) -> String { 63 | self.output_description(builder) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /templates/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | {page_title} 34 | -------------------------------------------------------------------------------- /src/html/process.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::sync::Arc; 3 | 4 | use lightningcss::stylesheet::{ParserOptions, PrinterOptions}; 5 | use swc::{try_with_handler, HandlerOpts, config::{JsMinifyOptions, Options}, BoolOrDataConfig}; 6 | use swc_common::{SourceMap, GLOBALS, FileName}; 7 | 8 | pub fn minify_html(input: String) -> Result { 9 | String::from_utf8(minify_html::minify( 10 | input.as_bytes(), 11 | &minify_html::Cfg { 12 | keep_closing_tags: true, 13 | ..Default::default() 14 | } 15 | )).map_err(|e| format!("{e}")) 16 | } 17 | 18 | pub fn minify_js(input: String) -> Result { 19 | // minify 20 | let cm = Arc::::default(); 21 | let c = swc::Compiler::new(cm.clone()); 22 | 23 | GLOBALS.set(&Default::default(), || { 24 | try_with_handler( 25 | cm.clone(), 26 | HandlerOpts { 27 | ..Default::default() 28 | }, 29 | |handler| { 30 | let mut fm = cm.new_source_file(FileName::Anon, input); 31 | let output = c.process_js_file( 32 | fm.clone(), 33 | handler, 34 | &Options { 35 | ..Default::default() 36 | } 37 | )?; 38 | // idk if there's a better way to do this lol 39 | fm = cm.new_source_file(FileName::Anon, output.code); 40 | c.minify( 41 | fm, 42 | handler, 43 | &JsMinifyOptions { 44 | compress: BoolOrDataConfig::from_bool(true), 45 | mangle: BoolOrDataConfig::from_bool(true), 46 | ..Default::default() 47 | }, 48 | ) 49 | } 50 | ) 51 | }) 52 | .map(|o| o.code) 53 | .map_err(|e| format!("{e}")) 54 | } 55 | 56 | pub fn minify_css(input: String) -> Result { 57 | let sheet = lightningcss::stylesheet::StyleSheet::parse( 58 | &input, ParserOptions::default() 59 | ).map_err(|e| format!("{e}"))?; 60 | sheet.to_css(PrinterOptions { 61 | minify: true, 62 | ..PrinterOptions::default() 63 | }).map(|s| s.code).map_err(|e| format!("{e}")) 64 | } 65 | -------------------------------------------------------------------------------- /templates/default.css: -------------------------------------------------------------------------------- 1 | 2 | html { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | height: 100%; 10 | background: var(--flash-body-bg); 11 | color: var(--flash-white); 12 | display: grid; 13 | grid-template-columns: minmax(1rem, 26.5rem) 1fr; 14 | grid-template-rows: 100% 100%; 15 | } 16 | 17 | @media only screen and (max-device-width: 250px) { 18 | body > .overlay.theme { 19 | display: none; 20 | } 21 | } 22 | 23 | @media only screen and (max-device-width: 800px) { 24 | body { 25 | grid-template-columns: 1fr; 26 | } 27 | 28 | nav { 29 | position: absolute; 30 | width: 100%; 31 | transition: opacity .25s; 32 | } 33 | 34 | nav.collapsed { 35 | opacity: 0%; 36 | pointer-events: none; 37 | } 38 | 39 | nav:not(.collapsed) { 40 | opacity: 100%; 41 | } 42 | 43 | nav:not(.collapsed) ~ .overlay.theme { 44 | display: none; 45 | } 46 | 47 | body > .overlay.menu { 48 | display: block; 49 | } 50 | } 51 | 52 | @media only screen and (min-device-width: 800px) and (max-device-width: 1100px) { 53 | body { 54 | grid-template-columns: 1fr; 55 | } 56 | 57 | nav { 58 | position: absolute; 59 | width: 30rem; 60 | transition: left .25s; 61 | } 62 | 63 | nav:not(.collapsed) { 64 | left: 0rem; 65 | box-shadow: .25rem 0rem .5rem var(--flash-shadow); 66 | } 67 | 68 | nav.collapsed { 69 | left: -30rem; 70 | } 71 | 72 | body > .overlay.menu { 73 | display: block; 74 | } 75 | } 76 | 77 | @media only screen and (min-device-width: 1100px) { 78 | body > .overlay.menu { 79 | display: none; 80 | } 81 | } 82 | 83 | body > .overlay.menu { 84 | left: 1rem; 85 | } 86 | 87 | body > .overlay.theme { 88 | right: 1rem; 89 | } 90 | 91 | body > .overlay { 92 | top: 1rem; 93 | position: absolute; 94 | display: flex; 95 | flex-direction: row; 96 | border-radius: 9999px; 97 | box-shadow: 0 .25rem .5rem var(--flash-shadow); 98 | z-index: 4; 99 | } 100 | 101 | body > .overlay > button { 102 | background-color: var(--flash-dark); 103 | color: var(--flash-light); 104 | border: none; 105 | padding: .35rem; 106 | padding-left: 1rem; 107 | padding-right: 1rem; 108 | } 109 | 110 | body > .overlay > button:first-child { 111 | border-top-left-radius: 9999px; 112 | border-bottom-left-radius: 9999px; 113 | } 114 | 115 | body > .overlay > button:last-child { 116 | border-top-right-radius: 9999px; 117 | border-bottom-right-radius: 9999px; 118 | } 119 | 120 | body > .overlay > button:hover:not(.selected) { 121 | background-color: var(--flash-less-dark); 122 | cursor: pointer; 123 | } 124 | 125 | body > .overlay > button.selected { 126 | background-color: var(--flash-tab-selected-bg); 127 | color: var(--flash-tab-selected-color); 128 | } 129 | 130 | body > .overlay > button > .feather { 131 | height: 1.25rem; 132 | } 133 | 134 | .version { 135 | font-weight: normal; 136 | color: var(--flash-light); 137 | margin-left: .25rem; 138 | } 139 | 140 | .stretch-apart { 141 | display: flex; 142 | flex-direction: row; 143 | justify-content: space-between; 144 | align-items: baseline; 145 | } 146 | -------------------------------------------------------------------------------- /src/annotation.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::ops::Range; 3 | 4 | struct Annotation { 5 | raw: String, 6 | range: Range, 7 | value: Option, 8 | } 9 | 10 | pub struct Annotations<'a> { 11 | next_in_iter: usize, 12 | raw: &'a str, 13 | /// These better be in sorted order by range or shit will break bad! 14 | annotations: Vec, 15 | } 16 | 17 | impl<'a> Annotations<'a> { 18 | pub fn new(raw: &'a str) -> Self { 19 | Self { 20 | raw, 21 | next_in_iter: 0, 22 | annotations: Self::create_annotations(raw), 23 | } 24 | } 25 | 26 | pub fn into_result(self) -> String { 27 | let mut result = String::from(self.raw); 28 | let mut offset = 0isize; 29 | for word in self.annotations { 30 | if let Some(value) = word.value { 31 | result.replace_range( 32 | (word.range.start as isize + offset) as usize 33 | ..(word.range.end as isize + offset) as usize, 34 | &value 35 | ); 36 | // Applying this annotation may cause the next annotations to 37 | // shifted if the replaced string is shorter / longer than the 38 | // original 39 | offset += value.len() as isize - word.raw.len() as isize; 40 | } 41 | } 42 | result 43 | } 44 | 45 | pub fn rewind(&mut self) { 46 | self.next_in_iter = 0; 47 | } 48 | 49 | pub fn next(&mut self) -> Option { 50 | self.annotations.iter() 51 | .skip(self.next_in_iter) 52 | .skip_while(|a| { 53 | if a.value.is_some() { 54 | self.next_in_iter += 1; 55 | true 56 | } 57 | else { 58 | false 59 | } 60 | }) 61 | .next() 62 | .inspect(|_| self.next_in_iter += 1) 63 | .map(|a| a.raw.clone()) 64 | } 65 | 66 | pub fn annotate(&mut self, value: String) { 67 | self.annotations.get_mut(self.next_in_iter - 1).unwrap().value = Some(value); 68 | } 69 | 70 | fn skip_to_next_word(raw: &'a str, iter_ix: &mut usize) { 71 | while let Some(i) = raw.chars().nth(*iter_ix) && !i.is_alphanumeric() { 72 | *iter_ix += 1; 73 | } 74 | } 75 | 76 | fn next_word(raw: &'a str, iter_ix: &mut usize) -> Option<(Range, String)> { 77 | let start = *iter_ix; 78 | let res: String = raw.chars() 79 | .skip(*iter_ix) 80 | .take_while(|c| c.is_alphanumeric()) 81 | .collect(); 82 | *iter_ix += res.len(); 83 | let end = *iter_ix; 84 | (!res.is_empty()).then_some((start..end, res)) 85 | } 86 | 87 | fn next_annotation(raw: &'a str, iter_ix: &mut usize) -> Option { 88 | Self::skip_to_next_word(raw, iter_ix); 89 | let word = Self::next_word(raw, iter_ix)?; 90 | let (range, word) = word; 91 | Some(Annotation { 92 | raw: word.clone(), 93 | range, 94 | value: None 95 | }) 96 | } 97 | 98 | fn create_annotations(raw: &'a str) -> Vec { 99 | let mut res = Vec::new(); 100 | let mut iter_ix = 0; 101 | while let Some(a) = Self::next_annotation(raw, &mut iter_ix) { 102 | res.push(a); 103 | } 104 | res 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(let_chains)] 2 | #![feature(is_some_and)] 3 | #![feature(result_option_inspect)] 4 | #![feature(iter_advance_by)] 5 | #![feature(iter_intersperse)] 6 | 7 | use crate::{analyze::create_docs, url::UrlPath, normalize::Normalize}; 8 | use clap::Parser; 9 | use config::Config; 10 | use std::{fs, path::{PathBuf, Path}, process::exit, io, time::Instant}; 11 | 12 | mod analyze; 13 | mod builder; 14 | mod cmake; 15 | mod config; 16 | mod html; 17 | mod url; 18 | mod normalize; 19 | mod annotation; 20 | mod lookahead; 21 | 22 | #[derive(Parser, Debug)] 23 | #[command(name("Flash"), version, about)] 24 | struct Args { 25 | /// Input directory with the flash.json file 26 | #[arg(short, long)] 27 | input: PathBuf, 28 | 29 | /// Output directory where to place the generated docs 30 | #[arg(short, long)] 31 | output: PathBuf, 32 | 33 | /// Whether to overwrite output directory if it already exists 34 | #[arg(long, default_value_t = false)] 35 | overwrite: bool, 36 | } 37 | 38 | fn remove_dir_contents>(path: P) -> io::Result<()> { 39 | for entry in fs::read_dir(path)? { 40 | let entry = entry?; 41 | let path = entry.path(); 42 | 43 | if entry.file_type()?.is_dir() { 44 | remove_dir_contents(&path)?; 45 | fs::remove_dir(path)?; 46 | } else { 47 | fs::remove_file(path)?; 48 | } 49 | } 50 | Ok(()) 51 | } 52 | 53 | #[tokio::main] 54 | async fn main() -> Result<(), String> { 55 | let args = Args::parse(); 56 | 57 | // Check if output dir exists 58 | if args.output.exists() 59 | // Check if it's empty 60 | && args.output.read_dir().map(|mut i| i.next().is_some()).unwrap_or(false) 61 | // Then overwrite must be specified 62 | && !args.overwrite 63 | { 64 | println!( 65 | "Output directory {} already exists and no --overwrite option was specified, aborting", 66 | args.output.to_str().unwrap() 67 | ); 68 | exit(1); 69 | } 70 | 71 | // Clear output dir if it exists 72 | if args.output.exists() { 73 | remove_dir_contents(&args.output).unwrap(); 74 | } 75 | else { 76 | fs::create_dir_all(&args.output).unwrap(); 77 | } 78 | 79 | let relative_output = if args.output.is_relative() { 80 | Some(UrlPath::try_from(&args.output).ok()).flatten() 81 | } else { 82 | None 83 | }; 84 | 85 | // Relink working directory to input dir and use absolute path for output 86 | // Not using fs::canonicalize because that returns UNC paths on Windows and 87 | // those break things 88 | let full_output = if args.output.is_absolute() { 89 | args.output 90 | } else { 91 | std::env::current_dir().unwrap().join(args.output).normalize() 92 | }; 93 | let full_input = if args.input.is_absolute() { 94 | args.input 95 | } else { 96 | std::env::current_dir().unwrap().join(args.input).normalize() 97 | }; 98 | std::env::set_current_dir(&full_input).expect( 99 | "Unable to set input dir as working directory \ 100 | (probable reason is it doesn't exist)", 101 | ); 102 | 103 | // Parse config 104 | let conf = Config::parse(full_input, full_output, relative_output)?; 105 | 106 | // Build the docs 107 | println!( 108 | "Building docs for {} ({})", 109 | conf.project.name, conf.project.version 110 | ); 111 | let now = Instant::now(); 112 | create_docs(conf.clone()).await?; 113 | println!("Docs built for {} in {}s", conf.project.name, now.elapsed().as_secs()); 114 | 115 | Ok(()) 116 | } 117 | -------------------------------------------------------------------------------- /src/cmake.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::{fs, path::PathBuf, process::Command, sync::Arc}; 3 | 4 | use crate::config::Config; 5 | 6 | #[derive(Deserialize, Clone)] 7 | pub struct CompileCommand { 8 | pub directory: PathBuf, 9 | pub command: String, 10 | pub file: PathBuf, 11 | } 12 | 13 | impl CompileCommand { 14 | pub fn get_command_list(&self, config: Arc) -> Vec { 15 | // Not using shlex because that screws up -DFMT_CONSTEVAL=\"\" 16 | let mut list: Vec = self.command.split(' ') 17 | // Skip clang.exe 18 | .skip(1) 19 | .flat_map(|s| 20 | // Expand .rsp files into their include directives 21 | // For some reason LibClang just doesn't want to work with the .rsp 22 | // files so got to do this 23 | if s.ends_with(".rsp") { 24 | fs::read_to_string( 25 | self.directory.join(s.replace('@', "")) 26 | ).expect("Unable to read compiler .rsp includes file") 27 | .split(' ') 28 | .map(|s| s.to_owned()) 29 | .collect() 30 | } else { 31 | // Hacky fix to make sure -DMACRO="" defines MACRO as empty and not as "" 32 | vec![s.to_owned().replace("=\"\"", "=")] 33 | } 34 | ) 35 | // Add header root to include directories 36 | .chain(vec![format!("-I{}", config.input_dir.to_str().unwrap())]) 37 | // Set working directory 38 | .chain(vec![format!("-working-directory={}", self.directory.to_str().unwrap())]) 39 | // Add extra compile args 40 | .chain(config.analysis.compile_args.clone()) 41 | .collect(); 42 | 43 | // Passing -c crashes LibClang 44 | while let Some(ix) = list.iter().position(|s| s == "-c") { 45 | list.drain(ix..ix + 2); 46 | } 47 | 48 | list 49 | } 50 | } 51 | 52 | type CompileCommands = Vec; 53 | 54 | pub fn cmake_configure(build_dir: &str, args: &Vec) -> Result<(), String> { 55 | Command::new("cmake") 56 | .arg(".") 57 | .args(["-B", build_dir]) 58 | .args(args) 59 | .spawn() 60 | .map_err(|e| format!("Error configuring CMake: {e}"))? 61 | .wait() 62 | .unwrap() 63 | .success() 64 | .then_some(()) 65 | .ok_or("CMake configure failed".into()) 66 | } 67 | 68 | pub fn cmake_build(build_dir: &str, args: &Vec) -> Result<(), String> { 69 | Command::new("cmake") 70 | .args(["--build", build_dir]) 71 | .args(args) 72 | .spawn() 73 | .map_err(|e| format!("Error building CMake: {e}"))? 74 | .wait() 75 | .unwrap() 76 | .success() 77 | .then_some(()) 78 | .ok_or("CMake build failed".into()) 79 | } 80 | 81 | pub fn cmake_compile_commands(config: Arc) -> Result { 82 | serde_json::from_str( 83 | &fs::read_to_string( 84 | config 85 | .input_dir 86 | .join(&config.cmake.as_ref().unwrap().build_dir) 87 | .join("compile_commands.json"), 88 | ) 89 | .map_err(|e| format!("Unable to read compile_commands.json: {e}"))?, 90 | ) 91 | .map_err(|e| format!("Unable to parse compile_commands.json: {e}")) 92 | } 93 | 94 | pub fn cmake_compile_args_for(config: Arc) -> Result, String> { 95 | let from = &config.cmake.as_ref() 96 | .ok_or(String::from("Project does not use CMake"))? 97 | .infer_args_from; 98 | for cmd in cmake_compile_commands(config.clone())? { 99 | if cmd.file == config.input_dir.join(from) { 100 | return Ok(cmd.get_command_list(config)); 101 | } 102 | } 103 | Err(format!("Unable to find compile args for '{}'", config.input_dir.join(from).to_string_lossy())) 104 | } 105 | -------------------------------------------------------------------------------- /src/analyze.rs: -------------------------------------------------------------------------------- 1 | use crate::{builder::builder::Builder, cmake, config::Config}; 2 | use indicatif::{ProgressBar, ProgressStyle}; 3 | use std::{fs, path::PathBuf, process::Command, sync::Arc, time::Duration}; 4 | 5 | fn run_command(cmd: &String) -> Result<(), String> { 6 | let args = 7 | shlex::split(cmd).unwrap_or_else(|| panic!("Unable to parse prebuild command `{cmd}`")); 8 | let exit = Command::new(&args[0]) 9 | .args(&args[1..]) 10 | .spawn() 11 | .map_err(|e| format!("Unable to execute prebuild command `{cmd}`: {e}"))? 12 | .wait() 13 | .unwrap(); 14 | if exit.success() { 15 | Ok(()) 16 | } else { 17 | Err(format!("Prebuild command `{cmd}` failed")) 18 | } 19 | } 20 | 21 | fn create_analyzable_file(config: Arc) -> Result { 22 | let out_path = config.output_dir.join("_analyze.cpp"); 23 | 24 | let mut data = String::from( 25 | "// File generated by Flash for including all headers in order to\n\ 26 | // parse them\n", 27 | ); 28 | for hdr in &config.all_includes() { 29 | data += &format!("#include <{}>\n", hdr.to_str().unwrap()); 30 | } 31 | fs::write(&out_path, data) 32 | .map_err(|e| format!("Unable to create source file for parsing headers: {e}"))?; 33 | 34 | Ok(out_path) 35 | } 36 | 37 | async fn analyze_with_clang(config: Arc, args: &[String]) -> Result<(), String> { 38 | // Initialize clang 39 | let clang = clang::Clang::new()?; 40 | let index = clang::Index::new(&clang, false, true); 41 | 42 | // Create a single source file that includes all headers 43 | let target_src = create_analyzable_file(config.clone())?; 44 | 45 | let pbar = Arc::from(ProgressBar::new_spinner()); 46 | pbar.set_style( 47 | ProgressStyle::with_template("{msg:>15} {spinner} [{elapsed_precise}]") 48 | .unwrap() 49 | .tick_strings(&[ 50 | "█░░░░░░", 51 | "██░░░░░", 52 | "███░░░░", 53 | "░███░░░", 54 | "░░███░░", 55 | "░░░███░", 56 | "░░░░███", 57 | "░░░░░██", 58 | "░░░░░░█", 59 | "░░░░░░░", 60 | "░░░░░░░", 61 | ]), 62 | ); 63 | pbar.set_message("Analyzing"); 64 | pbar.enable_steady_tick(Duration::from_millis(50)); 65 | 66 | // Create parser 67 | let unit = index.parser(&target_src).arguments(args).parse()?; 68 | 69 | // Build the navbar first 70 | pbar.set_message("Setting up"); 71 | let builder = Builder::new(config, unit.get_entity(), &clang, &index, args)?; 72 | 73 | // Build the doc files 74 | pbar.set_message("Building docs"); 75 | builder.build(Some(pbar.clone())).await?; 76 | 77 | pbar.set_message("Cleaning up files"); 78 | 79 | // Clean up analyzable file 80 | fs::remove_file(target_src).unwrap(); 81 | 82 | pbar.finish_using_style(); 83 | 84 | Ok(()) 85 | } 86 | 87 | async fn analyze_with_cmake(config: Arc) -> Result<(), String> { 88 | // Configure the cmake project 89 | cmake::cmake_configure( 90 | &config.cmake.as_ref().unwrap().build_dir, 91 | &config 92 | .cmake 93 | .as_ref() 94 | .unwrap() 95 | .config_args, 96 | )?; 97 | 98 | // Build the cmake project 99 | if config.cmake.as_ref().unwrap().build { 100 | cmake::cmake_build( 101 | &config.cmake.as_ref().unwrap().build_dir, 102 | &config 103 | .cmake 104 | .as_ref() 105 | .unwrap() 106 | .build_args, 107 | )?; 108 | } 109 | 110 | analyze_with_clang( 111 | config.clone(), 112 | &cmake::cmake_compile_args_for(config).expect("Unable to infer CMake compile args"), 113 | ) 114 | .await?; 115 | 116 | Ok(()) 117 | } 118 | 119 | pub async fn create_docs(config: Arc) -> Result<(), String> { 120 | // Execute prebuild commands 121 | if let Some(cmds) = config.run.as_ref().map(|c| &c.prebuild) { 122 | for cmd in cmds { 123 | run_command(cmd)?; 124 | } 125 | } 126 | 127 | // Build based on mode 128 | if config.cmake.is_some() { 129 | analyze_with_cmake(config).await 130 | } 131 | // Build with extra compile args only 132 | else { 133 | analyze_with_clang(config.clone(), &config.analysis.compile_args).await 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :zap: Flash 2 | 3 | A simple tool for generating beautiful documentation for C++. 4 | 5 | Built for projects that use CMake and host their docs on GitHub Pages. 6 | 7 | :rocket: Decently fast (~30s to build docs for [Geode](https://github.com/geode-sdk/geode)) 8 | 9 | :rocket: Beautiful, easily legible output 10 | 11 | :rocket: Opinionated with minimal configuration required (no 400-line Doxyfiles required) 12 | 13 | ## :question: Why? 14 | 15 | Because I tried Doxygen for five seconds and found its output way too bloated and way too ugly. 16 | 17 | The goals of this project is to generate documentation that looks beautiful and is super easy to navigate. I also wanted to make just browsing the docs intuitive and simple to encourage learning about what tools are available before you find an usecase for them. 18 | 19 | ## :star: Live example 20 | 21 | The docs for [Geode](https://github.com/geode-sdk/geode) have been built with Flash: . 22 | 23 | ## :point_right: Usage 24 | 25 | Flash can be compiled using `cargo build` as usual for Rust projects. 26 | 27 | Running Flash requires the following command line arguments: `flash -i -o [--overwrite]` 28 | 29 | `input_dir` points to a directory with the project you want to generate docs for, and `output_dir` is where to place the generated documentation pages. Unless `--overwrite` is specified, `output_dir` must not exist prior to running Flash. 30 | 31 | > :warning: `output_dir` should be a relative path, or bad things may happen with the links on the docs page. 32 | 33 | > :warning: The output directory should be the same relative root path as where the docs will eventually live, so for example doing `-o docs` means that the docs root URL on the website should be `site.com/docs`. 34 | 35 | Configuring Flash happens through a `flash.toml` file at the root of the project. 36 | 37 | | Key | Required | Default | Description | 38 | | --------------------- | -------- | -------- | ----------- | 39 | | `project.name` | Yes | None | Project name 40 | | `project.version` | Yes | None | Project version 41 | | `project.repository` | No | None | GitHub repository 42 | | `docs.include` | Yes | None | Headers files to include for the documentation. Supports glob, so `**/*.hpp` will match all headers under project root and subdirectories. Note that any files included by the specified headers are considered when building docs aswell, so if you have one root header that includes all the project's headers, you should just point `docs.include` to that only | 43 | | `docs.exclude` | No | None | Header files captured by `docs.include` that should actually be excluded from documentation. This does not exclude files if they are included through other files in `docs.include` with `#include` | 44 | | `docs.tree` | No | None | The online tree base to use for documentation. Allows Flash to automatically generate links to the headers. Flash assumes that the input directory root is the same as the tree root; as in, a file that exist at `some/dir/header.hpp` in the input directory exist at `root/some/dir/header.hpp` | 45 | | `run.prebuild` | No | None | List of command line commands to run prior to configuring docs | 46 | | `analysis.compile-args` | No | None | List of arguments to pass to LibClang | 47 | | `cmake.config-args` | No | None | List of arguments to pass to CMake when configuring | 48 | | `cmake.build-args` | No | None | List of arguments to pass to CMake when building, if `cmake.build` is true | 49 | | `cmake.build` | No | `false` | Whether to actually build the CMake project or not | 50 | | `cmake.infer-args-from` | Yes (if `cmake` is specified) | None | What source file to get compilation arguments (include paths, defines, etc.) from | 51 | | `template.class` | No | `templates/class.html` | The file to use as the base for formatting docs for classes | 52 | | `template.struct-` (sic.) | No | `templates/struct.html` | The file to use as the base for formatting docs for structs | 53 | | `template.function` | No | `templates/function.html` | The file to use as the base for formatting docs for functions | 54 | | `template.file` | No | `templates/file.html` | The file to use as the base for formatting docs for files | 55 | | `template.index` | No | `templates/index.html` | The file to use as the base for formatting the docs root page | 56 | | `template.head` | No | `templates/head.html` | The file to use as the base for formatting the `` element for each docs page | 57 | | `template.nav` | No | `templates/nav.html` | The file to use as the base for formatting the navigation browser | 58 | | `template.page` | No | `templates/page.html` | The file to use as the base for formatting a docs page | 59 | | `scripts.css` | No | All the `css` files in `templates` | The CSS files to include with the docs. All the files are placed at root | 60 | | `scripts.js` | No | All the `js` files in `templates` | The JS files to include with the docs. All the files are placed at root | 61 | -------------------------------------------------------------------------------- /templates/themes.css: -------------------------------------------------------------------------------- 1 | 2 | .flash-theme-dark { 3 | --flash-gray: #272727; 4 | --flash-gray-dark: #222; 5 | --flash-gray-darker: #1a1a1a; 6 | --flash-gray-darkest: #111; 7 | --flash-white: #eee; 8 | --flash-light: #aaa; 9 | --flash-less-light: #888; 10 | --flash-less-dark: #6a6a6a; 11 | --flash-dark: #4a4a4a; 12 | --flash-darker: #3a3a3a; 13 | --flash-blue: #6a80d8; 14 | --flash-cyan-light: #7adff8; 15 | --flash-cyan: #6ac2d8; 16 | --flash-cyan-dark: #427b8a; 17 | --flash-cyan-darker: #294e57; 18 | --flash-green: #9ff4b7; 19 | --flash-purple: #9e6ad4; 20 | --flash-pink: #f49fe9; 21 | --flash-skin: #EC897C; 22 | --flash-dark-skin: #58342f; 23 | --flash-red: #e23485; 24 | --flash-yellow: #f5dd9f; 25 | --flash-orange: #f5c99f; 26 | --flash-dark-orange: #5c4c3e; 27 | --flash-border: rgba(255, 255, 255, .2); 28 | --flash-hover: rgba(255, 255, 255, .1); 29 | --flash-hover-light: rgba(255, 255, 255, .3); 30 | --flash-shade: rgba(0, 0, 0, .25); 31 | --flash-shadow: rgba(0, 0, 0, .2); 32 | 33 | --flash-body-bg: var(--flash-gray); 34 | --flash-h1-color: var(--flash-white); 35 | --flash-tab-selected-bg: var(--flash-light); 36 | --flash-tab-selected-color: var(--flash-dark); 37 | --flash-search-match: var(--flash-blue); 38 | --flash-nav-arrow: var(--flash-light); 39 | --flash-highlight: var(--flash-cyan); 40 | } 41 | 42 | .flash-theme-ocean { 43 | --flash-gray: #272737; 44 | --flash-gray-dark: #223; 45 | --flash-gray-darker: #1a1a2a; 46 | --flash-gray-darkest: #112; 47 | --flash-white: #eee; 48 | --flash-light: #aac; 49 | --flash-less-light: #88a; 50 | --flash-less-dark: #6a6a8a; 51 | --flash-dark: #4a4a6a; 52 | --flash-darker: #3a3a5a; 53 | --flash-blue: #6a80d8; 54 | --flash-cyan-light: #7adff8; 55 | --flash-cyan: #6ac2d8; 56 | --flash-cyan-dark: #427b8a; 57 | --flash-cyan-darker: #294e57; 58 | --flash-green: #9ff4b7; 59 | --flash-purple: #9e6ad4; 60 | --flash-pink: #f49fe9; 61 | --flash-skin: #EC897C; 62 | --flash-dark-skin: #58342f; 63 | --flash-red: #e23485; 64 | --flash-yellow: #f5dd9f; 65 | --flash-orange: #f5c99f; 66 | --flash-dark-orange: #5c4c3e; 67 | --flash-border: rgba(155, 155, 255, .2); 68 | --flash-hover: rgba(255, 255, 255, .1); 69 | --flash-hover-light: rgba(255, 255, 255, .3); 70 | --flash-shade: rgba(0, 0, 0, .25); 71 | --flash-shadow: rgba(0, 0, 0, .2); 72 | 73 | --flash-body-bg: var(--flash-gray); 74 | --flash-h1-color: var(--flash-white); 75 | --flash-tab-selected-bg: var(--flash-blue); 76 | --flash-tab-selected-color: var(--flash-white); 77 | --flash-search-match: var(--flash-cyan); 78 | --flash-nav-arrow: var(--flash-light); 79 | --flash-highlight: var(--flash-cyan); 80 | } 81 | 82 | .flash-theme-peach { 83 | --flash-gray: #372733; 84 | --flash-gray-dark: #342030; 85 | --flash-gray-darker: #2a1a28; 86 | --flash-gray-darkest: #221020; 87 | --flash-white: #eee; 88 | --flash-light: #bab; 89 | --flash-less-light: #a89; 90 | --flash-less-dark: #9a6a8a; 91 | --flash-dark: #6a4a5a; 92 | --flash-darker: #5a3a4a; 93 | --flash-blue: #6a80d8; 94 | --flash-cyan-light: #7adff8; 95 | --flash-cyan: #6ac2d8; 96 | --flash-cyan-dark: #427b8a; 97 | --flash-cyan-darker: #294e57; 98 | --flash-green: #9ff4b7; 99 | --flash-purple: #9e6ad4; 100 | --flash-pink: #f49fe9; 101 | --flash-skin: #EC897C; 102 | --flash-dark-skin: #58342f; 103 | --flash-red: #e23485; 104 | --flash-yellow: #f5dd9f; 105 | --flash-orange: #f5c99f; 106 | --flash-dark-orange: #5c4c3e; 107 | --flash-border: rgba(255, 225, 155, .2); 108 | --flash-hover: rgba(255, 255, 255, .1); 109 | --flash-hover-light: rgba(255, 255, 255, .3); 110 | --flash-shade: rgba(0, 0, 0, .25); 111 | --flash-shadow: rgba(0, 0, 0, .2); 112 | 113 | --flash-body-bg: var(--flash-gray); 114 | --flash-h1-color: var(--flash-yellow); 115 | --flash-tab-selected-bg: var(--flash-yellow); 116 | --flash-tab-selected-color: var(--flash-dark); 117 | --flash-search-match: var(--flash-yellow); 118 | --flash-nav-arrow: var(--flash-white); 119 | --flash-highlight: var(--flash-yellow); 120 | } 121 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use flash_macros::decl_config; 2 | use glob::glob; 3 | use serde::{Deserialize, Deserializer}; 4 | use std::{fs, path::PathBuf, sync::Arc}; 5 | 6 | use crate::url::UrlPath; 7 | 8 | fn parse_template<'de, D>(deserializer: D) -> Result, D::Error> 9 | where 10 | D: Deserializer<'de>, 11 | { 12 | Ok(Arc::from( 13 | fs::read_to_string(PathBuf::deserialize(deserializer)?) 14 | .map_err(serde::de::Error::custom)?, 15 | )) 16 | } 17 | 18 | fn parse_sources<'de, D>(deserializer: D) -> Result>, D::Error> 19 | where 20 | D: Deserializer<'de>, 21 | { 22 | Ok(Vec::::deserialize(deserializer)? 23 | .into_iter() 24 | .map(|src| Arc::from(Source::from_raw(src).unwrap())) 25 | .collect()) 26 | } 27 | 28 | fn parse_glob<'de, D>(deserializer: D) -> Result, D::Error> 29 | where 30 | D: Deserializer<'de>, 31 | { 32 | Ok(Vec::::deserialize(deserializer)? 33 | .iter() 34 | .flat_map(|src| { 35 | glob(src.to_str().unwrap()) 36 | .unwrap_or_else(|_| panic!("Invalid glob pattern {}", src.to_str().unwrap())) 37 | .map(|g| g.unwrap()) 38 | }) 39 | .collect()) 40 | } 41 | 42 | macro_rules! default_template { 43 | ($name: expr) => { 44 | Arc::from(include_str!($name).to_string()) 45 | }; 46 | } 47 | 48 | macro_rules! default_scripts { 49 | () => { 50 | Vec::new(), 51 | }; 52 | 53 | (@ $name: expr) => { 54 | Script { 55 | name: $name.into(), 56 | content: default_template!(concat!("../templates/", $name)), 57 | } 58 | }; 59 | 60 | ($name: expr $(, $rest: expr)*) => { 61 | vec![default_scripts!(@ $name), $(default_scripts!(@ $rest)),*] 62 | }; 63 | } 64 | 65 | pub struct Source { 66 | pub name: String, 67 | pub dir: UrlPath, 68 | pub include: Vec, 69 | pub exists_online: bool, 70 | } 71 | 72 | impl Source { 73 | pub fn from_raw(src: RawSource) -> Result { 74 | let exclude = src 75 | .exclude 76 | .into_iter() 77 | .map(|p| src.dir.to_pathbuf().join(p)) 78 | .flat_map(|src| { 79 | glob(src.to_str().unwrap()) 80 | .unwrap_or_else(|_| panic!("Invalid glob pattern {}", src.to_str().unwrap())) 81 | .map(|g| g.unwrap()) 82 | }) 83 | .collect::>(); 84 | 85 | let include = src 86 | .include 87 | .into_iter() 88 | .map(|p| src.dir.to_pathbuf().join(p)) 89 | .flat_map(|src| { 90 | glob(src.to_str().unwrap()) 91 | .unwrap_or_else(|_| panic!("Invalid glob pattern {}", src.to_str().unwrap())) 92 | .map(|g| g.unwrap()) 93 | }) 94 | .filter(|p| !exclude.contains(p)) 95 | .collect::>(); 96 | 97 | Ok(Self { 98 | name: src.name, 99 | dir: src.dir, 100 | exists_online: src.exists_online, 101 | include, 102 | }) 103 | } 104 | } 105 | 106 | decl_config! { 107 | struct Script { 108 | name: String, 109 | content: Arc as parse_template, 110 | } 111 | 112 | struct RawSource { 113 | name: String, 114 | dir: UrlPath, 115 | include: Vec, 116 | exclude: Vec = Vec::new(), 117 | exists_online: bool = true, 118 | } 119 | 120 | struct Config { 121 | project { 122 | name: String, 123 | version: String, 124 | repository?: String, 125 | tree?: String, 126 | icon?: PathBuf, 127 | }, 128 | tutorials? { 129 | dir: PathBuf, 130 | assets: Vec as parse_glob = Vec::new(), 131 | }, 132 | sources: Vec> as parse_sources, 133 | run? { 134 | prebuild: Vec = Vec::new(), 135 | }, 136 | analysis { 137 | compile_args: Vec = Vec::new(), 138 | }, 139 | cmake? { 140 | config_args: Vec = Vec::new(), 141 | build_args: Vec = Vec::new(), 142 | build: bool = false, 143 | build_dir: String = String::from("build"), 144 | infer_args_from: PathBuf, 145 | }, 146 | templates { 147 | class: Arc as parse_template = default_template!("../templates/class.html"), 148 | struct_: Arc as parse_template = default_template!("../templates/struct.html"), 149 | function: Arc as parse_template = default_template!("../templates/function.html"), 150 | head: Arc as parse_template = default_template!("../templates/head.html"), 151 | nav: Arc as parse_template = default_template!("../templates/nav.html"), 152 | file: Arc as parse_template = default_template!("../templates/file.html"), 153 | page: Arc as parse_template = default_template!("../templates/page.html"), 154 | tutorial: Arc as parse_template = default_template!("../templates/tutorial.html"), 155 | tutorial_index: Arc as parse_template = default_template!("../templates/tutorial-index.html"), 156 | }, 157 | scripts { 158 | css: Vec