<commit_before>use std::fs::{self, File};
use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::ffi::OsStr;
use liquid::Value;
use rss;
use jsonfeed::Feed;
use jsonfeed;
use sass_rs::{compile_file as sass_compile_file, Options as SassOptions};

use datetime;
use document::Document;
use error::*;
use config::{Config, SortOrder};
use files::FilesBuilder;
use frontmatter;

/// The primary build function that transforms a directory into a site
pub fn build(config: &Config) -> Result<()> {
    trace!("Build configuration: {:?}", config);

    let source = Path::new(&config.source);
    let dest = Path::new(&config.dest);

    let template_extensions: Vec<&OsStr> = config
        .template_extensions
        .iter()
        .map(OsStr::new)
        .collect();

    let layouts = source.join(&config.layouts);
    let mut layouts_cache = HashMap::new();
    let posts_path = source.join(&config.posts);

    debug!("Layouts directory: {:?}", layouts);
    debug!("Posts directory: {:?}", posts_path);
    debug!("Draft mode enabled: {}", config.include_drafts);
    if config.include_drafts {
        debug!("Draft directory: {:?}", config.drafts);
    }

    let mut documents = vec![];

    let ignore_dest = {
        let ignore_dest = dest.join("**/*");
        let ignore_dest = ignore_dest
            .to_str()
            .ok_or_else(|| format!("Cannot convert pathname {:?} to UTF-8", dest))?
            .to_owned();
        Some(ignore_dest)
    };

    let mut page_files = FilesBuilder::new(source)?;
    page_files
        .add_ignore(&format!("!{}", config.posts))?
        .add_ignore(&format!("!{}/**", config.posts))?
        .add_ignore(&format!("{}/**/_*", config.posts))?
        .add_ignore(&format!("{}/**/_*/**", config.posts))?;
    for line in &config.ignore {
        page_files.add_ignore(line.as_str())?;
    }
    if let Some(ref ignore_dest) = ignore_dest {
        page_files.add_ignore(ignore_dest)?;
    }
    let page_files = page_files.build()?;
    for file_path in page_files
            .files()
            .filter(|p| {
                        template_extensions
                            .contains(&p.extension().unwrap_or_else(|| OsStr::new("")))
                    }) {
        // if the document is in the posts folder it's considered a post
        let src_path = source.join(file_path.as_path());
        let is_post = src_path.starts_with(posts_path.as_path());

        let mut default_front = frontmatter::FrontmatterBuilder::new()
            .set_post(is_post)
            .set_draft(false)
            .set_excerpt_separator(config.excerpt_separator.clone());
        if is_post {
            default_front = default_front.set_permalink(config.post_path.clone());
        }

        let doc = Document::parse(source, &file_path, &file_path, default_front)?;
        if !doc.front.is_draft || config.include_drafts {
            documents.push(doc);
        }
    }

    if config.include_drafts {
        let drafts_root = source.join(&config.drafts);
        let mut draft_files = FilesBuilder::new(drafts_root.as_path())?;
        for line in &config.ignore {
            draft_files.add_ignore(line.as_str())?;
        }
        let draft_files = draft_files.build()?;
        for file_path in draft_files
                .files()
                .filter(|p| {
                            template_extensions
                                .contains(&p.extension().unwrap_or_else(|| OsStr::new("")))
                        }) {
            let new_path = posts_path.join(&file_path);
            let new_path = new_path
                .strip_prefix(source)
                .expect("Entry not in source folder");

            let is_post = true;

            let mut default_front = frontmatter::FrontmatterBuilder::new()
                .set_post(is_post)
                .set_draft(true)
                .set_excerpt_separator(config.excerpt_separator.clone());
            if is_post {
                default_front = default_front.set_permalink(config.post_path.clone());
            }

            let doc = Document::parse(&drafts_root, &file_path, new_path, default_front)?;
            documents.push(doc);
        }
    }

    // January 1, 1970 0:00:00 UTC, the beginning of time
    let default_date = datetime::DateTime::default();

    let (mut posts, documents): (Vec<Document>, Vec<Document>) =
        documents.into_iter().partition(|x| x.front.is_post);

    // sort documents by date, if there's no date (none was provided or it couldn't be read) then
    // fall back to the default date
    posts.sort_by(|a, b| {
                      b.front
                          .published_date
                          .unwrap_or(default_date)
                          .cmp(&a.front.published_date.unwrap_or(default_date))
                  });

    match config.post_order {
        SortOrder::Asc => posts.reverse(),
        SortOrder::Desc => (),
    }

    // collect all posts attributes to pass them to other posts for rendering
    let simple_posts_data: Vec<Value> = posts
        .iter()
        .map(|x| Value::Object(x.attributes.clone()))
        .collect();

    trace!("Generating posts");
    for (i, post) in &mut posts.iter_mut().enumerate() {
        trace!("Generating {}", post.url_path);

        // posts are in reverse date order, so previous post is the next in the list (+1)
        if let Some(previous) = simple_posts_data.get(i + 1) {
            post.attributes
                .insert("previous".to_owned(), previous.clone());
        }
        if i >= 1 {
            if let Some(next) = simple_posts_data.get(i - 1) {
                post.attributes.insert("next".to_owned(), next.clone());
            }
        }

        for dump in config.dump.iter().filter(|d| d.is_doc()) {
            trace!("Dumping {:?}", dump);
            let (content, ext) = post.render_dump(*dump)?;
            let mut file_path = post.file_path.clone();
            let file_name = file_path
                .file_stem()
                .and_then(|p| p.to_str())
                .expect("page must have file name")
                .to_owned();
            let file_name = format!("_{}.{}.{}", file_name, dump, ext);
            file_path.set_file_name(file_name);
            trace!("Generating {:?}", file_path);
            create_document_file(content, &file_path, dest)?;
        }

        let mut context = post.get_render_context(&simple_posts_data);

        post.render_excerpt(&mut context, source, &config.syntax_highlight.theme)
            .chain_err(|| format!("Failed to render excerpt for {:?}", post.file_path))?;
        let post_html = post.render(&mut context,
                                    source,
                                    &layouts,
                                    &mut layouts_cache,
                                    &config.syntax_highlight.theme)
            .chain_err(|| format!("Failed to render for {:?}", post.file_path))?;
        create_document_file(post_html, &post.file_path, dest)?;
    }

    // check if we should create an RSS file and create it!
    if let Some(ref path) = config.rss {
        create_rss(path, dest, config, &posts)?;
    }
    // check if we should create an jsonfeed file and create it!
    if let Some(ref path) = config.jsonfeed {
        create_jsonfeed(path, dest, config, &posts)?;
    }

    // during post rendering additional attributes such as content were
    // added to posts. collect them so that non-post documents can access them
    let posts_data: Vec<Value> = posts
        .into_iter()
        .map(|x| Value::Object(x.attributes))
        .collect();

    trace!("Generating other documents");
    for mut doc in documents {
        trace!("Generating {}", doc.url_path);

        for dump in config.dump.iter().filter(|d| d.is_doc()) {
            trace!("Dumping {:?}", dump);
            let (content, ext) = doc.render_dump(*dump)?;
            let mut file_path = doc.file_path.clone();
            let file_name = file_path
                .file_stem()
                .and_then(|p| p.to_str())
                .expect("page must have file name")
                .to_owned();
            let file_name = format!("_{}.{}.{}", file_name, dump, ext);
            file_path.set_file_name(file_name);
            trace!("Generating {:?}", file_path);
            create_document_file(content, &file_path, dest)?;
        }

        let mut context = doc.get_render_context(&posts_data);
        let doc_html = doc.render(&mut context,
                                  source,
                                  &layouts,
                                  &mut layouts_cache,
                                  &config.syntax_highlight.theme)
            .chain_err(|| format!("Failed to render for {:?}", doc.file_path))?;
        create_document_file(doc_html, doc.file_path, dest)?;
    }

    // copy all remaining files in the source to the destination
    // compile SASS along the way
    {
        info!("Copying remaining assets");
        let mut sass_opts = SassOptions::default();
        //  FIXME: make this a config option
        sass_opts.include_paths =
            vec![source.join("_sass").into_os_string().into_string().unwrap()];
        let mut asset_files = FilesBuilder::new(source)?;
        for line in &config.ignore {
            asset_files.add_ignore(line.as_str())?;
        }
        if let Some(ref ignore_dest) = ignore_dest {
            asset_files.add_ignore(ignore_dest)?;
        }
        let asset_files = asset_files.build()?;
        for file_path in asset_files
                .files()
                .filter(|p| {
                            !template_extensions
                                 .contains(&p.extension().unwrap_or_else(|| OsStr::new("")))
                        }) {
            {
                let parent_dir = file_path.parent();
                if let Some(parent_dir) = parent_dir {
                    let parent_dir = dest.join(parent_dir);
                    debug!("Creating new directory {:?}", parent_dir);
                    fs::create_dir_all(parent_dir)?;
                }
            }
            let src_file = source.join(file_path.as_path());

            if file_path.extension().unwrap_or_else(|| OsStr::new("")) == "scss" {
                let content = sass_compile_file(src_file.as_path(), sass_opts.clone())?;
                let mut dest_file = dest.join(file_path.clone());
                dest_file.set_extension("css");

                let mut file =
                    File::create(&dest_file)
                        .map_err(|e| format!("Could not create {:?}: {}", file_path, e))?;

                file.write_all(content.as_bytes())?;

            } else {
                let dest_file = dest.join(file_path);
                debug!("Copying {:?} to {:?}", src_file, dest_file);
                fs::copy(src_file.as_path(), dest_file.as_path())
                    .map_err(|e| format!("Could not copy {:?} into {:?}: {}", src_file, dest_file, e))?;
            }
        }
    }

    Ok(())
}

// creates a new RSS file with the contents of the site blog
fn create_rss(path: &str, dest: &Path, config: &Config, posts: &[Document]) -> Result<()> {
    match (&config.name, &config.description, &config.link) {
        // these three fields are mandatory in the RSS standard
        (&Some(ref name), &Some(ref description), &Some(ref link)) => {
            trace!("Generating RSS data");

            let items: Result<Vec<rss::Item>> = posts.iter().map(|doc| doc.to_rss(link)).collect();
            let items = items?;

            let channel = rss::ChannelBuilder::default()
                .title(name.to_owned())
                .link(link.to_owned())
                .description(description.to_owned())
                .items(items)
                .build()?;

            let rss_string = channel.to_string();
            trace!("RSS data: {}", rss_string);

            let rss_path = dest.join(path);

            let mut rss_file = File::create(&rss_path)?;
            rss_file
                .write_all(br#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
            rss_file.write_all(&rss_string.into_bytes())?;
            rss_file.write_all(b"\n")?;

            info!("Created RSS file at {}", rss_path.display());
            Ok(())
        }
        _ => Err(ErrorKind::ConfigFileMissingFields.into()),
    }
}
// creates a new jsonfeed file with the contents of the site blog
fn create_jsonfeed(path: &str, dest: &Path, config: &Config, posts: &[Document]) -> Result<()> {
    match (&config.name, &config.description, &config.link) {
        (&Some(ref name), &Some(ref desc), &Some(ref link)) => {
            trace!("Generating jsonfeed data");

            let jsonitems = posts.iter().map(|doc| doc.to_jsonfeed(link)).collect();

            let feed = Feed {
                title: name.to_string(),
                items: jsonitems,
                home_page_url: Some(link.to_string()),
                description: Some(desc.to_string()),
                ..Default::default()
            };

            let jsonfeed_string = jsonfeed::to_string(&feed).unwrap();
            let jsonfeed_path = dest.join(path);
            let mut jsonfeed_file = File::create(&jsonfeed_path)?;
            jsonfeed_file.write_all(&jsonfeed_string.into_bytes())?;

            info!("Created jsonfeed file at {}", jsonfeed_path.display());
            Ok(())
        }
        _ => Err(ErrorKind::ConfigFileMissingFields.into()),
    }
}

fn create_document_file<S: AsRef<str>, T: AsRef<Path>, R: Into<PathBuf>>(content: S,
                                                                         relpath: T,
                                                                         dest: R)
                                                                         -> Result<()> {
    create_document_file_internal(content.as_ref(), relpath.as_ref(), dest.into())
}

fn create_document_file_internal(content: &str, relpath: &Path, dest: PathBuf) -> Result<()> {
    // construct target path
    let mut file_path = dest;
    file_path.push(relpath);

    // create target directories if any exist
    if let Some(parent) = file_path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("Could not create {:?}: {}", parent, e))?;
    }

    let mut file = File::create(&file_path)
        .map_err(|e| format!("Could not create {:?}: {}", file_path, e))?;

    file.write_all(content.as_bytes())?;
    info!("Created {}", file_path.display());
    Ok(())
}
<commit_msg>feat(scss): Implement compiling of SCSS files<commit_after>use std::fs::{self, File};
use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::ffi::OsStr;
use liquid::Value;
use rss;
use jsonfeed::Feed;
use jsonfeed;
use sass_rs;

use datetime;
use document::Document;
use error::*;
use config::{Config, SortOrder};
use files::FilesBuilder;
use frontmatter;

/// The primary build function that transforms a directory into a site
pub fn build(config: &Config) -> Result<()> {
    trace!("Build configuration: {:?}", config);

    let source = Path::new(&config.source);
    let dest = Path::new(&config.dest);

    let template_extensions: Vec<&OsStr> = config
        .template_extensions
        .iter()
        .map(OsStr::new)
        .collect();

    let layouts = source.join(&config.layouts);
    let mut layouts_cache = HashMap::new();
    let posts_path = source.join(&config.posts);

    debug!("Layouts directory: {:?}", layouts);
    debug!("Posts directory: {:?}", posts_path);
    debug!("Draft mode enabled: {}", config.include_drafts);
    if config.include_drafts {
        debug!("Draft directory: {:?}", config.drafts);
    }

    let mut documents = vec![];

    let ignore_dest = {
        let ignore_dest = dest.join("**/*");
        let ignore_dest = ignore_dest
            .to_str()
            .ok_or_else(|| format!("Cannot convert pathname {:?} to UTF-8", dest))?
            .to_owned();
        Some(ignore_dest)
    };

    let mut page_files = FilesBuilder::new(source)?;
    page_files
        .add_ignore(&format!("!{}", config.posts))?
        .add_ignore(&format!("!{}/**", config.posts))?
        .add_ignore(&format!("{}/**/_*", config.posts))?
        .add_ignore(&format!("{}/**/_*/**", config.posts))?;
    for line in &config.ignore {
        page_files.add_ignore(line.as_str())?;
    }
    if let Some(ref ignore_dest) = ignore_dest {
        page_files.add_ignore(ignore_dest)?;
    }
    let page_files = page_files.build()?;
    for file_path in page_files
            .files()
            .filter(|p| {
                        template_extensions
                            .contains(&p.extension().unwrap_or_else(|| OsStr::new("")))
                    }) {
        // if the document is in the posts folder it's considered a post
        let src_path = source.join(file_path.as_path());
        let is_post = src_path.starts_with(posts_path.as_path());

        let mut default_front = frontmatter::FrontmatterBuilder::new()
            .set_post(is_post)
            .set_draft(false)
            .set_excerpt_separator(config.excerpt_separator.clone());
        if is_post {
            default_front = default_front.set_permalink(config.post_path.clone());
        }

        let doc = Document::parse(source, &file_path, &file_path, default_front)?;
        if !doc.front.is_draft || config.include_drafts {
            documents.push(doc);
        }
    }

    if config.include_drafts {
        let drafts_root = source.join(&config.drafts);
        let mut draft_files = FilesBuilder::new(drafts_root.as_path())?;
        for line in &config.ignore {
            draft_files.add_ignore(line.as_str())?;
        }
        let draft_files = draft_files.build()?;
        for file_path in draft_files
                .files()
                .filter(|p| {
                            template_extensions
                                .contains(&p.extension().unwrap_or_else(|| OsStr::new("")))
                        }) {
            let new_path = posts_path.join(&file_path);
            let new_path = new_path
                .strip_prefix(source)
                .expect("Entry not in source folder");

            let is_post = true;

            let mut default_front = frontmatter::FrontmatterBuilder::new()
                .set_post(is_post)
                .set_draft(true)
                .set_excerpt_separator(config.excerpt_separator.clone());
            if is_post {
                default_front = default_front.set_permalink(config.post_path.clone());
            }

            let doc = Document::parse(&drafts_root, &file_path, new_path, default_front)?;
            documents.push(doc);
        }
    }

    // January 1, 1970 0:00:00 UTC, the beginning of time
    let default_date = datetime::DateTime::default();

    let (mut posts, documents): (Vec<Document>, Vec<Document>) =
        documents.into_iter().partition(|x| x.front.is_post);

    // sort documents by date, if there's no date (none was provided or it couldn't be read) then
    // fall back to the default date
    posts.sort_by(|a, b| {
                      b.front
                          .published_date
                          .unwrap_or(default_date)
                          .cmp(&a.front.published_date.unwrap_or(default_date))
                  });

    match config.post_order {
        SortOrder::Asc => posts.reverse(),
        SortOrder::Desc => (),
    }

    // collect all posts attributes to pass them to other posts for rendering
    let simple_posts_data: Vec<Value> = posts
        .iter()
        .map(|x| Value::Object(x.attributes.clone()))
        .collect();

    trace!("Generating posts");
    for (i, post) in &mut posts.iter_mut().enumerate() {
        trace!("Generating {}", post.url_path);

        // posts are in reverse date order, so previous post is the next in the list (+1)
        if let Some(previous) = simple_posts_data.get(i + 1) {
            post.attributes
                .insert("previous".to_owned(), previous.clone());
        }
        if i >= 1 {
            if let Some(next) = simple_posts_data.get(i - 1) {
                post.attributes.insert("next".to_owned(), next.clone());
            }
        }

        for dump in config.dump.iter().filter(|d| d.is_doc()) {
            trace!("Dumping {:?}", dump);
            let (content, ext) = post.render_dump(*dump)?;
            let mut file_path = post.file_path.clone();
            let file_name = file_path
                .file_stem()
                .and_then(|p| p.to_str())
                .expect("page must have file name")
                .to_owned();
            let file_name = format!("_{}.{}.{}", file_name, dump, ext);
            file_path.set_file_name(file_name);
            trace!("Generating {:?}", file_path);
            create_document_file(content, &file_path, dest)?;
        }

        let mut context = post.get_render_context(&simple_posts_data);

        post.render_excerpt(&mut context, source, &config.syntax_highlight.theme)
            .chain_err(|| format!("Failed to render excerpt for {:?}", post.file_path))?;
        let post_html = post.render(&mut context,
                                    source,
                                    &layouts,
                                    &mut layouts_cache,
                                    &config.syntax_highlight.theme)
            .chain_err(|| format!("Failed to render for {:?}", post.file_path))?;
        create_document_file(post_html, &post.file_path, dest)?;
    }

    // check if we should create an RSS file and create it!
    if let Some(ref path) = config.rss {
        create_rss(path, dest, config, &posts)?;
    }
    // check if we should create an jsonfeed file and create it!
    if let Some(ref path) = config.jsonfeed {
        create_jsonfeed(path, dest, config, &posts)?;
    }

    // during post rendering additional attributes such as content were
    // added to posts. collect them so that non-post documents can access them
    let posts_data: Vec<Value> = posts
        .into_iter()
        .map(|x| Value::Object(x.attributes))
        .collect();

    trace!("Generating other documents");
    for mut doc in documents {
        trace!("Generating {}", doc.url_path);

        for dump in config.dump.iter().filter(|d| d.is_doc()) {
            trace!("Dumping {:?}", dump);
            let (content, ext) = doc.render_dump(*dump)?;
            let mut file_path = doc.file_path.clone();
            let file_name = file_path
                .file_stem()
                .and_then(|p| p.to_str())
                .expect("page must have file name")
                .to_owned();
            let file_name = format!("_{}.{}.{}", file_name, dump, ext);
            file_path.set_file_name(file_name);
            trace!("Generating {:?}", file_path);
            create_document_file(content, &file_path, dest)?;
        }

        let mut context = doc.get_render_context(&posts_data);
        let doc_html = doc.render(&mut context,
                                  source,
                                  &layouts,
                                  &mut layouts_cache,
                                  &config.syntax_highlight.theme)
            .chain_err(|| format!("Failed to render for {:?}", doc.file_path))?;
        create_document_file(doc_html, doc.file_path, dest)?;
    }

    // copy all remaining files in the source to the destination
    // compile SASS along the way
    {
        info!("Copying remaining assets");
        let mut sass_opts = sass_rs::Options::default();
        //  FIXME: make this a config option
        sass_opts.include_paths =
            vec![source.join("_sass").into_os_string().into_string().unwrap()];
        let mut asset_files = FilesBuilder::new(source)?;
        for line in &config.ignore {
            asset_files.add_ignore(line.as_str())?;
        }
        if let Some(ref ignore_dest) = ignore_dest {
            asset_files.add_ignore(ignore_dest)?;
        }
        let asset_files = asset_files.build()?;
        for file_path in asset_files
                .files()
                .filter(|p| {
                            !template_extensions
                                 .contains(&p.extension().unwrap_or_else(|| OsStr::new("")))
                        }) {
            {
                let parent_dir = file_path.parent();
                if let Some(parent_dir) = parent_dir {
                    let parent_dir = dest.join(parent_dir);
                    debug!("Creating new directory {:?}", parent_dir);
                    fs::create_dir_all(parent_dir)?;
                }
            }
            let src_file = source.join(file_path.as_path());

            if file_path.extension() == Some(OsStr::new("scss")) {
                let content = sass_rs::compile_file(src_file.as_path(), sass_opts.clone())?;
                let mut dest_file = dest.join(file_path.clone());
                dest_file.set_extension("css");

                let mut file =
                    File::create(&dest_file)
                        .map_err(|e| format!("Could not create {:?}: {}", file_path, e))?;

                file.write_all(content.as_bytes())?;

            } else {
                let dest_file = dest.join(file_path);
                debug!("Copying {:?} to {:?}", src_file, dest_file);
                fs::copy(src_file.as_path(), dest_file.as_path())
                    .map_err(|e| format!("Could not copy {:?} into {:?}: {}", src_file, dest_file, e))?;
            }
        }
    }

    Ok(())
}

// creates a new RSS file with the contents of the site blog
fn create_rss(path: &str, dest: &Path, config: &Config, posts: &[Document]) -> Result<()> {
    match (&config.name, &config.description, &config.link) {
        // these three fields are mandatory in the RSS standard
        (&Some(ref name), &Some(ref description), &Some(ref link)) => {
            trace!("Generating RSS data");

            let items: Result<Vec<rss::Item>> = posts.iter().map(|doc| doc.to_rss(link)).collect();
            let items = items?;

            let channel = rss::ChannelBuilder::default()
                .title(name.to_owned())
                .link(link.to_owned())
                .description(description.to_owned())
                .items(items)
                .build()?;

            let rss_string = channel.to_string();
            trace!("RSS data: {}", rss_string);

            let rss_path = dest.join(path);

            let mut rss_file = File::create(&rss_path)?;
            rss_file
                .write_all(br#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
            rss_file.write_all(&rss_string.into_bytes())?;
            rss_file.write_all(b"\n")?;

            info!("Created RSS file at {}", rss_path.display());
            Ok(())
        }
        _ => Err(ErrorKind::ConfigFileMissingFields.into()),
    }
}
// creates a new jsonfeed file with the contents of the site blog
fn create_jsonfeed(path: &str, dest: &Path, config: &Config, posts: &[Document]) -> Result<()> {
    match (&config.name, &config.description, &config.link) {
        (&Some(ref name), &Some(ref desc), &Some(ref link)) => {
            trace!("Generating jsonfeed data");

            let jsonitems = posts.iter().map(|doc| doc.to_jsonfeed(link)).collect();

            let feed = Feed {
                title: name.to_string(),
                items: jsonitems,
                home_page_url: Some(link.to_string()),
                description: Some(desc.to_string()),
                ..Default::default()
            };

            let jsonfeed_string = jsonfeed::to_string(&feed).unwrap();
            let jsonfeed_path = dest.join(path);
            let mut jsonfeed_file = File::create(&jsonfeed_path)?;
            jsonfeed_file.write_all(&jsonfeed_string.into_bytes())?;

            info!("Created jsonfeed file at {}", jsonfeed_path.display());
            Ok(())
        }
        _ => Err(ErrorKind::ConfigFileMissingFields.into()),
    }
}

fn create_document_file<S: AsRef<str>, T: AsRef<Path>, R: Into<PathBuf>>(content: S,
                                                                         relpath: T,
                                                                         dest: R)
                                                                         -> Result<()> {
    create_document_file_internal(content.as_ref(), relpath.as_ref(), dest.into())
}

fn create_document_file_internal(content: &str, relpath: &Path, dest: PathBuf) -> Result<()> {
    // construct target path
    let mut file_path = dest;
    file_path.push(relpath);

    // create target directories if any exist
    if let Some(parent) = file_path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("Could not create {:?}: {}", parent, e))?;
    }

    let mut file = File::create(&file_path)
        .map_err(|e| format!("Could not create {:?}: {}", file_path, e))?;

    file.write_all(content.as_bytes())?;
    info!("Created {}", file_path.display());
    Ok(())
}
<|endoftext|>


<commit_before>//! # The implementation of librsvg
//!
//! The implementation of librsvg is in the `rsvg_internals` crate.  It is not a public
//! crate; instead, it exports the primitives necessary to implement librsvg's public APIs,
//! both the C and Rust APIs.  It has the XML and CSS parsing code, the SVG element
//! definitions and tree of elements, and all the drawing logic.
//!
//! Some interesting parts of rsvg_internals:
//!
//! * [The `Handle` struct](handle/struct.Handle.html) provides the primitives to implement
//! the public APIs, such as loading an SVG file and rendering it.
//!
//! * [The `DrawingCtx` struct](drawing_ctx/struct.DrawingCtx.html) is active while an SVG
//! handle is being drawn or queried for its geometry.  It has all the mutable state related
//! to the drawing process.
//!
//! * [The `Document` struct](document/struct.Document.html) represents a loaded SVG
//! document.  It holds the tree of [`Node`] elements, and a mapping of `id` attributes
//! to the corresponding element nodes.
//!
//! * [The `node` module](node/index.html) provides the [`Node`] struct and
//! helper traits used to operate on nodes.
//!
//! * [The `element` module](element/index.html) provides the [`Element`] struct and
//! the [`SetAttributes`] and [`Draw`] traits which are implemented by all SVG elements.
//!
//! * [The `xml` module](xml/index.html) receives events from the XML parser, and builds a
//! [`Document`] as a tree of [`Node`].
//!
//! * [The `properties` module](properties/index.html) contains structs that represent
//! collections of CSS properties.
//!
//! * [The `property_defs` module](property_defs/index.html) contains one type for each of
//! the CSS style properties that librsvg supports.
//!
//! * [The `css` module](css/index.html) contains the implementation of CSS parsing
//! and matching.
//!
//! [`Document`]: document/struct.Document.html
//! [`Node`]: node/type.Node.html
//! [`Element`]: element/struct.Element.html
//! [`SetAttributes`]: element/trait.SetAttributes.html
//! [`Draw`]: element/trait.Draw.html

#![allow(clippy::clone_on_ref_ptr)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]
#![allow(clippy::too_many_arguments)]
#![warn(unused)]

pub use crate::color::Color;

pub use crate::dpi::{rsvg_rust_set_default_dpi_x_y, Dpi};

pub use crate::error::{DefsLookupErrorKind, HrefError, LoadingError, RenderingError};

pub use crate::handle::{
    Handle, LoadOptions, RsvgDimensionData, RsvgPositionData, RsvgSizeFunc, SizeCallback,
};

pub use crate::length::{Length, LengthUnit, RsvgLength};

pub use crate::parsers::Parse;

pub use crate::rect::IRect;

pub use crate::structure::IntrinsicDimensions;

pub use crate::surface_utils::{
    iterators::Pixels,
    shared_surface::{SharedImageSurface, SurfaceType},
};

pub use crate::viewbox::ViewBox;

#[macro_use]
pub mod log;

#[macro_use]
mod parsers;

#[macro_use]
mod coord_units;

#[macro_use]
mod float_eq_cairo;

#[macro_use]
mod node;

#[macro_use]
mod property_macros;

mod allowed_url;
mod angle;
mod aspect_ratio;
mod bbox;
mod color;
mod cond;
mod css;
mod dasharray;
mod document;
mod dpi;
mod drawing_ctx;
mod element;
mod error;
mod filter;
pub mod filters;
mod font_props;
mod gradient;
mod handle;
mod image;
mod io;
mod iri;
mod length;
mod limits;
mod marker;
mod number_list;
mod paint_server;
mod path_builder;
mod path_parser;
mod pattern;
mod properties;
mod property_bag;
mod property_defs;
pub mod rect;
mod shapes;
mod space;
mod structure;
mod style;
pub mod surface_utils;
mod text;
mod transform;
mod unit_interval;
mod util;
mod viewbox;
mod xml;
mod xml2;
mod xml2_load;
<commit_msg>rsvg_internals: export the Rect type along with IRect<commit_after>//! # The implementation of librsvg
//!
//! The implementation of librsvg is in the `rsvg_internals` crate.  It is not a public
//! crate; instead, it exports the primitives necessary to implement librsvg's public APIs,
//! both the C and Rust APIs.  It has the XML and CSS parsing code, the SVG element
//! definitions and tree of elements, and all the drawing logic.
//!
//! Some interesting parts of rsvg_internals:
//!
//! * [The `Handle` struct](handle/struct.Handle.html) provides the primitives to implement
//! the public APIs, such as loading an SVG file and rendering it.
//!
//! * [The `DrawingCtx` struct](drawing_ctx/struct.DrawingCtx.html) is active while an SVG
//! handle is being drawn or queried for its geometry.  It has all the mutable state related
//! to the drawing process.
//!
//! * [The `Document` struct](document/struct.Document.html) represents a loaded SVG
//! document.  It holds the tree of [`Node`] elements, and a mapping of `id` attributes
//! to the corresponding element nodes.
//!
//! * [The `node` module](node/index.html) provides the [`Node`] struct and
//! helper traits used to operate on nodes.
//!
//! * [The `element` module](element/index.html) provides the [`Element`] struct and
//! the [`SetAttributes`] and [`Draw`] traits which are implemented by all SVG elements.
//!
//! * [The `xml` module](xml/index.html) receives events from the XML parser, and builds a
//! [`Document`] as a tree of [`Node`].
//!
//! * [The `properties` module](properties/index.html) contains structs that represent
//! collections of CSS properties.
//!
//! * [The `property_defs` module](property_defs/index.html) contains one type for each of
//! the CSS style properties that librsvg supports.
//!
//! * [The `css` module](css/index.html) contains the implementation of CSS parsing
//! and matching.
//!
//! [`Document`]: document/struct.Document.html
//! [`Node`]: node/type.Node.html
//! [`Element`]: element/struct.Element.html
//! [`SetAttributes`]: element/trait.SetAttributes.html
//! [`Draw`]: element/trait.Draw.html

#![allow(clippy::clone_on_ref_ptr)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]
#![allow(clippy::too_many_arguments)]
#![warn(unused)]

pub use crate::color::Color;

pub use crate::dpi::{rsvg_rust_set_default_dpi_x_y, Dpi};

pub use crate::error::{DefsLookupErrorKind, HrefError, LoadingError, RenderingError};

pub use crate::handle::{
    Handle, LoadOptions, RsvgDimensionData, RsvgPositionData, RsvgSizeFunc, SizeCallback,
};

pub use crate::length::{Length, LengthUnit, RsvgLength};

pub use crate::parsers::Parse;

pub use crate::rect::{IRect, Rect};

pub use crate::structure::IntrinsicDimensions;

pub use crate::surface_utils::{
    iterators::Pixels,
    shared_surface::{SharedImageSurface, SurfaceType},
};

pub use crate::viewbox::ViewBox;

#[macro_use]
pub mod log;

#[macro_use]
mod parsers;

#[macro_use]
mod coord_units;

#[macro_use]
mod float_eq_cairo;

#[macro_use]
mod node;

#[macro_use]
mod property_macros;

mod allowed_url;
mod angle;
mod aspect_ratio;
mod bbox;
mod color;
mod cond;
mod css;
mod dasharray;
mod document;
mod dpi;
mod drawing_ctx;
mod element;
mod error;
mod filter;
pub mod filters;
mod font_props;
mod gradient;
mod handle;
mod image;
mod io;
mod iri;
mod length;
mod limits;
mod marker;
mod number_list;
mod paint_server;
mod path_builder;
mod path_parser;
mod pattern;
mod properties;
mod property_bag;
mod property_defs;
pub mod rect;
mod shapes;
mod space;
mod structure;
mod style;
pub mod surface_utils;
mod text;
mod transform;
mod unit_interval;
mod util;
mod viewbox;
mod xml;
mod xml2;
mod xml2_load;
<|endoftext|>


<commit_before>use std::{fs, path::Path};

use chromiumoxide::{
    cdp::browser_protocol::page::CaptureScreenshotFormat, handler::viewport::Viewport, Browser,
    BrowserConfig,
};

use codec::{
    common::{async_trait::async_trait, base64, eyre::Result, futures::StreamExt, tokio},
    stencila_schema::{ImageObject, Node},
    utils::vec_string,
    Codec, CodecTrait, DecodeOptions, EncodeOptions,
};
use codec_html::HtmlCodec;

/// Encode and decode a document node to a PNG image.
///
/// This codec will "decode" strings or files to an `ImageObject` and uses a
/// headless browser to encode nodes as a screenshot of the HTML
/// encoding of the node.
pub struct PngCodec {}

#[async_trait]
impl CodecTrait for PngCodec {
    fn spec() -> Codec {
        Codec {
            status: "alpha".to_string(),
            formats: vec_string!["png"],
            root_types: vec_string!["*"],
            ..Default::default()
        }
    }

    /// Decode a document node from a string
    ///
    /// Simply returns an `ImageObject` with the string as its `content_url`
    /// (i.e assumes that the string is a DataURI with a Base64 encoded PNG)
    fn from_str(str: &str, _options: Option<DecodeOptions>) -> Result<Node> {
        Ok(Node::ImageObject(ImageObject {
            content_url: str.to_string(),
            ..Default::default()
        }))
    }

    /// Decode a document node from a file system path
    ///
    /// Simply returns an `ImageObject` with the path as its `content_url`
    async fn from_path(path: &Path, _options: Option<DecodeOptions>) -> Result<Node> {
        Ok(Node::ImageObject(ImageObject {
            content_url: path.display().to_string(),
            ..Default::default()
        }))
    }

    /// Encode a document node to a string
    ///
    /// Returns a Base64 encoded dataURI with media type `image/png`.
    async fn to_string_async(node: &Node, options: Option<EncodeOptions>) -> Result<String> {
        let bytes = nodes_to_bytes(&[node], options).await?;
        let string = ["data:image/png;base64,", &base64::encode(&bytes[0])].concat();
        Ok(string)
    }

    /// Encode a document node to a file system path
    ///
    /// This override is necessary to avoid the dataURI prefix and Base64 encoding that `to_string_async`
    /// does. It simply writes that bytes to a file at the path.
    async fn to_path(node: &Node, path: &Path, options: Option<EncodeOptions>) -> Result<()> {
        let bytes = nodes_to_bytes(&[node], options).await?;
        fs::write(path, &bytes[0])?;
        Ok(())
    }
}

/// Encode a set of document nodes to PNGs as bytes
///
/// This function is based around creating multiple PNGs, rather than a single one, to
/// reduce the per-image overhead of starting the browser, loading the theme etc.
pub async fn nodes_to_bytes(
    nodes: &[&Node],
    options: Option<EncodeOptions>,
) -> Result<Vec<Vec<u8>>> {
    // Return early if possible to avoid the following, including requiring Chrome.
    if nodes.is_empty() {
        return Ok(Vec::new());
    }

    // Generate HTML for each node
    let mut nodes_html = String::new();
    for (index, node) in nodes.iter().enumerate() {
        let node_html = HtmlCodec::to_string(
            node,
            Some(EncodeOptions {
                standalone: false,
                bundle: true,
                ..Default::default()
            }),
        )?;
        nodes_html.push_str(&format!(
            r#"
{}
"#,
            index, node_html
        ));
    }

    let theme = options
        .unwrap_or_default()
        .theme
        .unwrap_or_else(|| "rpng".to_string());

    // Wrap the generated HTML into a standalone page with bundled CSS & JS
    let html = codec_html::wrap_standalone(
        &nodes_html,
        EncodeOptions {
            bundle: true,
            theme: Some(theme),
            components: false,
            ..Default::default()
        },
        "PngCodec",
        r"
        body {{
            width: 640px; /* Avoid having images of block nodes that are too wide */
        }}
        div.node {{
            margin: 10px; /* Mainly to improve spacing when previewing HTML during development */
            min-height: 1px; /* To avoid 'Error -32000: Cannot take screenshot with 0 height' */
            min-width: 1px;
        }}",
    );

    // It can be useful to insect the generated HTML during development. Uncomment this to do so..
    // std::fs::write("rpng.html", &html)?;

    // Launch the browser
    let chrome = binaries::require_any(&[("chrome", "*"), ("chromium", "*")]).await?;
    let config = BrowserConfig::builder()
        .chrome_executable(chrome.path)
        .viewport(Viewport {
            // Increase the scale for higher resolution images. See https://github.com/puppeteer/puppeteer/issues/571#issuecomment-325404760
            device_scale_factor: Some(2.0),
            // Increase size of viewport to be able to encompass the largest images
            height: 10000,
            width: 10000,
            ..Default::default()
        })
        .build()
        .expect("Should build config");
    let (browser, mut handler) = Browser::launch(config).await?;
    let handler_task = tokio::task::spawn(async move {
        loop {
            let _ = handler.next().await.unwrap();
        }
    });

    // Create a page, set its HTML and wait for "navigation"
    let page = browser.new_page("about:blank").await?;
    page.set_content(html).await?.wait_for_navigation().await?;

    // Take a screenshot of each element
    // This uses `:first-child`, rather than screen-shotting the entire div, so that for
    // inline elements we do not get wide (page width) images. Assumes that the node is represented
    // by one element.
    let mut pngs = Vec::with_capacity(nodes.len());
    for index in 0..nodes.len() {
        let element = page
            .find_element(&format!("#node-{} :first-child", index))
            .await?;
        let bytes = element.screenshot(CaptureScreenshotFormat::Png).await?;
        pngs.push(bytes)
    }

    // Abort the handler task (if this is not done can get a `ResetWithoutClosingHandshake`
    // when this function ends)
    handler_task.abort();

    Ok(pngs)
}

#[cfg(test)]
mod tests {
    use codec::stencila_schema::CodeChunk;
    use test_utils::common::tempfile;

    use super::*;

    #[tokio::test]
    async fn encode() -> super::Result<()> {
        let node = Node::CodeChunk(CodeChunk {
            programming_language: "python".to_string(),
            text: "print(\"Hello world!\")".to_string(),
            outputs: Some(vec![Node::String("Hello world!".to_string())]),
            ..Default::default()
        });

        let dir = tempfile::tempdir()?;
        let path = dir.path().join("temp.png");
        PngCodec::to_path(&node, &path, None).await?;
        assert!(path.exists());

        let data = PngCodec::to_string_async(&node, None).await?;
        assert!(data.starts_with("data:image/png;base64,"));

        Ok(())
    }
}
<commit_msg>fix(PNG codec): Remove unecessary brace escapes<commit_after>use std::{fs, path::Path};

use chromiumoxide::{
    cdp::browser_protocol::page::CaptureScreenshotFormat, handler::viewport::Viewport, Browser,
    BrowserConfig,
};

use codec::{
    common::{async_trait::async_trait, base64, eyre::Result, futures::StreamExt, tokio},
    stencila_schema::{ImageObject, Node},
    utils::vec_string,
    Codec, CodecTrait, DecodeOptions, EncodeOptions,
};
use codec_html::HtmlCodec;

/// Encode and decode a document node to a PNG image.
///
/// This codec will "decode" strings or files to an `ImageObject` and uses a
/// headless browser to encode nodes as a screenshot of the HTML
/// encoding of the node.
pub struct PngCodec {}

#[async_trait]
impl CodecTrait for PngCodec {
    fn spec() -> Codec {
        Codec {
            status: "alpha".to_string(),
            formats: vec_string!["png"],
            root_types: vec_string!["*"],
            ..Default::default()
        }
    }

    /// Decode a document node from a string
    ///
    /// Simply returns an `ImageObject` with the string as its `content_url`
    /// (i.e assumes that the string is a DataURI with a Base64 encoded PNG)
    fn from_str(str: &str, _options: Option<DecodeOptions>) -> Result<Node> {
        Ok(Node::ImageObject(ImageObject {
            content_url: str.to_string(),
            ..Default::default()
        }))
    }

    /// Decode a document node from a file system path
    ///
    /// Simply returns an `ImageObject` with the path as its `content_url`
    async fn from_path(path: &Path, _options: Option<DecodeOptions>) -> Result<Node> {
        Ok(Node::ImageObject(ImageObject {
            content_url: path.display().to_string(),
            ..Default::default()
        }))
    }

    /// Encode a document node to a string
    ///
    /// Returns a Base64 encoded dataURI with media type `image/png`.
    async fn to_string_async(node: &Node, options: Option<EncodeOptions>) -> Result<String> {
        let bytes = nodes_to_bytes(&[node], options).await?;
        let string = ["data:image/png;base64,", &base64::encode(&bytes[0])].concat();
        Ok(string)
    }

    /// Encode a document node to a file system path
    ///
    /// This override is necessary to avoid the dataURI prefix and Base64 encoding that `to_string_async`
    /// does. It simply writes that bytes to a file at the path.
    async fn to_path(node: &Node, path: &Path, options: Option<EncodeOptions>) -> Result<()> {
        let bytes = nodes_to_bytes(&[node], options).await?;
        fs::write(path, &bytes[0])?;
        Ok(())
    }
}

/// Encode a set of document nodes to PNGs as bytes
///
/// This function is based around creating multiple PNGs, rather than a single one, to
/// reduce the per-image overhead of starting the browser, loading the theme etc.
pub async fn nodes_to_bytes(
    nodes: &[&Node],
    options: Option<EncodeOptions>,
) -> Result<Vec<Vec<u8>>> {
    // Return early if possible to avoid the following, including requiring Chrome.
    if nodes.is_empty() {
        return Ok(Vec::new());
    }

    // Generate HTML for each node
    let mut nodes_html = String::new();
    for (index, node) in nodes.iter().enumerate() {
        let node_html = HtmlCodec::to_string(
            node,
            Some(EncodeOptions {
                standalone: false,
                bundle: true,
                ..Default::default()
            }),
        )?;
        nodes_html.push_str(&format!(
            r#"
{}
"#,
            index, node_html
        ));
    }

    let theme = options
        .unwrap_or_default()
        .theme
        .unwrap_or_else(|| "rpng".to_string());

    // Wrap the generated HTML into a standalone page with bundled CSS & JS
    let html = codec_html::wrap_standalone(
        &nodes_html,
        EncodeOptions {
            bundle: true,
            theme: Some(theme),
            components: false,
            ..Default::default()
        },
        "PngCodec",
        r"
        body {
            width: 640px; /* Avoid having images of block nodes that are too wide */
        }
        div.node {
            margin: 10px; /* Mainly to improve spacing when previewing HTML during development */
            min-height: 1px; /* To avoid 'Error -32000: Cannot take screenshot with 0 height' */
            min-width: 1px;
        }",
    );

    // It can be useful to insect the generated HTML during development. Uncomment this to do so..
    // std::fs::write("rpng.html", &html)?;

    // Launch the browser
    let chrome = binaries::require_any(&[("chrome", "*"), ("chromium", "*")]).await?;
    let config = BrowserConfig::builder()
        .chrome_executable(chrome.path)
        .viewport(Viewport {
            // Increase the scale for higher resolution images. See https://github.com/puppeteer/puppeteer/issues/571#issuecomment-325404760
            device_scale_factor: Some(2.0),
            // Increase size of viewport to be able to encompass the largest images
            height: 10000,
            width: 10000,
            ..Default::default()
        })
        .build()
        .expect("Should build config");
    let (browser, mut handler) = Browser::launch(config).await?;
    let handler_task = tokio::task::spawn(async move {
        loop {
            let _ = handler.next().await.unwrap();
        }
    });

    // Create a page, set its HTML and wait for "navigation"
    let page = browser.new_page("about:blank").await?;
    page.set_content(html).await?.wait_for_navigation().await?;

    // Take a screenshot of each element
    // This uses `:first-child`, rather than screen-shotting the entire div, so that for
    // inline elements we do not get wide (page width) images. Assumes that the node is represented
    // by one element.
    let mut pngs = Vec::with_capacity(nodes.len());
    for index in 0..nodes.len() {
        let element = page
            .find_element(&format!("#node-{} :first-child", index))
            .await?;
        let bytes = element.screenshot(CaptureScreenshotFormat::Png).await?;
        pngs.push(bytes)
    }

    // Abort the handler task (if this is not done can get a `ResetWithoutClosingHandshake`
    // when this function ends)
    handler_task.abort();

    Ok(pngs)
}

#[cfg(test)]
mod tests {
    use codec::stencila_schema::CodeChunk;
    use test_utils::common::tempfile;

    use super::*;

    #[tokio::test]
    async fn encode() -> super::Result<()> {
        let node = Node::CodeChunk(CodeChunk {
            programming_language: "python".to_string(),
            text: "print(\"Hello world!\")".to_string(),
            outputs: Some(vec![Node::String("Hello world!".to_string())]),
            ..Default::default()
        });

        let dir = tempfile::tempdir()?;
        let path = dir.path().join("temp.png");
        PngCodec::to_path(&node, &path, None).await?;
        assert!(path.exists());

        let data = PngCodec::to_string_async(&node, None).await?;
        assert!(data.starts_with("data:image/png;base64,"));

        Ok(())
    }
}
<|endoftext|>


<commit_before>// Copyright (C) 2020 Julen Ruiz Aizpuru, https://github.com/julen

extern crate plurr;

use plurr::{Plurr, PlurrError};

#[test]
fn format_error_unmatched_braces() {
    let mut p = Plurr::new();

    let result_opening = p.format("err {");
    assert!(result_opening.is_err());
    assert_eq!(result_opening, Err(PlurrError::UnmatchedOpeningBrace));

    let result_closing = p.format("err }");
    assert!(result_closing.is_err());
    assert_eq!(result_closing, Err(PlurrError::UnmatchedClosingBrace));
}

#[test]
fn format_error_param_undefined() {
    let mut p = Plurr::new();

    let result = p.format("{foo}");
    assert!(result.is_err());
    assert_eq!(result, Err(PlurrError::UndefinedParameter));
}

#[test]
fn format_error_empty_placeholder() {
    let mut p = Plurr::new();

    let s = "Delete {:{N} file|{N} files}?";
    assert_eq!(
        p.param("N", "2").format(s),
        Err(PlurrError::EmptyPlaceholder)
    )
}

#[test]
fn format_n_plural() {
    let mut p = Plurr::new();
    let s = "Do you want to delete {N_PLURAL:this {N} file|these {N} files} permanently?";

    assert_eq!(
        p.param("N", "1").format(s),
        Ok("Do you want to delete this 1 file permanently?".to_string())
    );

    assert_eq!(
        p.param("N", "5").format(s),
        Ok("Do you want to delete these 5 files permanently?".to_string())
    );
}

#[test]
fn format_no_auto_plurals() {
    let mut p = Plurr::new();
    p.auto_plurals(false);
    let s = "Do you want to delete {N_PLURAL:this {N} file|these {N} files} permanently?";

    assert_eq!(
        p.param("N", "1").format(s),
        Err(PlurrError::UndefinedParameter)
    );
    assert_eq!(
        p.param("N_PLURAL", "0").param("N", "1").format(s),
        Ok("Do you want to delete this 1 file permanently?".to_string())
    );
    assert_eq!(
        p.param("N_PLURAL", "1").param("N", "5").format(s),
        Ok("Do you want to delete these 5 files permanently?".to_string())
    );
}

#[test]
fn format_not_zero_or_positive_args() {
    let mut p = Plurr::new();
    let s = "{N_PLURAL:One file|{N} files}";

    assert_eq!(
        p.param("N", "-1").format(s),
        Err(PlurrError::NotZeroOrPositiveValue)
    );
    assert_eq!(
        p.param("N", "Hello").format(s),
        Err(PlurrError::NotZeroOrPositiveValue)
    );
}

#[test]
fn format_arbitrary_arg() {
    let mut p = Plurr::new();

    let s = "Do you want to drink {CHOICE:coffee|tea|juice}?";

    assert_eq!(
        p.param("CHOICE", "0").format(s),
        Ok("Do you want to drink coffee?".to_string())
    );
    assert_eq!(
        p.param("CHOICE", "1").format(s),
        Ok("Do you want to drink tea?".to_string())
    );
    assert_eq!(
        p.param("CHOICE", "2").format(s),
        Ok("Do you want to drink juice?".to_string())
    );
    // when out of bounds, last form is used
    assert_eq!(
        p.param("CHOICE", "5").format(s),
        Ok("Do you want to drink juice?".to_string())
    );
}

#[test]
fn format_locale_arg() {
    let mut p = Plurr::new();

    let s = "Удалить {N_PLURAL:этот {N} файл|эти {N} файла|эти {N} файлов} навсегда?";
    assert_eq!(
        p.locale("ru").param("N", "1").format(s),
        Ok("Удалить этот 1 файл навсегда?".to_string())
    );
    assert_eq!(
        p.locale("ru").param("N", "2").format(s),
        Ok("Удалить эти 2 файла навсегда?".to_string())
    );
    assert_eq!(
        p.locale("ru").param("N", "5").format(s),
        Ok("Удалить эти 5 файлов навсегда?".to_string())
    );
}

#[test]
fn format_nested() {
    let mut p = Plurr::new();

    let s = "{X_PLURAL:{X:|One|{X}} file|{X:No|{X}} files} found.";

    assert_eq!(
        p.param("X", "0").format(s),
        Ok("No files found.".to_string())
    );
    assert_eq!(
        p.param("X", "1").format(s),
        Ok("One file found.".to_string())
    );
    assert_eq!(
        p.param("X", "2").format(s),
        Ok("2 files found.".to_string())
    );
}

#[test]
fn format_nested_locale() {
    let mut p = Plurr::new();
    let s =
        "{X_PLURAL:Найден {X:|один|{X}} файл|Найдены {X} файла|{X:Не найдено|Найдено {X}} файлов}.";

    assert_eq!(
        p.locale("ru").param("X", "0").format(s),
        Ok("Не найдено файлов.".to_string())
    );
    assert_eq!(
        p.locale("ru").param("X", "1").format(s),
        Ok("Найден один файл.".to_string())
    );
    assert_eq!(
        p.locale("ru").param("X", "2").format(s),
        Ok("Найдены 2 файла.".to_string())
    );
    assert_eq!(
        p.locale("ru").param("X", "5").format(s),
        Ok("Найдено 5 файлов.".to_string())
    );
}

#[test]
fn format_args() {
    let mut p = Plurr::new();
    let s = "{FOO}";

    assert_eq!(p.param("FOO", "1").format(s), Ok("1".to_string()));
    assert_eq!(p.param("FOO", "5.5").format(s), Ok("5.5".to_string()));
    assert_eq!(p.param("FOO", "bar").format(s), Ok("bar".to_string()));
}
<commit_msg>Cleanup: remove superfluous assertions<commit_after>// Copyright (C) 2020 Julen Ruiz Aizpuru, https://github.com/julen

extern crate plurr;

use plurr::{Plurr, PlurrError};

#[test]
fn format_error_unmatched_braces() {
    let mut p = Plurr::new();

    let result_opening = p.format("err {");
    assert_eq!(result_opening, Err(PlurrError::UnmatchedOpeningBrace));

    let result_closing = p.format("err }");
    assert_eq!(result_closing, Err(PlurrError::UnmatchedClosingBrace));
}

#[test]
fn format_error_param_undefined() {
    let mut p = Plurr::new();

    let result = p.format("{foo}");
    assert_eq!(result, Err(PlurrError::UndefinedParameter));
}

#[test]
fn format_error_empty_placeholder() {
    let mut p = Plurr::new();

    let s = "Delete {:{N} file|{N} files}?";
    assert_eq!(
        p.param("N", "2").format(s),
        Err(PlurrError::EmptyPlaceholder)
    )
}

#[test]
fn format_n_plural() {
    let mut p = Plurr::new();
    let s = "Do you want to delete {N_PLURAL:this {N} file|these {N} files} permanently?";

    assert_eq!(
        p.param("N", "1").format(s),
        Ok("Do you want to delete this 1 file permanently?".to_string())
    );

    assert_eq!(
        p.param("N", "5").format(s),
        Ok("Do you want to delete these 5 files permanently?".to_string())
    );
}

#[test]
fn format_no_auto_plurals() {
    let mut p = Plurr::new();
    p.auto_plurals(false);
    let s = "Do you want to delete {N_PLURAL:this {N} file|these {N} files} permanently?";

    assert_eq!(
        p.param("N", "1").format(s),
        Err(PlurrError::UndefinedParameter)
    );
    assert_eq!(
        p.param("N_PLURAL", "0").param("N", "1").format(s),
        Ok("Do you want to delete this 1 file permanently?".to_string())
    );
    assert_eq!(
        p.param("N_PLURAL", "1").param("N", "5").format(s),
        Ok("Do you want to delete these 5 files permanently?".to_string())
    );
}

#[test]
fn format_not_zero_or_positive_args() {
    let mut p = Plurr::new();
    let s = "{N_PLURAL:One file|{N} files}";

    assert_eq!(
        p.param("N", "-1").format(s),
        Err(PlurrError::NotZeroOrPositiveValue)
    );
    assert_eq!(
        p.param("N", "Hello").format(s),
        Err(PlurrError::NotZeroOrPositiveValue)
    );
}

#[test]
fn format_arbitrary_arg() {
    let mut p = Plurr::new();

    let s = "Do you want to drink {CHOICE:coffee|tea|juice}?";

    assert_eq!(
        p.param("CHOICE", "0").format(s),
        Ok("Do you want to drink coffee?".to_string())
    );
    assert_eq!(
        p.param("CHOICE", "1").format(s),
        Ok("Do you want to drink tea?".to_string())
    );
    assert_eq!(
        p.param("CHOICE", "2").format(s),
        Ok("Do you want to drink juice?".to_string())
    );
    // when out of bounds, last form is used
    assert_eq!(
        p.param("CHOICE", "5").format(s),
        Ok("Do you want to drink juice?".to_string())
    );
}

#[test]
fn format_locale_arg() {
    let mut p = Plurr::new();

    let s = "Удалить {N_PLURAL:этот {N} файл|эти {N} файла|эти {N} файлов} навсегда?";
    assert_eq!(
        p.locale("ru").param("N", "1").format(s),
        Ok("Удалить этот 1 файл навсегда?".to_string())
    );
    assert_eq!(
        p.locale("ru").param("N", "2").format(s),
        Ok("Удалить эти 2 файла навсегда?".to_string())
    );
    assert_eq!(
        p.locale("ru").param("N", "5").format(s),
        Ok("Удалить эти 5 файлов навсегда?".to_string())
    );
}

#[test]
fn format_nested() {
    let mut p = Plurr::new();

    let s = "{X_PLURAL:{X:|One|{X}} file|{X:No|{X}} files} found.";

    assert_eq!(
        p.param("X", "0").format(s),
        Ok("No files found.".to_string())
    );
    assert_eq!(
        p.param("X", "1").format(s),
        Ok("One file found.".to_string())
    );
    assert_eq!(
        p.param("X", "2").format(s),
        Ok("2 files found.".to_string())
    );
}

#[test]
fn format_nested_locale() {
    let mut p = Plurr::new();
    let s =
        "{X_PLURAL:Найден {X:|один|{X}} файл|Найдены {X} файла|{X:Не найдено|Найдено {X}} файлов}.";

    assert_eq!(
        p.locale("ru").param("X", "0").format(s),
        Ok("Не найдено файлов.".to_string())
    );
    assert_eq!(
        p.locale("ru").param("X", "1").format(s),
        Ok("Найден один файл.".to_string())
    );
    assert_eq!(
        p.locale("ru").param("X", "2").format(s),
        Ok("Найдены 2 файла.".to_string())
    );
    assert_eq!(
        p.locale("ru").param("X", "5").format(s),
        Ok("Найдено 5 файлов.".to_string())
    );
}

#[test]
fn format_args() {
    let mut p = Plurr::new();
    let s = "{FOO}";

    assert_eq!(p.param("FOO", "1").format(s), Ok("1".to_string()));
    assert_eq!(p.param("FOO", "5.5").format(s), Ok("5.5".to_string()));
    assert_eq!(p.param("FOO", "bar").format(s), Ok("bar".to_string()));
}
<|endoftext|>


<commit_before>extern crate num;

// The `AdditiveIterator` trait adds the `sum` method to iterators
use std::iter::AdditiveIterator;
use std::iter;

fn main() {
    println!("Find the sum of all the squared odd numbers under 1000");
    let upper = 1000u;

    // Imperative approach
    // Declare accumulator variable
    let mut acc = 0;
    // Iterate: 0, 1, 2, ... to infinity
    for n in iter::count(0u, 1) {
        // Square the number
        let n_squared = n * n;

        if n_squared >= upper {
            // Break loop if exceeded the upper limit
            break;
        } else if is_odd(n_squared) {
            // Accumulate value, if it's odd
            acc += n_squared;
        }
    }
    println!("imperative style: {}", acc);

    // Functional approach
    let sum_of_squared_odd_numbers =
        // All natural numbers
        iter::count(0u, 1).
        // Squared
        map(|n| n * n).
        // Below upper limit
        take_while(|&n| n < upper).
        // That are odd
        filter(|n| is_odd(*n)).
        // Sum them
        sum();
    println!("functional style: {}", sum_of_squared_odd_numbers);
}

fn is_odd(n: uint) -> bool {
    n % 2 == 1
}
<commit_msg>hof: remove extern<commit_after>// The `AdditiveIterator` trait adds the `sum` method to iterators
use std::iter::AdditiveIterator;
use std::iter;

fn main() {
    println!("Find the sum of all the squared odd numbers under 1000");
    let upper = 1000u;

    // Imperative approach
    // Declare accumulator variable
    let mut acc = 0;
    // Iterate: 0, 1, 2, ... to infinity
    for n in iter::count(0u, 1) {
        // Square the number
        let n_squared = n * n;

        if n_squared >= upper {
            // Break loop if exceeded the upper limit
            break;
        } else if is_odd(n_squared) {
            // Accumulate value, if it's odd
            acc += n_squared;
        }
    }
    println!("imperative style: {}", acc);

    // Functional approach
    let sum_of_squared_odd_numbers =
        // All natural numbers
        iter::count(0u, 1).
        // Squared
        map(|n| n * n).
        // Below upper limit
        take_while(|&n| n < upper).
        // That are odd
        filter(|n| is_odd(*n)).
        // Sum them
        sum();
    println!("functional style: {}", sum_of_squared_odd_numbers);
}

fn is_odd(n: uint) -> bool {
    n % 2 == 1
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate rustful;

use std::sync::RwLock;
use std::borrow::ToOwned;
use std::error::Error;

use rustful::{Server, TreeRouter, Context, Response, Log, Handler};
use rustful::plugin::{PluginContext, ResponseAction, ContextPlugin, ResponsePlugin};
use rustful::plugin::ContextAction::{self, Continue};
use rustful::response::ResponseData;
use rustful::Method::Get;
use rustful::StatusCode;
use rustful::header::Headers;

fn say_hello(context: Context, response: Response) {
    let person = match context.variables.get(&"person".to_owned()) {
        Some(name) => &name[..],
        None => "stranger"
    };

    if let Err(e) = response.into_writer().send(format!("{{\"message\": \"Hello, {}!\"}}", person))  {
        //There is not much we can do now
        context.log.note(&format!("could not send hello: {}", e.description()));
    }
}

//Dodge an ICE, related to functions as handlers.
struct HandlerFn(fn(Context, Response));

impl Handler for HandlerFn {
    fn handle_request(&self, context: Context, response: Response) {
        self.0(context, response);
    }
}

fn main() {
    println!("Visit http://localhost:8080 or http://localhost:8080/Peter (if your name is Peter) to try this example.");

    let mut router = TreeRouter::new();
    insert_routes!{
        &mut router => {
            "print" => {
                "/" => Get: HandlerFn(say_hello),
                ":person" => Get: HandlerFn(say_hello)
            }
        }
    };

    let server_result = Server::new()
           .handlers(router)
           .port(8080)

            //Log path, change path, log again
           .with_context_plugin(RequestLogger::new())
           .with_context_plugin(PathPrefix::new("print"))
           .with_context_plugin(RequestLogger::new())

           .with_response_plugin(Jsonp::new("setMessage"))

           .run();

    match server_result {
        Ok(_server) => {},
        Err(e) => println!("could not start server: {}", e.description())
    }
}

struct RequestLogger {
    counter: RwLock<u32>
}

impl RequestLogger {
    pub fn new() -> RequestLogger {
        RequestLogger {
            counter: RwLock::new(0)
        }
    }
}

impl ContextPlugin for RequestLogger {
    ///Count requests and log the path.
    fn modify(&self, ctx: PluginContext, context: &mut Context) -> ContextAction {
        *self.counter.write().unwrap() += 1;
        ctx.log.note(&format!("Request #{} is to '{}'", *self.counter.read().unwrap(), context.path));
        Continue
    }
}

struct PathPrefix {
    prefix: &'static str
}

impl PathPrefix {
    pub fn new(prefix: &'static str) -> PathPrefix {
        PathPrefix {
            prefix: prefix
        }
    }
}

impl ContextPlugin for PathPrefix {
    ///Append the prefix to the path
    fn modify(&self, _ctx: PluginContext, context: &mut Context) -> ContextAction {
        context.path = format!("/{}{}", self.prefix.trim_matches('/'), context.path);
        Continue
    }
}

struct Jsonp {
    function: &'static str
}

impl Jsonp {
    pub fn new(function: &'static str) -> Jsonp {
        Jsonp {
            function: function
        }
    }
}

impl ResponsePlugin for Jsonp {
    fn begin(&self, _ctx: PluginContext, status: StatusCode, headers: Headers) -> (StatusCode, Headers, ResponseAction) {
        let action = ResponseAction::write(Some(format!("{}(", self.function)));
        (status, headers, action)
    }

    fn write<'a>(&'a self, _ctx: PluginContext, bytes: Option<ResponseData<'a>>) -> ResponseAction {
        ResponseAction::write(bytes)
    }

    fn end(&self, _ctx: PluginContext) -> ResponseAction {
        ResponseAction::write(Some(");"))
    }
}<commit_msg>Update plugins example to use plugin storage<commit_after>#[macro_use]
extern crate rustful;

use std::sync::RwLock;
use std::error::Error;

use rustful::{Server, TreeRouter, Context, Response, Log, Handler};
use rustful::plugin::{PluginContext, ResponseAction, ContextPlugin, ResponsePlugin};
use rustful::plugin::ContextAction::{self, Continue};
use rustful::response::ResponseData;
use rustful::Method::Get;
use rustful::StatusCode;
use rustful::header::Headers;

fn say_hello(mut context: Context, mut response: Response) {
    //Take the name of the JSONP function from the query variables
    if let Some(jsonp_name) = context.query.remove("jsonp") {
        response.plugin_storage().insert(JsonpFn(jsonp_name));
    }

    let person = match context.variables.get("person") {
        Some(name) => &name[..],
        None => "stranger"
    };

    if let Err(e) = response.into_writer().send(format!("{{\"message\": \"Hello, {}!\"}}", person))  {
        //There is not much we can do now
        context.log.note(&format!("could not send hello: {}", e.description()));
    }
}

//Dodge an ICE, related to functions as handlers.
struct HandlerFn(fn(Context, Response));

impl Handler for HandlerFn {
    fn handle_request(&self, context: Context, response: Response) {
        self.0(context, response);
    }
}

fn main() {
    println!("Visit http://localhost:8080 or http://localhost:8080/Peter (if your name is Peter) to try this example.");
    println!("Append ?jsonp=someFunction to get a JSONP response.");

    let mut router = TreeRouter::new();
    insert_routes!{
        &mut router => {
            "print" => {
                "/" => Get: HandlerFn(say_hello),
                ":person" => Get: HandlerFn(say_hello)
            }
        }
    };

    let server_result = Server::new()
           .handlers(router)
           .port(8080)

            //Log path, change path, log again
           .with_context_plugin(RequestLogger::new())
           .with_context_plugin(PathPrefix::new("print"))
           .with_context_plugin(RequestLogger::new())

           .with_response_plugin(Jsonp)

           .run();

    match server_result {
        Ok(_server) => {},
        Err(e) => println!("could not start server: {}", e.description())
    }
}

struct RequestLogger {
    counter: RwLock<u32>
}

impl RequestLogger {
    pub fn new() -> RequestLogger {
        RequestLogger {
            counter: RwLock::new(0)
        }
    }
}

impl ContextPlugin for RequestLogger {
    ///Count requests and log the path.
    fn modify(&self, ctx: PluginContext, context: &mut Context) -> ContextAction {
        *self.counter.write().unwrap() += 1;
        ctx.log.note(&format!("Request #{} is to '{}'", *self.counter.read().unwrap(), context.path));
        Continue
    }
}

struct PathPrefix {
    prefix: &'static str
}

impl PathPrefix {
    pub fn new(prefix: &'static str) -> PathPrefix {
        PathPrefix {
            prefix: prefix
        }
    }
}

impl ContextPlugin for PathPrefix {
    ///Append the prefix to the path
    fn modify(&self, _ctx: PluginContext, context: &mut Context) -> ContextAction {
        context.path = format!("/{}{}", self.prefix.trim_matches('/'), context.path);
        Continue
    }
}

struct JsonpFn(String);

struct Jsonp;

impl ResponsePlugin for Jsonp {
    fn begin(&self, ctx: PluginContext, status: StatusCode, headers: Headers) -> (StatusCode, Headers, ResponseAction) {
        //Check if a JSONP function is defined and write the beginning of the call
        let output = if let Some(&JsonpFn(ref function)) = ctx.storage.get() {
            Some(format!("{}(", function))
        } else {
            None
        };

        (status, headers, ResponseAction::write(output))
    }

    fn write<'a>(&'a self, _ctx: PluginContext, bytes: Option<ResponseData<'a>>) -> ResponseAction {
        ResponseAction::write(bytes)
    }

    fn end(&self, ctx: PluginContext) -> ResponseAction {
        //Check if a JSONP function is defined and write the end of the call
        let output = ctx.storage.get::<JsonpFn>().map(|_| ");");
        ResponseAction::write(output)
    }
}<|endoftext|>


<commit_before>use utils;

pub enum Direction {
    Up,
    Down,
    Left,
    Right,
}

#[deriving(Clone)]
pub enum CursorPos {
    Place(uint, uint),
}

impl CursorPos {
    pub fn expand(&self) -> (uint, uint) {
        match self {
            &CursorPos::Place(x, y) => return (x, y)
        }
    }

    pub fn get_offset(&self) -> uint {
        match self {
            &CursorPos::Place(x, _) => return x
        }
    }

    pub fn get_linenum(&self) -> uint {
        match self {
            &CursorPos::Place(_, y) => return y
        }
    }
}

#[deriving(Clone)]
pub struct Cursor {
    pub buffer_pos: CursorPos,
}

impl Cursor {
    /// Create a new cursor instance
    pub fn new() -> Cursor {
        Cursor {
            buffer_pos: CursorPos::Place(0, 0)
        }
    }

    /// Draw the cursor based on the `x` and `y` values
    pub fn draw(&self) {
        match self.buffer_pos {
            CursorPos::Place(x, y) => utils::draw_cursor(x, y)
        }
    }

    pub fn set_position(&mut self, x: uint, y: uint) {
        self.buffer_pos = CursorPos::Place(x, y);
    }

    pub fn get_position(&self) -> (uint, uint) {
        self.buffer_pos.expand()
    }

    pub fn get_offset(&self) -> uint {
        self.buffer_pos.get_offset()
    }

    pub fn get_linenum(&self) -> uint {
        self.buffer_pos.get_linenum()
    }
}
<commit_msg>Cleaned up visibility of pieces in cursor module<commit_after>use utils;

pub enum Direction {
    Up,
    Down,
    Left,
    Right,
}

enum CursorPos {
    Place(uint, uint),
}

impl CursorPos {
    fn expand(&self) -> (uint, uint) {
        match self {
            &CursorPos::Place(x, y) => return (x, y)
        }
    }

    fn get_offset(&self) -> uint {
        match self {
            &CursorPos::Place(x, _) => return x
        }
    }

    fn get_linenum(&self) -> uint {
        match self {
            &CursorPos::Place(_, y) => return y
        }
    }
}

pub struct Cursor {
    buffer_pos: CursorPos,
}

impl Cursor {
    /// Create a new cursor instance
    pub fn new() -> Cursor {
        Cursor {
            buffer_pos: CursorPos::Place(0, 0)
        }
    }

    /// Draw the cursor
    pub fn draw(&self) {
        let (offset, line_num) = self.get_position();
        utils::draw_cursor(offset, line_num)
    }

    pub fn set_position(&mut self, x: uint, y: uint) {
        self.buffer_pos = CursorPos::Place(x, y);
    }

    pub fn get_position(&self) -> (uint, uint) {
        self.buffer_pos.expand()
    }

    pub fn get_offset(&self) -> uint {
        self.buffer_pos.get_offset()
    }

    pub fn get_linenum(&self) -> uint {
        self.buffer_pos.get_linenum()
    }
}
<|endoftext|>


<commit_before><commit_msg>Reexport Socket from postgres<commit_after><|endoftext|>


<commit_before>/*
    Copyright 2017-2019 Andrew Medworth <github@medworth.org.uk>

    This file is part of Dots-and-Boxes Engine.

    Dots-and-Boxes Engine is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Dots-and-Boxes Engine is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Dots-and-Boxes Engine.  If not, see <http://www.gnu.org/licenses/>.
*/

use game::{Move, Position, SimplePosition, Side};
use nimstring;
use eval;

use std::io::{self, BufRead};
use std::fs::File;
use regex::{Regex, Captures};
use time;

#[derive(PartialEq)]
#[derive(Debug)]
enum Command {
    MakeMove(Move),
    UndoMove(Move),
    CalcNimstringValue,
    Evaluate,
    PrintHelp,
    Quit,
}

impl Command {
    fn execute(self: &Command, pos: &mut SimplePosition) {
        match self {
            &Command::MakeMove(m) => {
                if pos.is_legal_move(m) {
                    pos.make_move(m);
                }
                else {
                    println!("Not a legal move: {}", m);
                }
            },
            &Command::UndoMove(m) => { pos.undo_move(m); },
            &Command::CalcNimstringValue => {
                let (val, per_move) = nimstring::calc_value_with_moves(pos);
                println!("Position value is {}", val);
                let mut moves: Vec<&Move> = per_move.keys().collect();
                moves.sort_by(|a, b| a.y.cmp(&b.y).then(a.x.cmp(&b.x)).then(a.side.cmp(&b.side)));
                for &m in &moves {
                    println!("{} {}", m, per_move.get(m).unwrap());
                }
            },
            &Command::Evaluate => {
                let (val, best_move) = eval::eval(pos);
                if let Some(best_move) = best_move {
                    println!("V(P) = {}, best move {}", val, best_move);
                } else {
                    println!("V(P) = {}", val);
                }
            },
            &Command::PrintHelp => { print_help(); },
            &Command::Quit => { println!("Bye bye!"); },
        }
    }
}

fn print_help() {
    println!("Available commands:");
    println!("x y t/l/b/r - make move (x,y) top/left/bottom/right");
    println!("u x y t/l/b/r - undo move (x,y) top/left/bottom/right");
    println!("nv - calculate Nimstring value of current position");
    println!("eval - evaluate the current position");
    println!("help - print this help message");
    println!("quit/exit - exit program");
}

fn parse_side(side_s: &str) -> Option<Side> {
    if side_s == "l" || side_s == "left" { Some(Side::Left) }
    else if side_s == "r" || side_s == "right" { Some(Side::Right) }
    else if side_s == "t" || side_s == "top" { Some(Side::Top) }
    else if side_s == "b" || side_s == "bottom" { Some(Side::Bottom) }
    else { None }
}

fn parse_move(caps: Captures) -> Result<Move, String> {
    let x = caps[1].parse::<usize>().unwrap();
    let y = caps[2].parse::<usize>().unwrap();
    let side_s = caps[3].to_string();
    match parse_side(&side_s) {
        Some(side) => Ok(Move{x: x, y: y, side: side}),
        None => Err(format!("Unrecognised side: [{}]", side_s))
    }
}

fn parse_command(input: &str) -> Result<Command, String> {
    let input = input.to_lowercase();
    let move_re = Regex::new(r"^(\d+) (\d+) ([a-zA-Z]+)$").unwrap();
    if let Some(caps) = move_re.captures(&input) {
        match parse_move(caps) {
            Ok(m) => return Ok(Command::MakeMove(m)),
            Err(e) => return Err(format!("Cannot extract move from [{}]: {}", input, e))
        }
    }
    let undo_move_re = Regex::new(r"^[uU] (\d+) (\d+) ([a-zA-Z]+)$").unwrap();
    if let Some(caps) = undo_move_re.captures(&input) {
        match parse_move(caps) {
            Ok(m) => return Ok(Command::UndoMove(m)),
            Err(e) => return Err(format!("Cannot extract move from [{}]: {}", input, e))
        }
    }
    if "nv" == input {
        return Ok(Command::CalcNimstringValue);
    }
    if "eval" == input {
        return Ok(Command::Evaluate);
    }
    if "help" == input {
        return Ok(Command::PrintHelp);
    }
    if "exit" == input || "quit" == input {
        return Ok(Command::Quit);
    }
    Err("Unsupported command".to_string())
}

fn get_next_command() -> Command {
    loop {
        let mut input = String::new();
        if let Err(error) = io::stdin().read_line(&mut input) {
            println!("Error reading from standard input: {}", error);
            continue;
        }
        let input = input.trim();
        match parse_command(&input) {
            Ok(command) => return command,
            Err(error) => {
                println!("Cannot execute [{}]: {}", input, error);
                println!("For help, try 'help'");
            }
        }
    }
}

fn main_loop_from(pos: &mut SimplePosition) {
    loop {
        println!("{}", pos);
        let command = get_next_command();
        let start_time = time::precise_time_s();
        command.execute(pos);
        let end_time = time::precise_time_s();
        if command == Command::Quit {
            break;
        }
        let elapsed = end_time - start_time;
        if elapsed >= 0.1 {
            println!("({:.1} seconds)", end_time - start_time);
        }
        println!();
    }
}

fn parse_position<R: BufRead>(reader: R) -> SimplePosition {
    let mut lines = reader.lines();
    let size_spec = lines.next().expect("Empty file").expect("Could not read board size from first line");
    let size_re = Regex::new(r"^(\d+) (\d+)$").unwrap();
    let size_caps = size_re.captures(&size_spec).expect(&format!("Could not read board size from [{}]", size_spec));
    let width = size_caps[1].parse::<usize>().unwrap();
    let height = size_caps[2].parse::<usize>().unwrap();
    let mut pos = SimplePosition::new_game(width, height);
    for line in lines {
        let line = line.expect("Could not read line from file");
        if line.trim().len() == 0 {
            continue;
        }
        match parse_command(&line) {
            Ok(command) => command.execute(&mut pos),
            Err(error) => panic!("Cannot execute [{}]: {}", line, error),
        }
    }
    pos
}

// Enter the main loop of the CLI from the start of the game
pub fn main_loop_start(width: usize, height: usize) {
    main_loop_from(&mut SimplePosition::new_game(width, height));
}

// Execute a given file of commands (which must have the dimensions of the position on the first line)
// and then enter the CLI main loop.
pub fn main_loop_file(filename: &str) {
    let f = File::open(filename).expect(&format!("Could not open file [{}]", filename));
    let reader = io::BufReader::new(f);
    let mut pos = parse_position(reader);
    main_loop_from(&mut pos);
}

#[cfg(test)]
mod tests {
    use game::*;
    use cli::*;

    #[test]
    fn parse_make_move_cmd() {
        assert_eq!(Command::MakeMove(Move{x: 3, y: 5, side: Side::Bottom}), parse_command("3 5 b").unwrap());
        assert_eq!(Command::MakeMove(Move{x: 3, y: 5, side: Side::Bottom}), parse_command("3 5 Bottom").unwrap());
    }

    #[test]
    fn parse_undo_move_cmd() {
        assert_eq!(Command::UndoMove(Move{x: 8, y: 6, side: Side::Left}), parse_command("u 8 6 l").unwrap());
        assert_eq!(Command::UndoMove(Move{x: 8, y: 6, side: Side::Left}), parse_command("u 8 6 Left").unwrap());
    }

    #[test]
    fn parse_nimstring_value_cmd() {
        assert_eq!(Command::CalcNimstringValue, parse_command("nv").unwrap());
    }

    #[test]
    fn parse_evaluate_cmd() {
        assert_eq!(Command::Evaluate, parse_command("eval").unwrap());
    }

    #[test]
    fn parse_help_cmd() {
        assert_eq!(Command::PrintHelp, parse_command("help").unwrap());
    }

    #[test]
    fn parse_exit_cmd() {
        assert_eq!(Command::Quit, parse_command("quit").unwrap());
        assert_eq!(Command::Quit, parse_command("exit").unwrap());
    }
}
<commit_msg>Extract trait CLIPosition (#47)<commit_after>/*
    Copyright 2017-2019 Andrew Medworth <github@medworth.org.uk>

    This file is part of Dots-and-Boxes Engine.

    Dots-and-Boxes Engine is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Dots-and-Boxes Engine is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Dots-and-Boxes Engine.  If not, see <http://www.gnu.org/licenses/>.
*/

use game::{Move, SimplePosition, Side};
use nimstring;
use eval::{self, EvaluablePosition};

use std::fmt::Display;
use std::hash::Hash;
use std::io::{self, BufRead};
use std::fs::File;
use regex::Regex;
use time;

#[derive(PartialEq)]
#[derive(Debug)]
enum Command<M> {
    MakeMove(M),
    UndoMove(M),
    CalcNimstringValue,
    Evaluate,
    PrintHelp,
    Quit,
}

impl <M: Copy + Display + Eq + Hash> Command<M> {
    fn execute<P>(self: &Command<M>, pos: &mut P)
    where P: CLIPosition<M> {
        match self {
            &Command::MakeMove(m) => {
                if pos.is_legal_move(m) {
                    pos.make_move(m);
                }
                else {
                    println!("Not a legal move: {}", m);
                }
            },
            &Command::UndoMove(m) => { pos.undo_move(m); },
            &Command::CalcNimstringValue => {
                let (val, per_move) = nimstring::calc_value_with_moves(pos);
                println!("Position value is {}", val);
                let mut moves: Vec<&M> = per_move.keys().collect();
                pos.sort_moves(&mut moves);
                for &m in &moves {
                    println!("{} {}", m, per_move.get(m).unwrap());
                }
            },
            &Command::Evaluate => {
                let (val, best_move) = eval::eval(pos);
                if let Some(best_move) = best_move {
                    println!("V(P) = {}, best move {}", val, best_move);
                } else {
                    println!("V(P) = {}", val);
                }
            },
            &Command::PrintHelp => { print_help(); },
            &Command::Quit => { println!("Bye bye!"); },
        }
    }
}

trait CLIPosition<M> : EvaluablePosition<M> + Display + Clone {
    fn parse_move(&self, input: &str) -> Result<M, String>;
    // Sort moves into the optimal order for display
    fn sort_moves(&self, moves: &mut Vec<&M>);
}

impl CLIPosition<Move> for SimplePosition {
    fn parse_move(self: &SimplePosition, input: &str) -> Result<Move, String> {
        let move_re = Regex::new(r"^(\d+) (\d+) ([a-zA-Z]+)$").unwrap();
        if let Some(caps) = move_re.captures(&input) {
            let x = caps[1].parse::<usize>().unwrap();
            let y = caps[2].parse::<usize>().unwrap();
            let side_s = caps[3].to_string();
            match parse_side(&side_s) {
                Some(side) => Ok(Move{x: x, y: y, side: side}),
                None => Err(format!("Unrecognised side: [{}]", side_s))
            }
        } else {
            Err(format!("Could not extract move from [{}]", input))
        }
    }

    fn sort_moves(self: &SimplePosition, moves: &mut Vec<&Move>) {
        moves.sort_by(|a, b| a.y.cmp(&b.y).then(a.x.cmp(&b.x)).then(a.side.cmp(&b.side)));
    }
}

fn print_help() {
    println!("Available commands:");
    println!("x y t/l/b/r - make move (x,y) top/left/bottom/right");
    println!("u x y t/l/b/r - undo move (x,y) top/left/bottom/right");
    println!("nv - calculate Nimstring value of current position");
    println!("eval - evaluate the current position");
    println!("help - print this help message");
    println!("quit/exit - exit program");
}

fn parse_side(side_s: &str) -> Option<Side> {
    if side_s == "l" || side_s == "left" { Some(Side::Left) }
    else if side_s == "r" || side_s == "right" { Some(Side::Right) }
    else if side_s == "t" || side_s == "top" { Some(Side::Top) }
    else if side_s == "b" || side_s == "bottom" { Some(Side::Bottom) }
    else { None }
}

fn parse_command<M, P: CLIPosition<M>>(input: &str, pos: &P) -> Result<Command<M>, String> {
    let input = input.to_lowercase();
    let move_re = Regex::new(r"^(\d.*)$").unwrap();
    let undo_move_re = Regex::new(r"^[uU] (\d.*)$").unwrap();
    if let Some(caps) = move_re.captures(&input) {
        match pos.parse_move(&caps[1]) {
            Ok(m) => return Ok(Command::MakeMove(m)),
            Err(e) => return Err(format!("Cannot extract move from [{}]: {}", input, e))
        }
    }
    if let Some(caps) = undo_move_re.captures(&input) {
        match pos.parse_move(&caps[1]) {
            Ok(m) => return Ok(Command::UndoMove(m)),
            Err(e) => return Err(format!("Cannot extract move from [{}]: {}", input, e))
        }
    }
    if "nv" == input {
        return Ok(Command::CalcNimstringValue);
    }
    if "eval" == input {
        return Ok(Command::Evaluate);
    }
    if "help" == input {
        return Ok(Command::PrintHelp);
    }
    if "exit" == input || "quit" == input {
        return Ok(Command::Quit);
    }
    Err("Unsupported command".to_string())
}

fn get_next_command<M, P: CLIPosition<M>>(pos: &P) -> Command<M> {
    loop {
        let mut input = String::new();
        if let Err(error) = io::stdin().read_line(&mut input) {
            println!("Error reading from standard input: {}", error);
            continue;
        }
        let input = input.trim();
        match parse_command(&input, pos) {
            Ok(command) => return command,
            Err(error) => {
                println!("Cannot execute [{}]: {}", input, error);
                println!("For help, try 'help'");
            }
        }
    }
}

fn main_loop_from<M, P>(pos: &mut P)
where M: Copy + Display + Eq + Hash, P: CLIPosition<M> {
    loop {
        println!("{}", pos);
        let command = get_next_command(pos);
        let start_time = time::precise_time_s();
        command.execute(pos);
        let end_time = time::precise_time_s();
        if command == Command::Quit {
            break;
        }
        let elapsed = end_time - start_time;
        if elapsed >= 0.1 {
            println!("({:.1} seconds)", end_time - start_time);
        }
        println!();
    }
}

fn parse_position<R: BufRead>(reader: R) -> SimplePosition {
    let mut lines = reader.lines();
    let size_spec = lines.next().expect("Empty file").expect("Could not read board size from first line");
    let size_re = Regex::new(r"^(\d+) (\d+)$").unwrap();
    let size_caps = size_re.captures(&size_spec).expect(&format!("Could not read board size from [{}]", size_spec));
    let width = size_caps[1].parse::<usize>().unwrap();
    let height = size_caps[2].parse::<usize>().unwrap();
    let mut pos = SimplePosition::new_game(width, height);
    for line in lines {
        let line = line.expect("Could not read line from file");
        if line.trim().len() == 0 {
            continue;
        }
        match parse_command(&line, &pos) {
            Ok(command) => command.execute(&mut pos),
            Err(error) => panic!("Cannot execute [{}]: {}", line, error),
        }
    }
    pos
}

// Enter the main loop of the CLI from the start of the game
pub fn main_loop_start(width: usize, height: usize) {
    main_loop_from(&mut SimplePosition::new_game(width, height));
}

// Execute a given file of commands (which must have the dimensions of the position on the first line)
// and then enter the CLI main loop.
pub fn main_loop_file(filename: &str) {
    let f = File::open(filename).expect(&format!("Could not open file [{}]", filename));
    let reader = io::BufReader::new(f);
    let mut pos = parse_position(reader);
    main_loop_from(&mut pos);
}

#[cfg(test)]
mod tests {
    use game::*;
    use cli::*;

    #[test]
    fn parse_make_move_cmd() {
        let pos = SimplePosition::new_game(6, 6);
        assert_eq!(Command::MakeMove(Move::new(3, 5, Side::Bottom)), parse_command("3 5 b", &pos).unwrap());
        assert_eq!(Command::MakeMove(Move::new(3, 5, Side::Bottom)), parse_command("3 5 Bottom", &pos).unwrap());
    }

    #[test]
    fn parse_undo_move_cmd() {
        let pos = SimplePosition::new_game(9, 9);
        assert_eq!(Command::UndoMove(Move::new(8, 6, Side::Left)), parse_command("u 8 6 l", &pos).unwrap());
        assert_eq!(Command::UndoMove(Move::new(8, 6, Side::Left)), parse_command("u 8 6 Left", &pos).unwrap());
    }

    #[test]
    fn parse_nimstring_value_cmd() {
        let pos = SimplePosition::new_game(1, 1);
        assert_eq!(Command::CalcNimstringValue, parse_command("nv", &pos).unwrap());
    }

    #[test]
    fn parse_evaluate_cmd() {
        let pos = SimplePosition::new_game(1, 1);
        assert_eq!(Command::Evaluate, parse_command("eval", &pos).unwrap());
    }

    #[test]
    fn parse_help_cmd() {
        let pos = SimplePosition::new_game(1, 1);
        assert_eq!(Command::PrintHelp, parse_command("help", &pos).unwrap());
    }

    #[test]
    fn parse_exit_cmd() {
        let pos = SimplePosition::new_game(1, 1);
        assert_eq!(Command::Quit, parse_command("quit", &pos).unwrap());
        assert_eq!(Command::Quit, parse_command("exit", &pos).unwrap());
    }
}
<|endoftext|>