doubter-impl 0.1.0

Internal implementation of doubter
Documentation
use proc_macro2::TokenStream;
use std::env;
use std::io;
use std::io::{BufWriter, Write};
use std::path::PathBuf;

use proc_macro2::Span;
use quote::TokenStreamExt;
use syn::Ident;

use config::{Config, Mode};
use extract::extract_code_blocks;
use tree::{Dir, MarkdownFile, Node, Tree};
use util::{io_error, read_to_string};

#[derive(Debug)]
pub struct RenderContext {
    config: Config,
    root_dir: PathBuf,
    tree: Tree,
}

impl RenderContext {
    pub fn init(config: Config) -> io::Result<RenderContext> {
        let root_dir = env::var_os("CARGO_MANIFEST_DIR")
            .map(PathBuf::from)
            .ok_or_else(|| io_error("the environment variable `CARGO_MANIFEST_DIR` is not set"))?;

        let mut tree = Tree::default();
        for pattern in &config.includes {
            tree.register_md_files(pattern, &root_dir)?;
        }

        Ok(RenderContext {
            config,
            root_dir,
            tree,
        })
    }

    fn mode(&self) -> Mode {
        if self.config.use_external_doc {
            Mode::Raw
        } else {
            self.config.mode.unwrap_or_else(|| Mode::Raw)
        }
    }

    pub fn write<W>(&self, writer: &mut W) -> io::Result<()>
    where
        W: Write,
    {
        let mut tokens = TokenStream::new();
        self.render(&mut tokens)?;

        let mut writer = BufWriter::new(writer);
        writer.write_all(tokens.to_string().as_bytes())
    }

    pub fn render(&self, tokens: &mut TokenStream) -> io::Result<()> {
        (Renderer {
            context: self,
            tokens,
        }).render_tree(&self.tree)
    }
}

#[derive(Debug)]
struct Renderer<'a> {
    context: &'a RenderContext,
    tokens: &'a mut TokenStream,
}

impl<'a> Renderer<'a> {
    fn with_tokens<F>(&self, f: F) -> io::Result<TokenStream>
    where
        F: FnOnce(&mut Renderer) -> io::Result<()>,
    {
        let mut tokens = TokenStream::new();
        f(&mut Renderer {
            tokens: &mut tokens,
            context: &*self.context,
        })?;
        Ok(tokens)
    }

    fn render_tree(&mut self, tree: &Tree) -> io::Result<()> {
        let inner = self.with_tokens(|r| r.render_dir(&tree.root))?;
        self.tokens.append_all(quote!(
            pub mod doctests {
                #inner
            }
        ));

        Ok(())
    }

    fn render_node(&mut self, node: &Node) -> io::Result<()> {
        match *node {
            Node::Dir(ref dir) => self.render_dir(dir),
            Node::File(ref file) => self.render_file(file),
        }
    }

    fn render_dir(&mut self, dir: &Dir) -> io::Result<()> {
        for (segment, node) in dir.iter() {
            let module_name = match segment {
                s if s == ".." => Ident::new("__PARENT__", Span::call_site()),
                segment => Ident::new(&sanitize::sanitize(segment), Span::call_site()),
            };

            let inner = self.with_tokens(|r| r.render_node(node))?;
            self.tokens.append_all(quote! {
                pub mod #module_name {
                    #inner
                }
            });
        }
        Ok(())
    }

    fn render_file(&mut self, file: &MarkdownFile) -> io::Result<()> {
        match self.context.mode() {
            Mode::Raw => {
                if self.context.config.use_external_doc {
                    let path = file.path.to_string_lossy();
                    self.tokens.append_all(quote!(#![doc(include = #path)]));
                } else {
                    let content = read_to_string(&file.path)?;
                    self.tokens.append_all(quote!(#![doc = #content]));
                }
            }
            Mode::Extract => {
                let content = read_to_string(&file.path)?;
                let blocks = extract_code_blocks(&content);

                for block in blocks {
                    let header = format!("```{}", block.info);
                    let content = &block.content;
                    let const_name = Ident::new(&format!("line_{}", block.line), Span::call_site());
                    self.tokens.append_all(quote!{
                        #[doc = #header]
                        #(#[doc = #content])*
                        #[doc = "```"]
                        #[allow(non_upper_case_globals)]
                        pub const #const_name: () = ();
                    });
                }
            }
        }
        Ok(())
    }
}

mod sanitize {
    #[allow(unused_imports, deprecated)]
    use std::ascii::AsciiExt;
    use std::ffi::OsStr;

    pub fn sanitize<S>(s: S) -> String
    where
        S: AsRef<OsStr>,
    {
        s.as_ref()
            .to_string_lossy()
            .to_ascii_lowercase()
            .replace(|c: char| !c.is_ascii() || !c.is_alphanumeric(), "_")
    }

    #[test]
    fn test_sanitize() {
        assert_eq!(sanitize("foo.md"), "foo_md");
        assert_eq!(sanitize("_foo.md"), "_foo_md");
        assert_eq!(sanitize("with whitespace.md"), "with_whitespace_md");
        assert_eq!(sanitize("with-hyphen.md"), "with_hyphen_md");
        assert_eq!(sanitize("with%non&ascii.md"), "with_non_ascii_md");
    }
}