// A literate program combines code and prose (documentation) in one file format. // The following ascii diagram depicts how ascii generates this html. // +-----------------------------------------+ // | File containing the program description | // | peppered with scraps of program code. | // | This is what the programmer works on. | // | (e.g. source.rs) | // +-----------------------------------------+ // | // v // o---------------------------o // | Rocco | // o---------------------------o // | // +------------+-------------+ // | | // v v // +------------------+ +--------------------------+ // | Prose | | Code | // +------------------+ +--------------------------+ // ------------------------------------------------------------------- // The source and prose below is rocco's output on its own source code. // `Language` represents various attributes of a language used to // generate and parse code and prose. #[derive(Debug, Content, serde::Serialize, serde::Deserialize)] pub struct Language { // of language (e.g, python, rust, go) name: String, // the delimiter which denotes a comment (//) comment: String, } // `LANGUAGES` represents a one time initialized json map of languages with their various attributes. static LANGUAGES: Lazy> = Lazy::new(|| { let lang_json = include_str!("assets/languages.json"); serde_json::from_str(lang_json.as_ref()).expect("Language map initialization failed") }); // A `Section` represents a parsed chunk of code and prose. #[derive(Content, Debug)] pub struct Section { num: usize, docs_html: String, code_html: String, } // The `Docco` instance contains all items to successfully render a source code. #[derive(Content)] pub struct Docco { sections: Vec
, css: String, html: String, filename: String, output: String, extension: String, language: String, doc_symbol: String, } impl Docco { // Creates a new Docco instance. pub fn new(source: &PathBuf, output: Option) -> Result { if !source.is_file() { return Err(Error::DoccoInitFailed); } let source_str = source.as_path().display().to_string(); let output = if let Some(output) = output { output.as_path().display().to_string() } else { let a = source_str.find('.').unwrap(); let slice = &source_str[..a]; format!("{}.html", slice) }; let (language, comment, extension) = if let Some(ext) = source_str.split(".").last() { let language = LANGUAGES .get(ext) .map(|l| l) .ok_or(Error::UnsupportedExt(ext.to_string()))?; (&language.name, &language.comment, ext.to_string()) } else { return Err(Error::DoccoInitFailed); }; Ok(Self { sections: vec![], filename: source_str, css: include_str!("assets/template.css").to_string(), html: include_str!("assets/template.html").to_string(), output, language: language.to_string(), extension, doc_symbol: comment.to_string(), }) } // Sets the output html file pub fn set_output(&mut self, output: &str) { self.output = output.to_string(); } // Renders the parsed sections to an html file. pub fn render(&self) -> Result<(), Error> { let template = Template::new(self.html.as_str()).map_err(|_| Error::InvalidTemplateSource)?; let path = std::path::Path::new(&self.output); let prefix = path.parent().unwrap(); std::fs::create_dir_all(prefix).unwrap(); template.render_to_file(&self.output, self)?; Ok(()) } // Parses code blocks fn parse_code( &self, iter: &mut Peekable>>, code_buffer: &mut String, ) -> Result<(), Error> { while let Some(Ok(next_line)) = iter.peek() { let line_trimmed = next_line.trim_start(); if !line_trimmed.starts_with(&self.doc_symbol) && !line_trimmed.is_empty() { let next_line = next_line.replace("<", "<"); let next_line = next_line.replace(">", ">"); code_buffer.push_str(&next_line); if !line_trimmed.ends_with("\n") { code_buffer.push_str("\n"); } iter.next(); } else { return Ok(()); } } Ok(()) } // Parses documentation blocks fn parse_doc( &self, iter: &mut Peekable>>, doc_buffer: &mut String, ) -> Result<(), Error> { while let Some(Ok(next_line)) = iter.peek() { if next_line.trim().starts_with(&self.doc_symbol) { // rust specific doc comments if next_line.trim().starts_with("///") { doc_buffer.push_str(next_line.trim_start()); doc_buffer.push_str("\n"); } else { let next_line = next_line.trim_start().trim_start_matches(&self.doc_symbol); doc_buffer.push_str(next_line); doc_buffer.push_str("\n"); } iter.next(); } else { return Ok(()); } } Ok(()) } // High level method that iterates over lines in the given source file // and parses code and prose blocks as `Section`s. Additionally, processes the // documentation through markdown. pub fn parse(&mut self) -> Result<(), Error> { let fs = BufReader::new(OpenOptions::new().read(true).open(&self.filename)?); let mut lines = fs.lines().peekable(); let mut idx = 0; while let Some(Ok(next_line)) = lines.peek() { if next_line.is_empty() { lines.next(); continue; } let mut doc = String::new(); let mut code = String::new(); self.parse_doc(&mut lines, &mut doc)?; self.parse_code(&mut lines, &mut code)?; let docs_html = comrak::markdown_to_html(&doc, &comrak::ComrakOptions::default()); let section = Section { num: idx, docs_html, code_html: code, }; self.sections.push(section); idx += 1; } Ok(()) } }