summaryrefslogtreecommitdiffstats
path: root/src/generator
diff options
context:
space:
mode:
Diffstat (limited to 'src/generator')
-rw-r--r--src/generator/config.rs53
-rw-r--r--src/generator/mod.rs216
-rw-r--r--src/generator/pandoc.rs166
3 files changed, 435 insertions, 0 deletions
diff --git a/src/generator/config.rs b/src/generator/config.rs
new file mode 100644
index 0000000..43fee60
--- /dev/null
+++ b/src/generator/config.rs
@@ -0,0 +1,53 @@
1use serde_derive::{Deserialize, Serialize};
2use structopt::StructOpt;
3
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone, Serialize)]
7pub(crate) struct Config {
8 /// Tells us in which language, which entity types should inline which children entity type in
9 /// their documentation.
10 ///
11 /// e.g. in C++, classes should inline their children methods documentation.
12 pub(crate) inlines: HashMap<String, HashMap<String, HashSet<String>>>,
13}
14
15impl Config {
16 pub(crate) fn from_merge(_cli: ProvidedConfig, conf: ProvidedConfig) -> Self {
17 Self {
18 inlines: conf.inlines.unwrap_or_else(default_inlines),
19 }
20 }
21}
22
23fn default_inlines() -> HashMap<String, HashMap<String, HashSet<String>>> {
24 let mut clang = HashMap::new();
25 let mut clang_inline_children = HashSet::new();
26 // TODO: this is not great: no differences between variable/field for children, but differences
27 // as a parent...
28 clang_inline_children.insert(String::from("variable"));
29 //classes.insert(String::from("field"));
30 clang_inline_children.insert(String::from("function"));
31 //classes.insert(String::from("method"));
32 clang.insert(String::from("struct"), clang_inline_children.clone());
33 clang.insert(String::from("class"), clang_inline_children.clone());
34 clang.insert(String::from("namespace"), clang_inline_children);
35
36 let mut inlines = HashMap::new();
37 inlines.insert(String::from("clang"), clang);
38
39 inlines
40}
41
42impl Default for Config {
43 fn default() -> Self {
44 Config::from_merge(ProvidedConfig::default(), ProvidedConfig::default())
45 }
46}
47
48#[derive(Debug, Clone, Default, StructOpt, Deserialize, Serialize)]
49#[serde(rename_all = "kebab-case")]
50pub(crate) struct ProvidedConfig {
51 #[structopt(skip)]
52 pub(crate) inlines: Option<HashMap<String, HashMap<String, HashSet<String>>>>,
53}
diff --git a/src/generator/mod.rs b/src/generator/mod.rs
new file mode 100644
index 0000000..b4e1c94
--- /dev/null
+++ b/src/generator/mod.rs
@@ -0,0 +1,216 @@
1pub(crate) mod config;
2mod pandoc;
3
4use self::config::Config;
5//use crate::entities::{
6// Description, DynEntity, EntitiesManager, EntitiesManagerComponents, Usr,
7//};
8use self::pandoc::into_pandoc;
9use crate::types::Entity;
10
11use anyhow::{ensure, Context, Result};
12use rayon::Scope;
13use thiserror::Error;
14
15use std::collections::BTreeMap;
16use std::io::Write;
17use std::path::Path;
18
19const DEFAULT_CSS: &[u8] = include_bytes!("../../res/style.css");
20
21pub(crate) fn generate(base_dir: &Path, entities: BTreeMap<String, Entity>, config: &Config) -> Result<()> {
22 let md_output_dir = base_dir.join("markdown");
23 let html_output_dir = base_dir.join("html");
24
25 std::fs::create_dir_all(&md_output_dir)
26 .context("Failed to create the markdown output directory")?;
27 std::fs::create_dir_all(&html_output_dir)
28 .context("Failed to create the HTML output directory")?;
29
30 let mut css_tempfile = tempfile::Builder::new()
31 .prefix("style")
32 .suffix(".css")
33 .tempfile()?;
34 css_tempfile.write_all(DEFAULT_CSS)?;
35
36 let css_path = css_tempfile.path();
37 debug!("Generated temporary file with CSS at: {:?}", css_path);
38
39 rayon::scope(|scope| {
40 for (id, entity) in entities {
41 generate_recursively(
42 id,
43 entity,
44 scope,
45 &md_output_dir,
46 &html_output_dir,
47 css_path,
48 config,
49 );
50 }
51 });
52
53 Ok(())
54}
55
56fn generate_recursively<'a>(
57 id: String,
58 entity: Entity,
59 pool: &Scope<'a>,
60 md_output_dir: &'a Path,
61 html_output_dir: &'a Path,
62 css_path: &'a Path,
63 config: &'a Config,
64) {
65 pool.spawn(move |pool| {
66 trace!("Trying to generate {}", id);
67
68 let leftovers = generate_single(
69 &id,
70 entity,
71 &md_output_dir,
72 &html_output_dir,
73 &css_path,
74 config,
75 )
76 .unwrap();
77
78 for (id, entity) in leftovers {
79 generate_recursively(
80 id,
81 entity,
82 pool,
83 md_output_dir,
84 html_output_dir,
85 css_path,
86 config,
87 );
88 }
89 });
90}
91
92fn generate_single(
93 id: &str,
94 entity: Entity,
95 md_output_dir: impl AsRef<Path>,
96 html_output_dir: impl AsRef<Path>,
97 css_path: impl AsRef<Path>,
98 config: &Config
99) -> Result<BTreeMap<String, Entity>> {
100 use std::io::prelude::*;
101 use std::process::{Command, Stdio};
102
103 let md_output_file = md_output_dir.as_ref().join(id);
104 let html_output_file = html_output_dir.as_ref().join(id);
105
106 let mut command = Command::new("pandoc");
107
108 command
109 .stdin(Stdio::piped())
110 .stdout(Stdio::piped())
111 .stderr(Stdio::piped())
112 .args(&[
113 "--from=json",
114 "--to=markdown",
115 "--output",
116 md_output_file
117 .to_str()
118 .context("Entity name is not valid UTF-8")?,
119 ]);
120
121 debug!("Launching command: {:?}", command);
122
123 let mut pandoc = command
124 .spawn()
125 .context("Failed to execute Pandoc command")?;
126
127 let (pandoc_ast, leftovers) = into_pandoc(entity, config);
128
129 if log_enabled!(log::Level::Trace) {
130 let json =
131 serde_json::to_string(&pandoc_ast).context("Failed to convert Pandoc AST to JSON")?;
132 trace!("Sending json: {}", json);
133 write!(
134 pandoc.stdin.as_mut().expect("Pandoc stdin not captured"),
135 "{}",
136 &json,
137 )
138 .context("Failed to convert Pandoc AST to JSON")?;
139 } else {
140 serde_json::to_writer(
141 pandoc.stdin.as_mut().expect("Pandoc stdin not captured"),
142 &pandoc_ast,
143 )
144 .context("Failed to convert Pandoc AST to JSON")?;
145 }
146
147 let command_str = format!("{:?}", pandoc);
148
149 let output = pandoc
150 .wait_with_output()
151 .expect("Pandoc command wasn't running");
152
153 ensure!(
154 output.status.success(),
155 CommandError {
156 command: format!("{:?}", command_str),
157 status: output.status,
158 stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"),
159 }
160 );
161
162 let mut command = Command::new("pandoc");
163 command
164 .stdin(Stdio::piped())
165 .stdout(Stdio::piped())
166 .stderr(Stdio::piped())
167 .args(&[
168 "--from=markdown-raw_tex",
169 "--to=html",
170 "--css",
171 css_path
172 .as_ref()
173 .to_str()
174 .context("CSS path is not valid UTF-8")?,
175 "--standalone",
176 "--self-contained",
177 md_output_file
178 .to_str()
179 .context("Entity name is not valid UTF-8")?,
180 "--output",
181 html_output_file
182 .to_str()
183 .context("Entity name is not valid UTF-8")?,
184 ]);
185
186 let command_str = format!("{:?}", command);
187 debug!("Launching command: {}", command_str);
188
189 let output = command
190 .output()
191 .context("Failed to execute Pandoc command")?;
192
193 ensure!(
194 output.status.success(),
195 CommandError {
196 command: command_str,
197 status: output.status,
198 stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"),
199 }
200 );
201
202 Ok(leftovers)
203}
204
205#[derive(Debug, Clone, Error)]
206#[error(
207 "Command {} returned status {:?} and stderr {:?}",
208 command,
209 status,
210 stderr
211)]
212struct CommandError {
213 command: String,
214 status: std::process::ExitStatus,
215 stderr: String,
216}
diff --git a/src/generator/pandoc.rs b/src/generator/pandoc.rs
new file mode 100644
index 0000000..1753d34
--- /dev/null
+++ b/src/generator/pandoc.rs
@@ -0,0 +1,166 @@
1use super::config::Config;
2use crate::types::Entity;
3
4use pandoc_types::definition::{Attr, Block, Inline, Meta, MetaValue, Pandoc};
5
6use std::collections::BTreeMap;
7
8pub(crate) fn into_pandoc(
9 entity: Entity,
10 config: &Config,
11) -> (Pandoc, BTreeMap<String, Entity>) {
12 let mut meta = Meta::null();
13
14 let title = vec![Inline::Code(Attr::null(), entity.name.clone())];
15
16 meta.0
17 .insert("title".to_string(), MetaValue::MetaString(entity.name));
18
19 let mut content = Vec::new();
20
21 content.push(Block::Header(1, Attr::null(), title));
22
23 if !entity.documentation.is_empty() {
24 content.push(Block::Header(
25 2,
26 Attr::null(),
27 vec![Inline::Str(String::from("Description"))],
28 ));
29
30 content.push(Block::Div(
31 Attr(String::new(), vec![String::from("doc")], vec![]),
32 vec![raw_markdown(entity.documentation.clone())],
33 ));
34 }
35
36 let mut inline_children = BTreeMap::new();
37 let mut separate_children = BTreeMap::new();
38
39 let entity_kind = entity.kind;
40 for (children_kind, children) in entity.children {
41 if config
42 .inlines
43 .get(&entity.language)
44 .and_then(|lang_inlines| lang_inlines.get(&entity_kind))
45 .map(|children_inlines| children_inlines.contains(&children_kind))
46 == Some(true)
47 {
48 inline_children.insert(children_kind, children);
49 } else {
50 // By default, do not inline
51 separate_children.insert(children_kind, children);
52 }
53 }
54
55 for (section_name, children) in &separate_children {
56 if let Some(members_list) = member_list(children) {
57 content.push(Block::Header(
58 2,
59 Attr::null(),
60 vec![Inline::Str(String::from(section_name))],
61 ));
62
63 content.push(members_list);
64 }
65 }
66
67 let mut embedded_documentation = Vec::new();
68
69 for (section_name, children) in inline_children {
70 if let Some(members_list) = member_list(&children) {
71 content.push(Block::Header(
72 2,
73 Attr::null(),
74 vec![Inline::Str(section_name.clone())],
75 ));
76
77 content.push(members_list);
78
79 embedded_documentation.push(Block::Header(
80 2,
81 Attr::null(),
82 vec![Inline::Str(section_name + " Documentation")],
83 ));
84
85 for (_id, child) in children {
86 embedded_documentation.push(Block::Header(
87 3,
88 Attr::null(),
89 vec![Inline::Code(Attr::null(), child.name)],
90 ));
91
92 embedded_documentation.push(Block::Div(
93 Attr(String::new(), vec![String::from("doc")], vec![]),
94 vec![raw_markdown(child.documentation)],
95 ));
96 }
97 }
98 }
99
100 content.append(&mut embedded_documentation);
101
102 let leftovers = separate_children
103 .into_iter()
104 .map(|(_section_name, children)| children)
105 .flatten()
106 .collect();
107
108 (Pandoc(meta, content), leftovers)
109}
110
111fn str_block(content: String) -> Block {
112 Block::Plain(vec![Inline::Str(content)])
113}
114
115fn entity_link(id: &str, name: String) -> Inline {
116 use pandoc_types::definition::Target;
117 use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
118
119 use std::iter::once;
120
121 // https://url.spec.whatwg.org/#fragment-percent-encode-set
122 const FRAGMENT: &AsciiSet = &CONTROLS
123 .add(b' ')
124 .add(b'"')
125 .add(b'<')
126 .add(b'>')
127 .add(b'`')
128 .add(b'#')
129 .add(b'?')
130 .add(b'{')
131 .add(b'}');
132
133 Inline::Link(
134 Attr::null(),
135 vec![Inline::Code(Attr::null(), name)],
136 Target(
137 once("./")
138 .chain(utf8_percent_encode(id, FRAGMENT))
139 .collect(),
140 String::new(),
141 ),
142 )
143}
144
145fn raw_markdown(text: String) -> Block {
146 use pandoc_types::definition::Format;
147 Block::RawBlock(Format(String::from("markdown")), text)
148}
149
150fn member_list<'a>(members: impl IntoIterator<Item = (&'a String, &'a Entity)>) -> Option<Block> {
151 let definitions: Vec<(Vec<Inline>, Vec<Vec<Block>>)> = members
152 .into_iter()
153 .map(|(id, entity)| {
154 (
155 vec![entity_link(id, entity.name.clone())],
156 vec![vec![str_block(entity.brief_description.clone())]],
157 )
158 })
159 .collect();
160
161 if definitions.is_empty() {
162 None
163 } else {
164 Some(Block::DefinitionList(definitions))
165 }
166}