From 3301430c676e4af6b95d96b6408a66f9d2768653 Mon Sep 17 00:00:00 2001 From: Minijackson Date: Sun, 8 Sep 2019 16:15:46 +0200 Subject: First version --- src/cli.rs | 18 ++ src/doxygen.rs | 454 +++++++++++++++++++++++++++++++++++++++++++++++ src/doxygen/builders.rs | 109 ++++++++++++ src/entities.rs | 134 ++++++++++++++ src/generator.rs | 209 ++++++++++++++++++++++ src/main.rs | 44 +++++ src/pandoc.rs | 246 ++++++++++++++++++++++++++ src/pandoc/types.rs | 39 +++++ src/parsing.rs | 457 ++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1710 insertions(+) create mode 100644 src/cli.rs create mode 100644 src/doxygen.rs create mode 100644 src/doxygen/builders.rs create mode 100644 src/entities.rs create mode 100644 src/generator.rs create mode 100644 src/main.rs create mode 100644 src/pandoc.rs create mode 100644 src/pandoc/types.rs create mode 100644 src/parsing.rs (limited to 'src') diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..e106de7 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,18 @@ +use structopt::StructOpt; + +#[derive(Debug, Clone, StructOpt)] +pub(crate) struct Cli { + #[structopt(long, short, parse(from_occurrences))] + pub(crate) verbosity: u8, + + #[structopt(long, number_of_values = 1, parse(try_from_str = shell_words::split))] + pub(crate) extra_arg: Vec>, + + #[structopt(subcommand)] + pub(crate) command: Command, +} + +#[derive(Debug, Clone, StructOpt)] +pub(crate) enum Command { + Generate { file: String }, +} diff --git a/src/doxygen.rs b/src/doxygen.rs new file mode 100644 index 0000000..8d2267d --- /dev/null +++ b/src/doxygen.rs @@ -0,0 +1,454 @@ +/// A 'recursive descent' parser for Doxygen XML files +mod builders; + +use xml::{ + attribute::OwnedAttribute, + reader::{EventReader, ParserConfig, XmlEvent}, +}; + +use std::collections::HashMap; +use std::io::Read; + +// === Types === + +#[derive(Debug, Clone)] +pub(crate) enum DoxygenType { + NameSpace(NameSpace), + Class(Class), + Unhandled, +} + +#[derive(Debug, Clone)] +pub(crate) struct NameSpace { + pub(crate) name: String, + pub(crate) members: Vec, + pub(crate) brief_description: Description, + pub(crate) detailed_description: Description, +} + +#[derive(Debug, Clone)] +pub(crate) struct FunctionArgument { + pub(crate) name: String, + pub(crate) r#type: String, + pub(crate) default_value: Option, +} + +#[derive(Debug, Clone)] +pub(crate) enum Member { + Variable { + name: String, + r#type: String, + }, + // What + /* + Enum { + + } + */ + Function { + name: String, + args: Vec, + return_type: String, + }, + Unhandled, +} + +#[derive(Debug, Clone)] +pub(crate) struct Class { + pub(crate) name: String, + pub(crate) inners: Vec<(String, String)>, + pub(crate) sections: HashMap>, + pub(crate) brief_description: Description, + pub(crate) detailed_description: Description, +} + +// === Description === + +#[derive(Debug, Clone)] +pub(crate) struct Description { + pub(crate) inner: Vec, +} + +impl Description { + fn new() -> Self { + Self { inner: Vec::new() } + } +} + +#[derive(Debug, Clone)] +pub(crate) enum DescriptionNode { + Text(String), + Title(String), + Para(Paragraph), + Sect1(String), + Internal(String), +} + +#[derive(Debug, Clone)] +pub(crate) struct Paragraph { + pub(crate) inner: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) enum ParagraphNode { + Text(String), + Ref(Ref), +} + +#[derive(Debug, Clone)] +pub(crate) struct Ref { + pub(crate) id: String, + pub(crate) kind: RefKind, + pub(crate) text: String, +} + +#[derive(Debug, Clone)] +pub(crate) enum RefKind { + Compound, + Member, +} + +#[derive(Debug, Clone)] +enum DoxygenTypeKind { + NameSpace, + Unhandled, +} + +trait TypeBuilder { + //const KIND: DoxygenTypeKind; + + fn build(self: Box) -> DoxygenType; + + fn name(&mut self, value: String); + fn brief_description(&mut self, description: Description); + fn detailed_description(&mut self, description: Description); + + fn add_inner(&mut self, name: String, kind: String); + fn add_member(&mut self, section: String, member: Member); +} + +trait FromXML { + fn from_xml(attributes: Vec, event_reader: &mut EventReader) + -> Self; +} + +impl FromXML for Description { + fn from_xml( + _attributes: Vec, + mut event_reader: &mut EventReader, + ) -> Self { + let mut inner = Vec::new(); + + while let Ok(event) = event_reader.next() { + match event { + XmlEvent::Characters(text) => inner.push(DescriptionNode::Text(text)), + XmlEvent::StartElement { + name, attributes, .. + } => inner.push(match name.local_name.as_str() { + "para" => { + DescriptionNode::Para(Paragraph::from_xml(attributes, &mut event_reader)) + } + "title" => DescriptionNode::Title(event_simple(&mut event_reader)), + "sect1" => DescriptionNode::Sect1(event_simple(&mut event_reader)), + "internal" => DescriptionNode::Internal(event_simple(&mut event_reader)), + other => { + warn!("Description element not supported: {}", other); + continue; + } + }), + XmlEvent::EndElement { .. } => break, + other => { + warn!("Description event not supported: {:?}", other); + } + } + } + + Self { inner } + } +} + +impl FromXML for Paragraph { + fn from_xml( + _attributes: Vec, + mut event_reader: &mut EventReader, + ) -> Self { + let mut inner = Vec::new(); + + while let Ok(event) = event_reader.next() { + match event { + XmlEvent::Characters(text) => inner.push(ParagraphNode::Text(text)), + XmlEvent::StartElement { + name, attributes, .. + } => inner.push(match name.local_name.as_str() { + "ref" => ParagraphNode::Ref(Ref::from_xml(attributes, &mut event_reader)), + other => { + warn!("Paragraph element not supported: {}", other); + continue; + } + }), + XmlEvent::EndElement { .. } => break, + other => { + warn!("Paragraph event not supported: {:?}", other); + } + } + } + + Self { inner } + } +} + +impl FromXML for Ref { + fn from_xml( + attributes: Vec, + mut event_reader: &mut EventReader, + ) -> Self { + let mut id = None; + let mut kind = None; + + for attr in attributes.into_iter() { + match attr.name.local_name.as_str() { + "refid" => id = Some(attr.value), + "kindref" => { + kind = Some(match attr.value.as_str() { + "compound" => RefKind::Compound, + "member" => RefKind::Member, + other => { + warn!("Ref kind not supported: {}", other); + RefKind::Compound + } + }); + } + other => { + warn!("Ref element not supported: {}", other); + } + } + } + + let text = event_simple(&mut event_reader); + + Ref { + id: id.unwrap(), + kind: kind.unwrap(), + text, + } + } +} + +/// Parse a type from a XML Doxygen file +pub(crate) fn parse_type(reader: impl Read) -> Vec { + let mut event_reader = EventReader::new(reader); + + match event_reader.next().unwrap() { + XmlEvent::StartDocument { .. } => (), + _ => panic!("XML file does not begin with document"), + } + + match event_reader.next().unwrap() { + XmlEvent::StartElement { name, .. } => { + if name.local_name != "doxygen" { + panic!("XML file does not start with a 'doxygen' element"); + } + } + _ => panic!("XML file does not start with a 'doxygen' element"), + } + + let mut result = Vec::new(); + + while let Ok(event) = event_reader.next() { + match dbg!(event) { + XmlEvent::StartElement { + name, attributes, .. + } => { + if name.local_name != "compounddef" { + panic!("'doxygen' is not a sequence of 'compounddef' elements"); + } + + result.push(event_compounddef(attributes, &mut event_reader)); + } + XmlEvent::Whitespace(_) => (), + XmlEvent::EndElement { .. } => break, + _ => panic!("'doxygen' is not a sequence of 'compounddef' elements"), + } + } + + // Unnecessarily strict? + match event_reader.next().unwrap() { + XmlEvent::EndDocument => (), + _ => panic!("XML file does not end after 'doxygen' element"), + } + + dbg!(result) +} + +fn event_compounddef( + attributes: Vec, + mut event_reader: &mut EventReader, +) -> DoxygenType { + let kind = attributes + .into_iter() + .find_map(|attr| { + if attr.name.local_name == "kind" { + Some(attr.value) + } else { + None + } + }) + .unwrap(); + + let mut builder = builders::builder_for(kind); + + while let Ok(event) = event_reader.next() { + match dbg!(event) { + XmlEvent::StartElement { + name, attributes, .. + } => match name.local_name.as_str() { + "compoundname" => { + let name = event_simple(&mut event_reader); + builder.name(name); + } + "briefdescription" => { + let brief_description = Description::from_xml(attributes, &mut event_reader); + builder.brief_description(brief_description); + } + "detaileddescription" => { + let detailed_description = Description::from_xml(attributes, &mut event_reader); + builder.detailed_description(detailed_description); + } + "location" => { + event_simple(&mut event_reader); + debug!("Do something?"); + } + "sectiondef" => { + event_section(&mut builder, &mut event_reader); + } + other_name if is_inner_type(other_name) => { + let inner = event_simple(&mut event_reader); + builder.add_inner(inner, other_name.to_string()); + } + _other_name => { + event_ignore(&mut event_reader); + } + }, + XmlEvent::Whitespace(_) => (), + XmlEvent::EndElement { .. } => break, + _ => panic!("Unhandled XML event"), + } + } + + builder.build() +} + +/// Returns true if the given XML Element is a reference to another inner type. +/// +/// Corresponds to the "refType" XSD type in `compound.xsd` +fn is_inner_type(element_name: &str) -> bool { + match element_name { + "innerdir" | "innerfile" | "innerclass" | "innernamespace" | "innerpage" | "innergroup" => { + true + } + _ => false, + } +} + +/// Get the text inside a simple, non recursive XML tag +fn event_simple(event_reader: &mut EventReader) -> String { + let result; + + match dbg!(event_reader.next().unwrap()) { + XmlEvent::Characters(tmp_result) => { + result = tmp_result; + } + XmlEvent::EndElement { .. } => return "".to_string(), + _ => panic!("Simple XML event is not so simple"), + } + + match dbg!(event_reader.next().unwrap()) { + XmlEvent::EndElement { .. } => (), + _ => panic!("Simple XML event is not so simple"), + } + + result +} + +fn event_ignore(mut event_reader: &mut EventReader) { + loop { + let event = event_reader.next().unwrap(); + match dbg!(event) { + XmlEvent::StartElement { .. } => event_ignore(&mut event_reader), + XmlEvent::EndElement { .. } => break, + _ => (), + } + } +} + +fn event_section( + mut builder: &mut Box, + mut event_reader: &mut EventReader, +) { + loop { + let event = event_reader.next().unwrap(); + match dbg!(event) { + XmlEvent::StartElement { + name, attributes, .. + } => match name.local_name.as_str() { + "memberdef" => { + let member = event_member(attributes, &mut event_reader); + builder.add_member("bla".to_owned(), member); + } + _ => panic!("Unhandled thing"), + }, + + XmlEvent::Whitespace(_) => (), + XmlEvent::EndElement { .. } => break, + _ => panic!("Unknown section XML event"), + } + } +} + +fn event_member( + attributes: Vec, + mut event_reader: &mut EventReader, +) -> Member { + let kind = attributes + .into_iter() + .find_map(|attr| { + if attr.name.local_name == "kind" { + Some(attr.value) + } else { + None + } + }) + .unwrap(); + + match kind.as_str() { + "variable" => event_member_variable(&mut event_reader), + _ => { + event_ignore(&mut event_reader); + Member::Unhandled + } + } +} + +fn event_member_variable(mut event_reader: &mut EventReader) -> Member { + let mut member_name = None; + let mut r#type = None; + + loop { + let event = event_reader.next().unwrap(); + match dbg!(event) { + XmlEvent::StartElement { name, .. } => match name.local_name.as_str() { + "name" => member_name = Some(event_simple(&mut event_reader)), + "definition" => r#type = Some(event_simple(&mut event_reader)), + _ => event_ignore(&mut event_reader), + }, + XmlEvent::Whitespace(_) => (), + XmlEvent::EndElement { .. } => break, + _ => panic!("Unknown member XML event"), + } + } + + Member::Variable { + name: member_name.unwrap(), + r#type: r#type.unwrap(), + } +} diff --git a/src/doxygen/builders.rs b/src/doxygen/builders.rs new file mode 100644 index 0000000..9b60325 --- /dev/null +++ b/src/doxygen/builders.rs @@ -0,0 +1,109 @@ +use super::*; + +pub(super) fn builder_for(doxygen_kind: String) -> Box { + match doxygen_kind.as_str() { + "namespace" => Box::new(NameSpaceBuilder::default()), + "class" | "struct" => Box::new(ClassBuilder::default()), + _ => Box::new(UnhandledBuilder), + } +} + +#[derive(Debug, Default)] +struct NameSpaceBuilder { + name: Option, + members: Vec, + brief_description: Option, + detailed_description: Option, +} + +impl TypeBuilder for NameSpaceBuilder { + fn build(self: Box) -> DoxygenType { + let name = self.name.unwrap(); + + DoxygenType::NameSpace(NameSpace { + name, + members: self.members, + brief_description: self.brief_description.unwrap(), + detailed_description: self.detailed_description.unwrap(), + }) + } + + fn name(&mut self, value: String) { + self.name = Some(value); + } + + fn add_inner(&mut self, name: String, kind: String) { + self.members.push(name); + } + + fn add_member(&mut self, _section: String, _member: Member) { + panic!("Adding member in namespace?"); + } + + fn brief_description(&mut self, description: Description) { + self.brief_description = Some(description); + } + + fn detailed_description(&mut self, description: Description) { + self.brief_description = Some(description); + } +} + +#[derive(Debug, Default)] +struct UnhandledBuilder; + +impl TypeBuilder for UnhandledBuilder { + fn build(self: Box) -> DoxygenType { + DoxygenType::Unhandled + } + + fn name(&mut self, _value: String) {} + + fn add_inner(&mut self, _name: String, _kind: String) {} + fn add_member(&mut self, _section: String, _member: Member) {} + fn brief_description(&mut self, _description: Description) {} + fn detailed_description(&mut self, _description: Description) {} +} + +#[derive(Debug, Default)] +struct ClassBuilder { + name: Option, + inners: Vec<(String, String)>, + sections: HashMap>, + brief_description: Option, + detailed_description: Option, +} + +impl TypeBuilder for ClassBuilder { + fn build(self: Box) -> DoxygenType { + let name = self.name.unwrap(); + + DoxygenType::Class(Class { + name, + inners: self.inners, + sections: self.sections, + brief_description: self.brief_description.unwrap_or(Description::new()), + detailed_description: self.detailed_description.unwrap_or(Description::new()), + }) + } + + fn name(&mut self, value: String) { + self.name = Some(value); + } + + fn add_inner(&mut self, name: String, kind: String) { + self.inners.push((name, kind)); + } + + fn add_member(&mut self, section: String, member: Member) { + self.sections.entry(section).or_default().push(member); + } + + fn brief_description(&mut self, description: Description) { + self.brief_description = Some(description); + } + + fn detailed_description(&mut self, description: Description) { + self.brief_description = Some(description); + } +} diff --git a/src/entities.rs b/src/entities.rs new file mode 100644 index 0000000..b7368df --- /dev/null +++ b/src/entities.rs @@ -0,0 +1,134 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct Usr(pub(crate) String); + +#[derive(Debug, Clone)] +pub(crate) struct EntitiesManager { + toplevel_entities: HashMap, + descriptions: HashMap, +} + +pub(crate) struct EntitiesManagerComponents { + pub(crate) toplevel_entities: HashMap, + pub(crate) descriptions: HashMap, +} + +impl EntitiesManager { + pub fn new() -> Self { + EntitiesManager { + toplevel_entities: HashMap::new(), + descriptions: HashMap::new(), + } + } + + pub fn insert(&mut self, usr: Usr, description: Description) { + info!("Found entity {:?}", description.name); + self.descriptions.insert(usr, description); + } + + pub fn insert_toplevel(&mut self, usr: Usr, entity: Entity) { + self.toplevel_entities.insert(usr, entity); + } + + pub fn decompose(self) -> EntitiesManagerComponents { + EntitiesManagerComponents { + toplevel_entities: self.toplevel_entities, + descriptions: self.descriptions, + } + } +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct Description { + pub(crate) name: String, + pub(crate) brief: String, + pub(crate) detailed: String, +} + +impl Description { + pub(crate) fn with_name(name: String) -> Self { + Description { + name, + ..Default::default() + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Described { + pub(crate) description: Description, + pub(crate) entity: T, +} + +#[derive(Debug, Clone)] +pub(crate) enum Entity { + NameSpace(NameSpace), + Variable(Variable), + Function(Function), + Class(Class), +} + +impl Entity { + fn kind_name(&self) -> &'static str { + match self { + Entity::NameSpace(_) => "namespace", + Entity::Variable(_) => "variable", + Entity::Function(_) => "function", + Entity::Class(_) => "class", + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct NameSpace { + pub(crate) members: HashMap, +} + +impl From for Entity { + fn from(ns: NameSpace) -> Self { + Entity::NameSpace(ns) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Variable { + pub(crate) r#type: String, +} + +impl From for Entity { + fn from(var: Variable) -> Self { + Entity::Variable(var) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Function { + pub(crate) arguments: Vec, + pub(crate) return_type: String, +} + +impl From for Entity { + fn from(func: Function) -> Self { + Entity::Function(func) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct FunctionArgument { + pub(crate) name: String, + pub(crate) r#type: String, +} + +#[derive(Debug, Clone)] +pub(crate) struct Class { + pub(crate) member_types: Vec, + pub(crate) member_functions: HashMap, + pub(crate) member_variables: HashMap, +} + +impl From for Entity { + fn from(class: Class) -> Self { + Entity::Class(class) + } +} diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..0ab4511 --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,209 @@ +use crate::entities::{ + Described, Description, EntitiesManager, EntitiesManagerComponents, Entity, Usr, +}; + +use anyhow::{Context, Result}; +use thiserror::Error; +use threadpool::ThreadPool; + +use std::collections::HashMap; +use std::path::Path; +use std::sync::{mpsc::channel, Arc}; + +pub(crate) fn generate(base_dir: &Path, manager: EntitiesManager) -> Result<()> { + let EntitiesManagerComponents { + toplevel_entities, + descriptions, + } = manager.decompose(); + + let md_output_dir = base_dir.join("markdown"); + let html_output_dir = base_dir.join("html"); + + std::fs::create_dir_all(&md_output_dir) + .context("Failed to create the markdown output directory")?; + std::fs::create_dir_all(&html_output_dir) + .context("Failed to create the HTML output directory")?; + + let pool = ThreadPool::new(num_cpus::get()); + + let descriptions = Arc::new(descriptions); + + let (tx, rx) = channel::<()>(); + + for (usr, entity) in toplevel_entities { + generate_recursively( + usr, + entity, + pool.clone(), + tx.clone(), + descriptions.clone(), + &md_output_dir, + &html_output_dir, + ); + } + + drop(tx); + + // This is not really idiomatic, but iter returns None when every Sender is destroyed, so just + // by passing around Senders in generate_recursively, we wait for every job. + rx.iter().for_each(drop); + + Ok(()) +} + +fn generate_recursively( + usr: Usr, + entity: Entity, + pool: ThreadPool, + tx: std::sync::mpsc::Sender<()>, + descriptions: Arc>, + md_output_dir: impl AsRef, + html_output_dir: impl AsRef, +) { + let descriptions = descriptions.clone(); + let md_output_dir = md_output_dir.as_ref().to_owned(); + let html_output_dir = html_output_dir.as_ref().to_owned(); + + let pool2 = pool.clone(); + pool.execute(move || { + trace!("Trying to generate {}", usr.0); + + let entity = Described:: { + entity, + description: descriptions.get(&usr).unwrap().clone(), + }; + + let leftovers = generate_single( + &usr, + entity, + &descriptions, + &md_output_dir, + &html_output_dir, + ) + .unwrap(); + + for (usr, entity) in leftovers { + let pool = pool2.clone(); + let tx = tx.clone(); + generate_recursively( + usr, + entity, + pool, + tx, + descriptions.clone(), + md_output_dir.clone(), + html_output_dir.clone(), + ); + } + }); +} + +fn generate_single( + usr: &Usr, + entity: Described, + descriptions: &HashMap, + md_output_dir: impl AsRef, + html_output_dir: impl AsRef, +) -> Result> { + use std::io::prelude::*; + use std::process::{Command, Stdio}; + + let md_output_file = md_output_dir.as_ref().join(&usr.0); + let html_output_file = html_output_dir.as_ref().join(&usr.0); + + let mut command = Command::new("pandoc"); + + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(&[ + "--from=json", + "--to=markdown", + "--output", + md_output_file + .to_str() + .context("Entity name is not valid UTF-8")?, + ]); + + debug!("Launching command: {:?}", command); + + let mut pandoc = command + .spawn() + .context("Failed to execute Pandoc command")?; + + let (pandoc_ast, leftovers) = entity.into_pandoc(&descriptions); + + if log_enabled!(log::Level::Trace) { + let json = + serde_json::to_string(&pandoc_ast).context("Failed to convert Pandoc AST to JSON")?; + trace!("Sending json: {}", json); + write!( + pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), + "{}", + &json, + ) + .context("Failed to convert Pandoc AST to JSON")?; + } else { + serde_json::to_writer( + pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), + &pandoc_ast, + ) + .context("Failed to convert Pandoc AST to JSON")?; + } + + let output = pandoc + .wait_with_output() + .expect("Pandoc command wasn't running"); + + if !output.status.success() { + Err(CommandError { + status: output.status, + stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), + }) + .context("Pandoc command failed")?; + } + + let mut command = Command::new("pandoc"); + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(&[ + "--from=markdown", + "--to=html", + "--css=res/style.css", + "--standalone", + "--self-contained", + md_output_file + .to_str() + .context("Entity name is not valid UTF-8")?, + "--output", + html_output_file + .to_str() + .context("Entity name is not valid UTF-8")?, + ]); + + debug!("Launching command: {:?}", command); + + let output = command + .output() + .context("Failed to execute Pandoc command")?; + + if !output.status.success() { + Err(CommandError { + status: output.status, + stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), + }) + .context("Pandoc command failed")?; + } + + Ok(leftovers) +} + +#[derive(Debug, Clone, Error)] +#[error("Command returned status {:?} and stderr {:?}", status, stderr)] +struct CommandError { + status: std::process::ExitStatus, + stderr: String, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e1d4752 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,44 @@ +//mod doxygen; +mod cli; +mod entities; +mod generator; +mod pandoc; +mod parsing; + +#[macro_use] +extern crate log; + +use cli::Command; +use generator::generate; +use parsing::parse_file; + +use anyhow::Result; +use structopt::StructOpt; + +fn main() -> Result<()> { + let cli = cli::Cli::from_args(); + pretty_env_logger::formatted_builder() + .filter( + None, + match cli.verbosity { + // Warnings and errors for internal warnings / errors + 0 => log::LevelFilter::Warn, + 1 => log::LevelFilter::Info, + 2 => log::LevelFilter::Debug, + _ => log::LevelFilter::Trace, + }, + ) + .try_init()?; + + match cli.command { + Command::Generate { file } => { + let extra_args = cli.extra_arg.iter().flatten().map(AsRef::as_ref).collect(); + let manager = parse_file(file, extra_args); + + let base_output_dir = std::path::Path::new("doc"); + generate(&base_output_dir, manager)?; + } + } + + Ok(()) +} diff --git a/src/pandoc.rs b/src/pandoc.rs new file mode 100644 index 0000000..5ca84e7 --- /dev/null +++ b/src/pandoc.rs @@ -0,0 +1,246 @@ +//mod types; + +use crate::entities::*; + +use pandoc_types::definition::{Attr, Block, Inline, Meta, MetaValue, Pandoc}; + +use std::collections::HashMap; + +impl Described { + pub fn into_pandoc( + self, + descriptions: &HashMap, + ) -> (Pandoc, HashMap) { + let mut meta = Meta::null(); + + let title = self.title(); + let mut content_before = self.content_before(&descriptions); + let mut content_after = self.content_after(&descriptions); + let leftovers = self.leftovers(); + + meta.0.insert( + "title".to_string(), + MetaValue::MetaString(self.description.name), + ); + + let mut content = Vec::new(); + + content.push(Block::Header(1, Attr::null(), title)); + + content.append(&mut content_before); + + if !self.description.detailed.is_empty() { + content.push(Block::Header( + 2, + Attr::null(), + vec![Inline::Str(String::from("Description"))], + )); + + content.push(Block::Div( + Attr(String::new(), vec![String::from("doc")], vec![]), + vec![raw_markdown(self.description.detailed)], + )); + } + + content.append(&mut content_after); + + (Pandoc(meta, content), leftovers) + } +} + +// TODO: replace with single function so we can move out, and remove all of those clones +trait PandocDisplay { + fn content_before(&self, _descriptions: &HashMap) -> Vec { + vec![] + } + + fn content_after(&self, _descriptions: &HashMap) -> Vec { + vec![] + } + + fn leftovers(&self) -> HashMap { + HashMap::new() + } +} + +impl Described { + fn title(&self) -> Vec { + match &self.entity { + Entity::Variable(variable) => vec![Inline::Code( + Attr(String::new(), vec![String::from("cpp")], vec![]), + variable.r#type.clone() + " " + &self.description.name, + )], + _ => vec![Inline::Code(Attr::null(), self.description.name.clone())], + } + } +} + +impl PandocDisplay for Described { + fn content_before(&self, descriptions: &HashMap) -> Vec { + match &self.entity { + Entity::NameSpace(ns) => ns.content_before(descriptions), + Entity::Variable(var) => var.content_before(descriptions), + Entity::Function(func) => func.content_before(descriptions), + Entity::Class(class) => class.content_before(descriptions), + } + } + + fn content_after(&self, descriptions: &HashMap) -> Vec { + match &self.entity { + Entity::NameSpace(ns) => ns.content_after(descriptions), + Entity::Variable(var) => var.content_after(descriptions), + Entity::Function(func) => func.content_after(descriptions), + Entity::Class(class) => class.content_after(descriptions), + } + } + + fn leftovers(&self) -> HashMap { + match &self.entity { + Entity::NameSpace(ns) => ns.leftovers(), + Entity::Variable(var) => var.leftovers(), + Entity::Function(func) => func.leftovers(), + Entity::Class(class) => class.leftovers(), + } + } +} + +impl PandocDisplay for NameSpace { + fn content_after(&self, descriptions: &HashMap) -> Vec { + let mut content = Vec::new(); + + content.push(Block::Header( + 2, + Attr::null(), + vec![Inline::Str("Members".to_string())], + )); + + if let Some(member_list) = member_list(self.members.keys(), descriptions) { + content.push(member_list); + } else { + content.push(str_block(String::from("None"))); + } + + content + } + + fn leftovers(&self) -> HashMap { + self.members.clone() + } +} + +impl PandocDisplay for Class { + fn content_after(&self, descriptions: &HashMap) -> Vec { + let mut content = Vec::new(); + + if let Some(member_types) = member_list(&self.member_types, descriptions) { + content.push(Block::Header( + 2, + Attr::null(), + vec![Inline::Str("Member Types".to_string())], + )); + + content.push(member_types); + } + + if let Some(member_functions) = member_list(self.member_functions.keys(), descriptions) { + content.push(Block::Header( + 2, + Attr::null(), + vec![Inline::Str("Member Functions".to_string())], + )); + + content.push(member_functions); + } + + if let Some(member_variables) = member_list(self.member_variables.keys(), descriptions) { + content.push(Block::Header( + 2, + Attr::null(), + vec![Inline::Str("Member Variables".to_string())], + )); + + content.push(member_variables); + } + + content + } + + fn leftovers(&self) -> HashMap { + self.member_functions + .iter() + .map(|(usr, func)| (usr.clone(), Entity::from(func.clone()))) + .chain( + self.member_variables + .iter() + .map(|(usr, var)| (usr.clone(), Entity::from(var.clone()))), + ) + .collect() + } +} + +impl PandocDisplay for Variable {} + +impl PandocDisplay for Function {} + +fn str_block(content: String) -> Block { + Block::Plain(vec![Inline::Str(content)]) +} + +fn entity_link(usr: &Usr, name: String) -> Inline { + use pandoc_types::definition::Target; + use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; + + use std::iter::once; + + // https://url.spec.whatwg.org/#fragment-percent-encode-set + const FRAGMENT: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'<') + .add(b'>') + .add(b'`') + .add(b'#') + .add(b'?') + .add(b'{') + .add(b'}'); + + Inline::Link( + Attr::null(), + vec![Inline::Code(Attr::null(), name)], + Target( + once("./") + .chain(utf8_percent_encode(&usr.0, FRAGMENT)) + .collect(), + String::new(), + ), + ) +} + +fn raw_markdown(text: String) -> Block { + use pandoc_types::definition::Format; + Block::RawBlock(Format(String::from("markdown")), text) +} + +fn member_list<'a>( + members: impl IntoIterator, + descriptions: &HashMap, +) -> Option { + let definitions: Vec<(Vec, Vec>)> = members + .into_iter() + .filter_map(|usr| { + let name = &descriptions.get(usr)?.name; + Some(( + vec![entity_link(usr, name.clone())], + vec![vec![str_block( + descriptions.get(usr).unwrap().brief.clone(), + )]], + )) + }) + .collect(); + + if definitions.is_empty() { + None + } else { + Some(Block::DefinitionList(definitions)) + } +} diff --git a/src/pandoc/types.rs b/src/pandoc/types.rs new file mode 100644 index 0000000..dc5be64 --- /dev/null +++ b/src/pandoc/types.rs @@ -0,0 +1,39 @@ +use crate::pandoc::{Block, Inline}; + +#[derive(Debug, Clone)] +pub(super) struct Class { + inners: Vec, +} + +#[derive(Debug, Clone)] +struct Inner { + kind: InnerKind, + name: String, + //refid: String +} + +#[derive(Debug, Clone)] +enum InnerKind { + Class, + Enum, +} + +impl std::fmt::Display for InnerKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + InnerKind::Class => write!(f, "class"), + InnerKind::Enum => write!(f, "enum"), + } + } +} + +impl From for (Vec, Vec>) { + fn from(inner: Inner) -> (Vec, Vec>) { + ( + vec![Inline::Str(inner.name)], + vec![vec![Block::Plain(vec![Inline::Str( + inner.kind.to_string(), + )])]], + ) + } +} diff --git a/src/parsing.rs b/src/parsing.rs new file mode 100644 index 0000000..137b89d --- /dev/null +++ b/src/parsing.rs @@ -0,0 +1,457 @@ +use crate::entities::*; + +use clang::{Clang, Index}; +use codemap::CodeMap; + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +pub(crate) fn parse_file(path: T, mut extra_args: Vec<&str>) -> EntitiesManager +where + T: Into, + T: AsRef, + T: ToString, +{ + let mut codemap = CodeMap::new(); + let file_map = codemap.add_file(path.to_string(), std::fs::read_to_string(&path).unwrap()); + let file_span = file_map.span; + + let clang = Clang::new().unwrap(); + let index = Index::new(&clang, true, false); + let mut parser = index.parser(path); + parser.skip_function_bodies(true); + + extra_args.push("-Werror=documentation"); + parser.arguments(&extra_args); + + if log_enabled!(log::Level::Debug) { + for extra_arg in extra_args { + debug!("Extra LibClang argument: {:?}", extra_arg); + } + } + + let trans_unit = parser.parse().unwrap(); + + let mut manager = EntitiesManager::new(); + + trans_unit.get_entity().visit_children(|entity, _parent| { + if entity.is_in_system_header() { + trace!( + "Entity is in system header, skipping: USR = {:?}", + entity.get_display_name() + ); + return clang::EntityVisitResult::Continue; + } + + // TODO: wrap this callback in another function so that we can use the + // "?" operator instead of all these `match`es + let usr = match entity.get_usr() { + Some(usr) => usr, + None => return clang::EntityVisitResult::Continue, + }; + trace!("Entity with USR = {:?}", usr); + + let name = match entity.get_name() { + Some(name) => name, + None => return clang::EntityVisitResult::Continue, + }; + + debug!("Parsing toplevel entity with name = {:?}", name); + + let entity = entity.into_poseidoc_entity(&mut manager); + + manager.insert_toplevel(usr.into(), entity); + + clang::EntityVisitResult::Continue + }); + + use codemap_diagnostic::{ColorConfig, Emitter}; + + let mut emitter = Emitter::stderr(ColorConfig::Auto, Some(&codemap)); + + for diagnostic in trans_unit.get_diagnostics().iter() { + let main_diag = match clang_diag_to_codemap_diag(&diagnostic, &file_span) { + Some(diag) => diag, + None => continue, + }; + + let sub_diags = diagnostic + .get_children() + .into_iter() + .filter_map(|diagnostic| clang_diag_to_codemap_diag(&diagnostic, &file_span)); + + let fix_it_diags = diagnostic + .get_fix_its() + .into_iter() + .map(|fix_it| clang_fix_it_to_codemap_diag(&fix_it, &file_span)); + + emitter.emit( + &std::iter::once(main_diag) + .chain(sub_diags) + .chain(fix_it_diags) + .collect::>(), + ); + } + + manager +} + +fn clang_diag_to_codemap_diag( + diagnostic: &clang::diagnostic::Diagnostic, + file_span: &codemap::Span, +) -> Option { + use codemap_diagnostic::{Diagnostic, Level, SpanLabel, SpanStyle}; + + let ranges = diagnostic.get_ranges(); + + let (begin, end) = if ranges.is_empty() { + let location = diagnostic.get_location(); + + ( + location.get_file_location().offset as u64, + location.get_file_location().offset as u64, + ) + } else { + let range = diagnostic.get_ranges()[0]; + if !range.is_in_main_file() { + warn!("Skipping diagnostic: {:?}", diagnostic); + return None; + } + ( + range.get_start().get_file_location().offset as u64, + range.get_end().get_file_location().offset as u64, + ) + }; + + let label = SpanLabel { + span: file_span.subspan(begin, end), + label: None, + style: SpanStyle::Primary, + }; + + Some(Diagnostic { + level: match diagnostic.get_severity() { + clang::diagnostic::Severity::Ignored => return None, + clang::diagnostic::Severity::Note => Level::Note, + clang::diagnostic::Severity::Warning => Level::Warning, + clang::diagnostic::Severity::Error => Level::Error, + clang::diagnostic::Severity::Fatal => Level::Error, + }, + message: diagnostic.get_text(), + //code: Some("-Werror=documentation".to_string()), + code: None, + spans: vec![label], + }) +} + +fn clang_fix_it_to_codemap_diag( + fix_it: &clang::diagnostic::FixIt, + file_span: &codemap::Span, +) -> codemap_diagnostic::Diagnostic { + use clang::diagnostic::FixIt; + use codemap_diagnostic::{Diagnostic, Level, SpanLabel, SpanStyle}; + + let label = match fix_it { + FixIt::Deletion(range) => { + let begin = range.get_start().get_file_location().offset as u64; + let end = range.get_end().get_file_location().offset as u64; + + SpanLabel { + span: file_span.subspan(begin, end), + label: Some(String::from("Try deleting this")), + style: SpanStyle::Primary, + } + } + FixIt::Insertion(range, text) => { + let location = range.get_file_location().offset as u64; + + SpanLabel { + span: file_span.subspan(location, location), + label: Some(format!("Try insterting {:?}", text)), + style: SpanStyle::Primary, + } + } + FixIt::Replacement(range, text) => { + let begin = range.get_start().get_file_location().offset as u64; + let end = range.get_end().get_file_location().offset as u64; + + SpanLabel { + span: file_span.subspan(begin, end), + label: Some(format!("Try replacing this with {:?}", text)), + style: SpanStyle::Primary, + } + } + }; + + Diagnostic { + level: Level::Help, + message: String::from("Possible fix"), + //code: Some("-Werror=documentation".to_string()), + code: None, + spans: vec![label], + } +} + +pub(crate) struct Comment(String); + +impl Comment { + pub fn from_raw(raw: String) -> Self { + #[derive(Debug)] + enum CommentStyle { + // Comments of type `/**` or `/*!` + Starred, + // Comments of type `///` + SingleLine, + } + + let mut chars = raw.chars(); + let style = match &chars.as_str()[..3] { + "/*!" | "/**" => CommentStyle::Starred, + "///" => CommentStyle::SingleLine, + _ => panic!("Comment is empty or doesn't start with either `///`, `/**`, or `/*!`"), + }; + + chars.nth(2); + + let mut result = String::new(); + + 'parse_loop: loop { + let maybe_space = chars.next(); + let mut empty_line = false; + match maybe_space { + // TODO: Warn on empty comments + None => break, + Some(' ') => {} + Some('\n') => { + empty_line = true; + result.push('\n'); + } + Some(ch) => result.push(ch), + } + + if !empty_line { + let rest = chars.as_str(); + match rest.find('\n') { + None => { + result.push_str(rest); + break; + } + Some(position) => { + result.push_str(&rest[..position + 1]); + chars.nth(position); + } + } + } + + // Beginning of the line + let first_non_ws_ch = 'ws_loop: loop { + let maybe_whitespace = chars.next(); + match maybe_whitespace { + None => break 'parse_loop, + Some(ch) if ch.is_whitespace() => continue, + Some(ch) => break 'ws_loop ch, + } + }; + + match style { + CommentStyle::Starred if first_non_ws_ch == '*' => { + if &chars.as_str()[..1] == "/" { + break; + } + } + CommentStyle::Starred => result.push(first_non_ws_ch), + CommentStyle::SingleLine => { + assert!(first_non_ws_ch == '/'); + let rest = chars.as_str(); + if &rest[..2] == "//" { + chars.nth(1); + } else if &rest[..1] == "/" { + chars.nth(0); + } else { + panic!("Could not parse comment"); + } + } + } + } + + Self(result) + } +} + +impl From for Usr { + fn from(usr: clang::Usr) -> Self { + Self(usr.0) + } +} + +trait FromClangEntity { + /// Is responsible for inserting its documentation into the entities manager + fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self; +} + +trait IntoPoseidocEntity { + /// Useful for the `clang_entity.into_poseidoc_entity(&mut manager)` syntax. Implement + /// `FromClangEntity` instead. + fn into_poseidoc_entity(self, manager: &mut EntitiesManager) -> T; +} + +impl IntoPoseidocEntity for clang::Entity<'_> +where + T: FromClangEntity, +{ + fn into_poseidoc_entity(self, manager: &mut EntitiesManager) -> T { + T::from_clang_entity(self, manager) + } +} + +fn get_description(entity: &clang::Entity) -> Description { + let name = entity.get_display_name().unwrap(); + + if let (Some(brief), Some(comment)) = (entity.get_comment_brief(), entity.get_comment()) { + Description { + name, + brief, + detailed: Comment::from_raw(comment).0, + } + } else { + Description::with_name(name) + } +} + +impl FromClangEntity for Entity { + fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { + use clang::EntityKind; + + match entity.get_kind() { + EntityKind::Namespace => Self::NameSpace(entity.into_poseidoc_entity(manager)), + EntityKind::FieldDecl | EntityKind::VarDecl => { + Self::Variable(entity.into_poseidoc_entity(manager)) + } + EntityKind::FunctionDecl | EntityKind::Method | EntityKind::FunctionTemplate => { + Self::Function(entity.into_poseidoc_entity(manager)) + } + EntityKind::ClassDecl | EntityKind::StructDecl | EntityKind::ClassTemplate => { + Self::Class(entity.into_poseidoc_entity(manager)) + } + _ => panic!("Unsupported entity: {:?}", entity), + } + } +} + +impl FromClangEntity for NameSpace { + fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { + match entity.get_kind() { + clang::EntityKind::Namespace => {} + _ => panic!("Trying to parse a non-variable into a variable"), + } + debug!("Parsing NameSpace"); + + let members = entity + .get_children() + .into_iter() + .map(|child| { + let usr = child.get_usr().unwrap().into(); + trace!("Namespace has member: {:?}", usr); + (usr, child.into_poseidoc_entity(manager)) + }) + .collect(); + + manager.insert(entity.get_usr().unwrap().into(), get_description(&entity)); + + NameSpace { members } + } +} + +impl FromClangEntity for Variable { + fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { + match entity.get_kind() { + clang::EntityKind::VarDecl | clang::EntityKind::FieldDecl => {} + _ => panic!("Trying to parse a non-variable into a variable"), + } + debug!("Parsing Variable"); + + let r#type = entity.get_type().unwrap().get_display_name(); + trace!("Variable has type: {:?}", r#type); + + manager.insert(entity.get_usr().unwrap().into(), get_description(&entity)); + + Variable { r#type } + } +} + +impl FromClangEntity for Function { + fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { + match entity.get_kind() { + clang::EntityKind::Method + | clang::EntityKind::FunctionDecl + | clang::EntityKind::FunctionTemplate => {} + _ => panic!("Trying to parse a non-function into a function"), + } + debug!("Parsing Function"); + + let return_type = entity.get_result_type().unwrap().get_display_name(); + trace!("Function has return type: {:?}", return_type); + let arguments = entity + .get_arguments() + .unwrap() + .into_iter() + .map(|arg| { + let name = arg.get_display_name().unwrap(); + let r#type = arg.get_type().unwrap().get_display_name(); + trace!("Function has argument {:?} of type {:?}", name, r#type); + FunctionArgument { name, r#type } + }) + .collect(); + + manager.insert(entity.get_usr().unwrap().into(), get_description(&entity)); + + Function { + arguments, + return_type, + } + } +} + +impl FromClangEntity for Class { + fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { + match entity.get_kind() { + clang::EntityKind::ClassDecl + | clang::EntityKind::StructDecl + | clang::EntityKind::ClassTemplate => {} + _ => panic!("Trying to parse a non-class into a class"), + } + debug!("Parsing Class"); + + let mut member_types = Vec::new(); + let mut member_functions = HashMap::new(); + let mut member_variables = HashMap::new(); + + for child in entity.get_children() { + trace!("Class has child: {:?}", child); + + let child_usr = child.get_usr().unwrap().into(); + + use clang::EntityKind; + match child.get_kind() { + EntityKind::ClassDecl | EntityKind::StructDecl | EntityKind::TypeAliasDecl => { + member_types.push(child_usr); + } + EntityKind::Method => { + member_functions.insert(child_usr, child.into_poseidoc_entity(manager)); + } + EntityKind::FieldDecl => { + member_variables.insert(child_usr, child.into_poseidoc_entity(manager)); + } + _ => trace!("Skipping child"), + } + } + + manager.insert(entity.get_usr().unwrap().into(), get_description(&entity)); + + Class { + member_types, + member_functions, + member_variables, + } + } +} -- cgit v1.2.3