├── 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 |
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 |
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 |
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 |
3 |
4 |
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 |
14 | {tutorial_content}
15 |
16 |
17 | {entity_content}
18 |
19 |
22 |
23 |
24 |
25 |
28 |
31 |
32 |
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