<commit_before>#![crate_name="json_macros"]
#![crate_type="dylib"]
#![feature(phase, plugin_registrar, macro_rules, quote)]

#[phase(plugin, link)] extern crate log;
extern crate rustc;
extern crate syntax;
extern crate serialize;

use std::gc::Gc;
use syntax::ast::TokenTree;
use syntax::codemap::Span;
use rustc::plugin::Registry;

use syntax::ast;
use syntax::ext::base::{ExtCtxt, MacResult, MacExpr, DummyResult};
use syntax::parse::token;
use syntax::print::pprust;

type GcExpr = Gc<ast::Expr>;

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_macro("json", expand);
}

fn expand(cx: &mut ExtCtxt, sp: Span, tts: &[TokenTree]) -> Box<MacResult> {
    debug!("JSON token tree {}", tts);

    let tt = tts.get(0).expect("FIXME"); // FIXME
    let expr = match tt_to_expr(cx, sp, tt) {
        Some(e) => e,
        None => return DummyResult::expr(sp)
    };
    MacExpr::new(expr)
}

fn tt_to_expr(cx: &ExtCtxt, sp: Span, tt: &TokenTree) -> Option<GcExpr> {
    use syntax::ext::build::AstBuilder;

    match *tt {
        ast::TTTok(sp, ref tok) => token_to_expr(cx, sp, tok),
        ast::TTDelim(ref toks) => {
            match (**toks)[0] {
                // array
                ast::TTTok(sp, token::LBRACKET) => {
                    let exprs = match parse_array(cx, sp, toks.as_slice()) {
                        Some(e) => e,
                        None => return None
                    };
                    let exprs = cx.expr_vec(sp, exprs);
                    Some(quote_expr!(cx, {
                        {
                            let mut _vec = Vec::from_slice($exprs.as_slice());
                            ::serialize::json::List(_vec)
                        }
                    }))
                }
                // object
                ast::TTTok(sp, token::LBRACE) => {
                    let items = match parse_object(cx, sp, toks.as_slice()) {
                        Some(i) => i,
                        None => return None
                    };
                    let ob = quote_expr!(cx, _ob);
                    let mut stmts = vec![];
                    for &(key, value) in items.iter() {
                        stmts.push(quote_stmt!(cx, $ob.insert($key, $value)));
                    }

                    let res = quote_expr!(cx, {
                        {
                            let mut $ob = ::std::collections::TreeMap::new();
                            $stmts;
                            ::serialize::json::Object($ob)
                        }
                    });
                    Some(res)
                }
                ref tt => {
                    let pp = pprust::tt_to_string(tt);
                    let err = format!("unexpected `{}` in JSON", pp);
                    cx.span_err(best_span(sp, tt), err.as_slice());
                    None
                }
            }
        }
        // vvv are these code paths even reachable? copied this from
        // brainfuck_macros
        ast::TTSeq(sp, _, _, _) => {
            cx.span_err(sp, "`json!` doesn't support sequences");
            None
        }
        ast::TTNonterminal(sp, _) => {
            cx.span_err(sp, "`json!` doesn't support non-terminals");
            None
        }
    }
}

fn parse_array(cx: &ExtCtxt, sp: Span, tts: &[TokenTree]) -> Option<Vec<GcExpr>> {
    let mids = tts.slice(1, tts.len() - 1); // all but the []
    let mut exprs = Vec::with_capacity(mids.len() / 2);
    for (i, tt) in mids.iter().enumerate() {
        if i % 2 == 1 {
            match tt {
                &ast::TTTok(_, token::COMMA) => {
                    continue;
                }
                _ => {
                    expected_but_found(cx, sp, "`,`", tt);
                    return None;
                }
            }
        }
        let expr = tt_to_expr(cx, sp, tt);
        let expr = match expr {
            Some(e) => e,
            None => return None,
        };
        exprs.push(expr);
    }
    Some(exprs)
}

#[allow(dead_code)]
fn parse_object(cx: &ExtCtxt, sp: Span, tts: &[TokenTree]) -> Option<Vec<(GcExpr, GcExpr)>> {
    use syntax::ast::TTTok;
    use tok = syntax::parse::token;

    macro_rules! comma {
        () => {
            ::syntax::ast::TTTok(_, ::syntax::parse::token::COMMA)
        }
    }

    macro_rules! colon {
        () => {
            ::syntax::ast::TTTok(_, ::syntax::parse::token::COLON)
        };
        ($sp:ident) => {
            ::syntax::ast::TTTok($sp, ::syntax::parse::token::COLON)
        }
    }

    let mids = tts.slice(1, tts.len() - 1); // all but the {}
    let mut items = Vec::with_capacity(mids.len() / 4);
    if tts.len() == 0 {
        return Some(items);
    }

    // horrible
    for entry in mids.chunks(4) {
        let item = match entry {
            // "foo": VALUE | "foo": VALUE,
            [TTTok(_, tok::LIT_STR(ref n)), colon!(), ref v] |
            [TTTok(_, tok::LIT_STR(ref n)), colon!(), ref v, comma!()] => {
                let k = n.as_str();
                let v = tt_to_expr(cx, sp, v);
                if v.is_none() {
                    return None;
                }
                let k = quote_expr!(cx, $k.to_string());
                let v = quote_expr!(cx, $v);
                (k, v)
            }
            // "foo": VALUE X
            [TTTok(_, tok::LIT_STR(_)), colon!(), _, ref tt] => {
                expected_but_found(cx, sp, "`,`", tt);
                return None;
            }
            [TTTok(_, tok::LIT_STR(_)), colon!(sp)] => {
                cx.span_err(sp, "found `:` but no value afterwards");
                return None;
            }
            [TTTok(_, tok::LIT_STR(_)), ref tt, ..] => {
                expected_but_found(cx, sp, "`:`", tt);
                return None;
            }
            [TTTok(sp, tok::LIT_STR(_))] => {
                cx.span_err(sp, "found name but no colon-value afterwards");
                return None;
            }
            [ref tt, ..] => {
                expected_but_found(cx, sp, "string literal", tt);
                return None;
            }
            [] => unreachable!(), // chunks() never returns an empty slice
            // _ => unimplemented!()
        };
        items.push(item);
    }

    Some(items)
}

fn token_to_expr(cx: &ExtCtxt, sp: Span, tok: &token::Token) -> Option<GcExpr> {
    use std::from_str::FromStr;
    use syntax::print::pprust;

    match *tok {
        token::LIT_STR(ref n) => {
            let s = n.as_str();
            Some(quote_expr!(cx, {
                ::serialize::json::String($s.to_string())
            }))
        }
        token::LIT_INTEGER(ref n) => {
            let s = n.as_str();
            let n: i64 = FromStr::from_str(s).unwrap(); // FIXME: is i64 right?
            Some(quote_expr!(cx, {
                ::serialize::json::Number($n as f64)
            }))
        }
        token::LIT_FLOAT(ref _n) => {
            cx.span_err(sp, format!("json! does not yet support float literals").as_slice());
            None
        }
        token::IDENT(ref n, false) if n.as_str() == "null" => {
            Some(quote_expr!(cx, {
                ::serialize::json::Null
            }))
        }
        ref t @ token::IDENT(..) if token::is_keyword(token::keywords::True, t) => {
            Some(quote_expr!(cx, { ::serialize::json::Boolean(true) }))
        }
        ref t @ token::IDENT(..) if token::is_keyword(token::keywords::False, t) => {
            Some(quote_expr!(cx, { ::serialize::json::Boolean(false) }))
        }
        _ => {
            let tt = ast::TTTok(sp, tok.clone());
            let s = pprust::tt_to_string(&tt);
            cx.span_err(sp, format!("unexpected `{}` in JSON", s).as_slice());
            None
        }
    }
}

fn best_span(sp: Span, tt: &TokenTree) -> Span {
    let sp = match *tt {
        ast::TTTok(tok_sp, _) => tok_sp,
        ast::TTDelim(ref tts) => {
            match (**tts)[0] {
                ast::TTTok(bra_sp, _) => bra_sp,
                _ => sp
            }
        }
        _ => sp // the span passed into the function!
    };
    sp
}

fn expected_but_found(cx: &ExtCtxt, sp: Span, expected: &str, found: &TokenTree) {
    let pp = pprust::tt_to_string(found);
    let err = format!("expected {} but found: `{}`", expected, pp);
    cx.span_err(best_span(sp, found), err.as_slice());
}
<commit_msg>Handle empty macro case, other slight nits<commit_after>#![crate_name="json_macros"]
#![crate_type="dylib"]
#![feature(phase, plugin_registrar, macro_rules, quote)]

#[phase(plugin, link)] extern crate log;
extern crate rustc;
extern crate syntax;
extern crate serialize;

use std::gc::Gc;
use syntax::ast::TokenTree;
use syntax::codemap::Span;
use rustc::plugin::Registry;

use syntax::ast;
use syntax::ext::base::{ExtCtxt, MacResult, MacExpr, DummyResult};
use syntax::parse::token;
use syntax::print::pprust;

type GcExpr = Gc<ast::Expr>;

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_macro("json", expand);
}

fn expand(cx: &mut ExtCtxt, sp: Span, tts: &[TokenTree]) -> Box<MacResult> {
    debug!("JSON token tree {}", tts);

    let tt = match tts.get(0) {
        Some(tt) => tt,
        None => {
            cx.span_err(sp, "expected JSON literal");
            return DummyResult::expr(sp);
        }
    };

    let expr = match tt_to_expr(cx, sp, tt) {
        Some(e) => e,
        None => return DummyResult::expr(sp)
    };
    MacExpr::new(expr)
}

fn tt_to_expr(cx: &ExtCtxt, sp: Span, tt: &TokenTree) -> Option<GcExpr> {
    use syntax::ext::build::AstBuilder;

    match *tt {
        ast::TTTok(sp, ref tok) => token_to_expr(cx, sp, tok),
        ast::TTDelim(ref toks) => {
            match (**toks)[0] {
                // array
                ast::TTTok(sp, token::LBRACKET) => {
                    let exprs = match parse_array(cx, sp, toks.as_slice()) {
                        Some(e) => e,
                        None => return None
                    };
                    let exprs = cx.expr_vec(sp, exprs);
                    Some(quote_expr!(cx, {
                        {
                            let mut _vec = Vec::from_slice($exprs.as_slice());
                            ::serialize::json::List(_vec)
                        }
                    }))
                }
                // object
                ast::TTTok(sp, token::LBRACE) => {
                    let items = match parse_object(cx, sp, toks.as_slice()) {
                        Some(i) => i,
                        None => return None
                    };
                    let ob = quote_expr!(cx, _ob);
                    let mut stmts = vec![];
                    for &(key, value) in items.iter() {
                        stmts.push(quote_stmt!(cx, $ob.insert($key, $value)));
                    }

                    let res = quote_expr!(cx, {
                        {
                            let mut $ob = ::std::collections::TreeMap::new();
                            $stmts;
                            ::serialize::json::Object($ob)
                        }
                    });
                    Some(res)
                }
                // FIXME: could we allow arbitrary expressions inside `()`?
                ref tt => {
                    let pp = pprust::tt_to_string(tt);
                    let err = format!("unexpected `{}` in `json!`", pp);
                    cx.span_err(best_span(sp, tt), err.as_slice());
                    None
                }
            }
        }
        // vvv are these code paths even reachable? copied this from
        // brainfuck_macros
        ast::TTSeq(sp, _, _, _) => {
            cx.span_err(sp, "`json!` doesn't support sequences");
            None
        }
        ast::TTNonterminal(sp, _) => {
            cx.span_err(sp, "`json!` doesn't support non-terminals");
            None
        }
    }
}

fn parse_array(cx: &ExtCtxt, sp: Span, tts: &[TokenTree]) -> Option<Vec<GcExpr>> {
    let mids = tts.slice(1, tts.len() - 1); // all but the []
    let mut exprs = Vec::with_capacity(mids.len() / 2);
    for (i, tt) in mids.iter().enumerate() {
        if i % 2 == 1 {
            match tt {
                &ast::TTTok(_, token::COMMA) => {
                    continue;
                }
                _ => {
                    expected_but_found(cx, sp, "`,`", tt);
                    return None;
                }
            }
        }
        let expr = tt_to_expr(cx, sp, tt);
        let expr = match expr {
            Some(e) => e,
            None => return None,
        };
        exprs.push(expr);
    }
    Some(exprs)
}

#[allow(dead_code)]
fn parse_object(cx: &ExtCtxt, sp: Span, tts: &[TokenTree]) -> Option<Vec<(GcExpr, GcExpr)>> {
    use syntax::ast::TTTok;
    use tok = syntax::parse::token;

    macro_rules! comma {
        () => {
            ::syntax::ast::TTTok(_, ::syntax::parse::token::COMMA)
        }
    }

    macro_rules! colon {
        () => {
            ::syntax::ast::TTTok(_, ::syntax::parse::token::COLON)
        };
        ($sp:ident) => {
            ::syntax::ast::TTTok($sp, ::syntax::parse::token::COLON)
        }
    }

    let mids = tts.slice(1, tts.len() - 1); // all but the {}
    let mut items = Vec::with_capacity(mids.len() / 4);
    if tts.len() == 0 {
        return Some(items);
    }

    // horrible
    for entry in mids.chunks(4) {
        let item = match entry {
            // "foo": VALUE | "foo": VALUE,
            [TTTok(_, tok::LIT_STR(ref n)), colon!(), ref v] |
            [TTTok(_, tok::LIT_STR(ref n)), colon!(), ref v, comma!()] => {
                let k = n.as_str();
                let v = tt_to_expr(cx, sp, v);
                if v.is_none() {
                    return None;
                }
                let k = quote_expr!(cx, $k.to_string());
                let v = quote_expr!(cx, $v);
                (k, v)
            }
            // "foo": VALUE X
            [TTTok(_, tok::LIT_STR(_)), colon!(), _, ref tt] => {
                expected_but_found(cx, sp, "`,`", tt);
                return None;
            }
            [TTTok(_, tok::LIT_STR(_)), colon!(sp)] => {
                cx.span_err(sp, "found `:` but no value afterwards");
                return None;
            }
            [TTTok(_, tok::LIT_STR(_)), ref tt, ..] => {
                expected_but_found(cx, sp, "`:`", tt);
                return None;
            }
            [TTTok(sp, tok::LIT_STR(_))] => {
                cx.span_err(sp, "found name but no colon-value afterwards");
                return None;
            }
            [ref tt, ..] => {
                expected_but_found(cx, sp, "string literal", tt);
                return None;
            }
            [] => unreachable!(), // chunks() never returns an empty slice
        };
        items.push(item);
    }

    Some(items)
}

fn token_to_expr(cx: &ExtCtxt, sp: Span, tok: &token::Token) -> Option<GcExpr> {
    use std::from_str::FromStr;
    use syntax::print::pprust;

    match *tok {
        token::LIT_STR(ref n) => {
            let s = n.as_str();
            Some(quote_expr!(cx, {
                ::serialize::json::String($s.to_string())
            }))
        }
        token::LIT_INTEGER(ref n) => {
            let s = n.as_str();
            let n: i64 = FromStr::from_str(s).unwrap(); // FIXME: is i64 right?
            Some(quote_expr!(cx, {
                ::serialize::json::Number($n as f64)
            }))
        }
        token::LIT_FLOAT(..) => {
            cx.span_err(sp, format!("`json!` does not yet support float literals").as_slice());
            None
        }
        token::IDENT(ref id, false) if id.as_str() == "null" => {
            Some(quote_expr!(cx, {
                ::serialize::json::Null
            }))
        }
        ref t @ token::IDENT(..) if token::is_keyword(token::keywords::True, t) => {
            Some(quote_expr!(cx, { ::serialize::json::Boolean(true) }))
        }
        ref t @ token::IDENT(..) if token::is_keyword(token::keywords::False, t) => {
            Some(quote_expr!(cx, { ::serialize::json::Boolean(false) }))
        }
        _ => {
            let tt = ast::TTTok(sp, tok.clone());
            let s = pprust::tt_to_string(&tt);
            cx.span_err(sp, format!("unexpected `{}` in JSON", s).as_slice());
            None
        }
    }
}

fn best_span(sp: Span, tt: &TokenTree) -> Span {
    let sp = match *tt {
        ast::TTTok(tok_sp, _) => tok_sp,
        ast::TTDelim(ref tts) => {
            match (**tts)[0] {
                ast::TTTok(bra_sp, _) => bra_sp,
                _ => sp
            }
        }
        _ => sp // the span passed into the function!
    };
    sp
}

fn expected_but_found(cx: &ExtCtxt, sp: Span, expected: &str, found: &TokenTree) {
    let pp = pprust::tt_to_string(found);
    let err = format!("expected {} but found: `{}`", expected, pp);
    cx.span_err(best_span(sp, found), err.as_slice());
}
<|endoftext|>


<commit_before>use std::collections::BTreeMap;

use rustc_serialize::json::Json;

use term::Term;
use mapping::Mapping;

#[derive(Debug)]
pub struct Document {
    pub id: String,
    pub fields: BTreeMap<String, Vec<Term>>,
}

impl Document {
    pub fn from_json(id: String, data: Json, mapping: &Mapping) -> Document {
        let mut fields = BTreeMap::new();
        let mut all_field_tokens: Vec<Term> = Vec::new();

        for (field_name, field_value) in data.as_object().unwrap() {
            let processed_value = if let Some(field_mapping) = mapping.fields.get(field_name) {
                let value = field_mapping.process_value_for_index(field_value.clone());

                match value {
                    Some(ref value) => {
                        if field_mapping.is_in_all {
                            all_field_tokens.extend(value.iter().cloned());
                        }
                    }
                    None => {
                        warn!("Unprocessable value: {}", field_value);
                    }
                }

                value
            } else {
                Some(vec![Term::from_json(field_value)])
            };

            if let Some(field_value) = processed_value {
                fields.insert(field_name.clone(), field_value);
            }
        }

        // Insert _all field
        fields.insert("_all".to_owned(), all_field_tokens);

        Document {
            id: id,
            fields: fields,
        }
    }
}
<commit_msg>Tidy Document::from_json<commit_after>use std::collections::BTreeMap;

use rustc_serialize::json::Json;

use term::Term;
use mapping::Mapping;

#[derive(Debug)]
pub struct Document {
    pub id: String,
    pub fields: BTreeMap<String, Vec<Term>>,
}

impl Document {
    pub fn from_json(id: String, data: Json, mapping: &Mapping) -> Document {
        let mut fields = BTreeMap::new();
        let mut all_field_tokens: Vec<Term> = Vec::new();

        for (field_name, field_value) in data.as_object().unwrap() {
            match mapping.fields.get(field_name) {
                Some(field_mapping) => {
                    let value = field_mapping.process_value_for_index(field_value.clone());

                    match value {
                        Some(value) => {
                            // Copy the field's value into the _all field
                            if field_mapping.is_in_all {
                                all_field_tokens.extend(value.iter().cloned());
                            }

                            // Insert the field
                            fields.insert(field_name.clone(), value);
                        }
                        None => {
                            // TODO: Should probably be an error
                            warn!("Unprocessable value: {}", field_value);
                        }
                    }
                }
                None => {
                    // No mapping found, just insert the value as-is
                    // TODO: This should probably be an error
                    fields.insert(field_name.clone(), vec![Term::from_json(field_value)]);
                }
            }
        }

        // Insert _all field
        fields.insert("_all".to_owned(), all_field_tokens);

        Document {
            id: id,
            fields: fields,
        }
    }
}
<|endoftext|>


<commit_before>use std::clone::Clone;
use std::collections::HashMap;
use std::default::Default;
use std::path::{Path, PathBuf};

use chrono::{Datelike, Timelike};
use failure::ResultExt;
use itertools;
use jsonfeed;
use liquid;
use liquid::model::Value;
use liquid::Object;
use liquid::ValueView;
use regex::Regex;
use rss;

use crate::cobalt_model;
use crate::cobalt_model::files;
use crate::cobalt_model::permalink;
use crate::cobalt_model::slug;
use crate::cobalt_model::Minify;
use crate::error::*;

pub struct RenderContex<'a> {
    pub parser: &'a cobalt_model::Liquid,
    pub markdown: &'a cobalt_model::Markdown,
    pub vimwiki: &'a cobalt_model::Vimwiki,
    pub globals: &'a Object,
    pub minify: Minify,
}

#[cfg(not(feature = "html-minifier"))]
fn minify_if_enabled(html: String, _context: &RenderContex, _file_path: &Path) -> Result<String> {
    Ok(html)
}

#[cfg(feature = "html-minifier")]
fn minify_if_enabled(html: String, context: &RenderContex, file_path: &Path) -> Result<String> {
    let extension = file_path.extension().unwrap_or_else(|| Default::default());
    if context.minify.html && (extension == "html" || extension == "htm") {
        Ok(html_minifier::minify(html)?)
    } else {
        Ok(html)
    }
}

/// Convert the source file's relative path into a format useful for generating permalinks that
/// mirror the source directory hierarchy.
fn format_path_variable(source_file: &Path) -> String {
    let parent = source_file
        .parent()
        .and_then(|p| p.to_str())
        .unwrap_or("")
        .to_owned();
    let mut path = parent.replace("\\", "/");
    if path.starts_with("./") {
        path.remove(0);
    }
    if path.starts_with('/') {
        path.remove(0);
    }
    path
}

pub fn permalink_attributes(front: &cobalt_model::Frontmatter, dest_file: &Path) -> Object {
    let mut attributes = Object::new();

    attributes.insert(
        "parent".into(),
        Value::scalar(format_path_variable(dest_file)),
    );

    let filename = dest_file
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("")
        .to_owned();
    attributes.insert("name".into(), Value::scalar(filename));

    attributes.insert("ext".into(), Value::scalar(".html"));

    // TODO(epage): Add `collection` (the collection's slug), see #257
    // or `parent.slug`, see #323

    attributes.insert("slug".into(), Value::scalar(front.slug.clone()));

    attributes.insert(
        "categories".into(),
        Value::scalar(itertools::join(
            front.categories.iter().map(slug::slugify),
            "/",
        )),
    );

    if let Some(ref date) = front.published_date {
        attributes.insert("year".into(), Value::scalar(date.year().to_string()));
        attributes.insert(
            "month".into(),
            Value::scalar(format!("{:02}", &date.month())),
        );
        attributes.insert("i_month".into(), Value::scalar(date.month().to_string()));
        attributes.insert("day".into(), Value::scalar(format!("{:02}", &date.day())));
        attributes.insert("i_day".into(), Value::scalar(date.day().to_string()));
        attributes.insert("hour".into(), Value::scalar(format!("{:02}", &date.hour())));
        attributes.insert(
            "minute".into(),
            Value::scalar(format!("{:02}", &date.minute())),
        );
        attributes.insert(
            "second".into(),
            Value::scalar(format!("{:02}", &date.second())),
        );
    }

    attributes.insert("data".into(), Value::Object(front.data.clone()));

    attributes
}

fn document_attributes(
    front: &cobalt_model::Frontmatter,
    source_file: &Path,
    url_path: &str,
) -> Object {
    let categories = Value::Array(
        front
            .categories
            .iter()
            .cloned()
            .map(Value::scalar)
            .collect(),
    );
    // Reason for `file`:
    // - Allow access to assets in the original location
    // - Ease linking back to page's source
    let file: Object = vec![
        (
            "permalink".into(),
            Value::scalar(source_file.to_str().unwrap_or("").to_owned()),
        ),
        (
            "parent".into(),
            Value::scalar(
                source_file
                    .parent()
                    .and_then(Path::to_str)
                    .unwrap_or("")
                    .to_owned(),
            ),
        ),
    ]
    .into_iter()
    .collect();
    let attributes = vec![
        ("permalink".into(), Value::scalar(url_path.to_owned())),
        ("title".into(), Value::scalar(front.title.clone())),
        ("slug".into(), Value::scalar(front.slug.clone())),
        (
            "description".into(),
            Value::scalar(front.description.as_deref().unwrap_or("").to_owned()),
        ),
        ("categories".into(), categories),
        ("is_draft".into(), Value::scalar(front.is_draft)),
        ("weight".into(), Value::scalar(front.weight)),
        ("file".into(), Value::Object(file)),
        ("collection".into(), Value::scalar(front.collection.clone())),
        ("data".into(), Value::Object(front.data.clone())),
    ];
    let mut attributes: Object = attributes.into_iter().collect();

    if let Some(ref tags) = front.tags {
        let tags = Value::Array(tags.iter().cloned().map(Value::scalar).collect());
        attributes.insert("tags".into(), tags);
    }

    if let Some(ref published_date) = front.published_date {
        attributes.insert(
            "published_date".into(),
            Value::scalar(liquid::model::DateTime::from(*published_date)),
        );
    }

    attributes
}

#[derive(Debug, Clone)]
pub struct Document {
    pub url_path: String,
    pub file_path: PathBuf,
    pub content: String,
    pub attributes: Object,
    pub front: cobalt_model::Frontmatter,
}

impl Document {
    pub fn new(
        url_path: String,
        file_path: PathBuf,
        content: String,
        attributes: Object,
        front: cobalt_model::Frontmatter,
    ) -> Document {
        Document {
            url_path,
            file_path,
            content,
            attributes,
            front,
        }
    }

    pub fn parse(
        src_path: &Path,
        rel_path: &Path,
        default_front: cobalt_config::Frontmatter,
    ) -> Result<Document> {
        trace!("Parsing {:?}", rel_path);
        let content = files::read_file(src_path)?;
        let builder = cobalt_config::Document::parse(&content)?;
        let (front, content) = builder.into_parts();
        let front = front.merge_path(rel_path).merge(&default_front);

        let front = cobalt_model::Frontmatter::from_config(front)?;

        let (file_path, url_path) = {
            let perma_attributes = permalink_attributes(&front, rel_path);
            let url_path =
                permalink::explode_permalink(front.permalink.as_str(), &perma_attributes)
                    .with_context(|_| {
                        failure::format_err!("Failed to create permalink `{}`", front.permalink)
                    })?;
            let file_path = permalink::format_url_as_file(&url_path);
            (file_path, url_path)
        };

        let doc_attributes = document_attributes(&front, rel_path, url_path.as_ref());

        Ok(Document::new(
            url_path,
            file_path,
            content,
            doc_attributes,
            front,
        ))
    }

    /// Metadata for generating RSS feeds
    pub fn to_rss(&self, root_url: &str) -> Result<rss::Item> {
        let link = format!("{}/{}", root_url, &self.url_path);
        let guid = rss::GuidBuilder::default()
            .value(link.clone())
            .permalink(true)
            .build()
            .map_err(failure::err_msg)?;

        let item = rss::ItemBuilder::default()
            .title(Some(self.front.title.clone()))
            .link(Some(link))
            .guid(Some(guid))
            .pub_date(self.front.published_date.map(|date| date.to_rfc2822()))
            .description(self.description_to_str())
            .build()
            .map_err(failure::err_msg)?;
        Ok(item)
    }

    /// Metadata for generating JSON feeds
    pub fn to_jsonfeed(&self, root_url: &str) -> jsonfeed::Item {
        let link = format!("{}/{}", root_url, &self.url_path);

        jsonfeed::Item {
            id: link.clone(),
            url: Some(link),
            title: Some(self.front.title.clone()),
            content: jsonfeed::Content::Html(
                self.description_to_str().unwrap_or_else(|| "".into()),
            ),
            date_published: self.front.published_date.map(|date| date.to_rfc2822()),
            // TODO completely implement categories, see Issue 131
            tags: Some(self.front.categories.clone()),
            ..Default::default()
        }
    }

    pub fn to_sitemap<T: std::io::Write>(
        &self,
        root_url: &str,
        writer: &mut sitemap::writer::UrlSetWriter<T>,
    ) -> Result<()> {
        let link = format!("{}/{}", root_url, &self.url_path);
        let mut url = sitemap::structs::UrlEntry::builder();
        url = url.loc(link);
        if let Some(date) = self.front.published_date {
            url = url.lastmod(*date);
        }
        writer.url(url)?;
        Ok(())
    }

    fn description_to_str(&self) -> Option<String> {
        self.front
            .description
            .clone()
            .or_else(|| {
                self.attributes.get("excerpt").and_then(|excerpt| {
                    if excerpt.is_nil() {
                        None
                    } else {
                        Some(excerpt.render().to_string())
                    }
                })
            })
            .or_else(|| {
                self.attributes
                    .get("content")
                    .map(|s| s.render().to_string())
            })
    }

    /// Renders liquid templates into HTML in the context of current document.
    ///
    /// Takes `content` string and returns rendered HTML. This function doesn't
    /// take `"extends"` attribute into account. This function can be used for
    /// rendering content or excerpt.
    fn render_html(&self, content: &str, context: &RenderContex) -> Result<String> {
        let html = if self.front.templated {
            let template = context.parser.parse(content)?;
            template.render(context.globals)?
        } else {
            content.to_owned()
        };

        let html = match self.front.format {
            cobalt_model::SourceFormat::Raw => html,
            cobalt_model::SourceFormat::Markdown => context.markdown.parse(&html)?,
            cobalt_model::SourceFormat::Vimwiki => context.vimwiki.parse(&html)?,
        };

        Ok(html)
    }

    /// Renders the excerpt and adds it to attributes of the document.
    ///
    /// The excerpt is either taken from the `excerpt` frontmatter setting, if
    /// given, or extracted from the content, if `excerpt_separator` is not
    /// empty. When neither condition applies, the excerpt is set to the `Nil`
    /// value.
    pub fn render_excerpt(&mut self, context: &RenderContex) -> Result<()> {
        let value = if let Some(excerpt_str) = self.front.excerpt.as_ref() {
            let excerpt = self.render_html(excerpt_str, context)?;
            Value::scalar(excerpt)
        } else if self.front.excerpt_separator.is_empty() {
            Value::Nil
        } else {
            let excerpt = extract_excerpt(
                &self.content,
                self.front.format,
                &self.front.excerpt_separator,
            );
            let excerpt = self.render_html(&excerpt, context)?;
            Value::scalar(excerpt)
        };

        self.attributes.insert("excerpt".into(), value);
        Ok(())
    }

    /// Renders the content and adds it to attributes of the document.
    ///
    /// When we say "content" we mean only this document without extended layout.
    pub fn render_content(&mut self, context: &RenderContex) -> Result<()> {
        let content_html = self.render_html(&self.content, context)?;
        self.attributes
            .insert("content".into(), Value::scalar(content_html));
        Ok(())
    }

    /// Renders the document to an HTML string.
    ///
    /// Side effects:
    ///
    /// * layout may be inserted to layouts cache
    pub fn render(
        &mut self,
        context: &RenderContex,
        layouts: &HashMap<String, String>,
    ) -> Result<String> {
        if let Some(ref layout) = self.front.layout {
            let layout_data_ref = layouts.get(layout).ok_or_else(|| {
                failure::format_err!(
                    "Layout {} does not exist (referenced in {}).",
                    layout,
                    self.file_path.display()
                )
            })?;

            let template = context
                .parser
                .parse(layout_data_ref)
                .with_context(|_| failure::format_err!("Failed to parse layout {:?}", layout))?;
            let content_html = template
                .render(context.globals)
                .with_context(|_| failure::format_err!("Failed to render layout {:?}", layout))?;
            let content_html = minify_if_enabled(content_html, context, &self.file_path)?;
            Ok(content_html)
        } else {
            let path = &[
                kstring::KStringCow::from_static("page").into(),
                kstring::KStringCow::from_static("content").into(),
            ];
            let content_html = liquid::model::try_find(context.globals, path)
                .ok_or_else(|| failure::err_msg("Internal error: page isn't in globals"))?
                .render()
                .to_string();

            let content_html = minify_if_enabled(content_html, context, &self.file_path)?;
            Ok(content_html)
        }
    }
}

fn extract_excerpt_raw(content: &str, excerpt_separator: &str) -> String {
    content
        .split(excerpt_separator)
        .next()
        .unwrap_or(content)
        .to_owned()
}

fn extract_excerpt_markdown(content: &str, excerpt_separator: &str) -> String {
    lazy_static! {
        static ref MARKDOWN_REF: Regex = Regex::new(r"(?m:^ {0,3}\[[^\]]+\]:.+$)").unwrap();
    }

    let mut trail = String::new();

    if MARKDOWN_REF.is_match(content) {
        for mat in MARKDOWN_REF.find_iter(content) {
            trail.push_str(mat.as_str());
            trail.push('\n');
        }
    }
    trail + content.split(excerpt_separator).next().unwrap_or(content)
}

fn extract_excerpt(
    content: &str,
    format: cobalt_model::SourceFormat,
    excerpt_separator: &str,
) -> String {
    match format {
        cobalt_model::SourceFormat::Markdown => {
            extract_excerpt_markdown(content, excerpt_separator)
        }
        cobalt_model::SourceFormat::Vimwiki | cobalt_model::SourceFormat::Raw => {
            extract_excerpt_raw(content, excerpt_separator)
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn format_path_variable_file() {
        let input = Path::new("/hello/world/file.liquid");
        let actual = format_path_variable(input);
        assert_eq!(actual, "hello/world");
    }

    #[test]
    fn format_path_variable_relative() {
        let input = Path::new("hello/world/file.liquid");
        let actual = format_path_variable(input);
        assert_eq!(actual, "hello/world");

        let input = Path::new("./hello/world/file.liquid");
        let actual = format_path_variable(input);
        assert_eq!(actual, "hello/world");
    }
}
<commit_msg>refactor: Remove a layer<commit_after>use std::clone::Clone;
use std::collections::HashMap;
use std::default::Default;
use std::path::{Path, PathBuf};

use chrono::{Datelike, Timelike};
use failure::ResultExt;
use itertools;
use jsonfeed;
use liquid;
use liquid::model::Value;
use liquid::Object;
use liquid::ValueView;
use regex::Regex;
use rss;

use crate::cobalt_model;
use crate::cobalt_model::files;
use crate::cobalt_model::permalink;
use crate::cobalt_model::slug;
use crate::cobalt_model::Minify;
use crate::error::*;

pub struct RenderContex<'a> {
    pub parser: &'a cobalt_model::Liquid,
    pub markdown: &'a cobalt_model::Markdown,
    pub vimwiki: &'a cobalt_model::Vimwiki,
    pub globals: &'a Object,
    pub minify: Minify,
}

#[cfg(not(feature = "html-minifier"))]
fn minify_if_enabled(html: String, _context: &RenderContex, _file_path: &Path) -> Result<String> {
    Ok(html)
}

#[cfg(feature = "html-minifier")]
fn minify_if_enabled(html: String, context: &RenderContex, file_path: &Path) -> Result<String> {
    let extension = file_path.extension().unwrap_or_else(|| Default::default());
    if context.minify.html && (extension == "html" || extension == "htm") {
        Ok(html_minifier::minify(html)?)
    } else {
        Ok(html)
    }
}

/// Convert the source file's relative path into a format useful for generating permalinks that
/// mirror the source directory hierarchy.
fn format_path_variable(source_file: &Path) -> String {
    let parent = source_file
        .parent()
        .and_then(|p| p.to_str())
        .unwrap_or("")
        .to_owned();
    let mut path = parent.replace("\\", "/");
    if path.starts_with("./") {
        path.remove(0);
    }
    if path.starts_with('/') {
        path.remove(0);
    }
    path
}

pub fn permalink_attributes(front: &cobalt_model::Frontmatter, dest_file: &Path) -> Object {
    let mut attributes = Object::new();

    attributes.insert(
        "parent".into(),
        Value::scalar(format_path_variable(dest_file)),
    );

    let filename = dest_file
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("")
        .to_owned();
    attributes.insert("name".into(), Value::scalar(filename));

    attributes.insert("ext".into(), Value::scalar(".html"));

    // TODO(epage): Add `collection` (the collection's slug), see #257
    // or `parent.slug`, see #323

    attributes.insert("slug".into(), Value::scalar(front.slug.clone()));

    attributes.insert(
        "categories".into(),
        Value::scalar(itertools::join(
            front.categories.iter().map(slug::slugify),
            "/",
        )),
    );

    if let Some(ref date) = front.published_date {
        attributes.insert("year".into(), Value::scalar(date.year().to_string()));
        attributes.insert(
            "month".into(),
            Value::scalar(format!("{:02}", &date.month())),
        );
        attributes.insert("i_month".into(), Value::scalar(date.month().to_string()));
        attributes.insert("day".into(), Value::scalar(format!("{:02}", &date.day())));
        attributes.insert("i_day".into(), Value::scalar(date.day().to_string()));
        attributes.insert("hour".into(), Value::scalar(format!("{:02}", &date.hour())));
        attributes.insert(
            "minute".into(),
            Value::scalar(format!("{:02}", &date.minute())),
        );
        attributes.insert(
            "second".into(),
            Value::scalar(format!("{:02}", &date.second())),
        );
    }

    attributes.insert("data".into(), Value::Object(front.data.clone()));

    attributes
}

fn document_attributes(
    front: &cobalt_model::Frontmatter,
    source_file: &Path,
    url_path: &str,
) -> Object {
    let categories = Value::Array(
        front
            .categories
            .iter()
            .cloned()
            .map(Value::scalar)
            .collect(),
    );
    // Reason for `file`:
    // - Allow access to assets in the original location
    // - Ease linking back to page's source
    let file: Object = vec![
        (
            "permalink".into(),
            Value::scalar(source_file.to_str().unwrap_or("").to_owned()),
        ),
        (
            "parent".into(),
            Value::scalar(
                source_file
                    .parent()
                    .and_then(Path::to_str)
                    .unwrap_or("")
                    .to_owned(),
            ),
        ),
    ]
    .into_iter()
    .collect();
    let attributes = vec![
        ("permalink".into(), Value::scalar(url_path.to_owned())),
        ("title".into(), Value::scalar(front.title.clone())),
        ("slug".into(), Value::scalar(front.slug.clone())),
        (
            "description".into(),
            Value::scalar(front.description.as_deref().unwrap_or("").to_owned()),
        ),
        ("categories".into(), categories),
        ("is_draft".into(), Value::scalar(front.is_draft)),
        ("weight".into(), Value::scalar(front.weight)),
        ("file".into(), Value::Object(file)),
        ("collection".into(), Value::scalar(front.collection.clone())),
        ("data".into(), Value::Object(front.data.clone())),
    ];
    let mut attributes: Object = attributes.into_iter().collect();

    if let Some(ref tags) = front.tags {
        let tags = Value::Array(tags.iter().cloned().map(Value::scalar).collect());
        attributes.insert("tags".into(), tags);
    }

    if let Some(ref published_date) = front.published_date {
        attributes.insert(
            "published_date".into(),
            Value::scalar(liquid::model::DateTime::from(*published_date)),
        );
    }

    attributes
}

#[derive(Debug, Clone)]
pub struct Document {
    pub url_path: String,
    pub file_path: PathBuf,
    pub content: String,
    pub attributes: Object,
    pub front: cobalt_model::Frontmatter,
}

impl Document {
    pub fn parse(
        src_path: &Path,
        rel_path: &Path,
        default_front: cobalt_config::Frontmatter,
    ) -> Result<Document> {
        trace!("Parsing {:?}", rel_path);
        let content = files::read_file(src_path)?;
        let builder = cobalt_config::Document::parse(&content)?;
        let (front, content) = builder.into_parts();
        let front = front.merge_path(rel_path).merge(&default_front);

        let front = cobalt_model::Frontmatter::from_config(front)?;

        let (file_path, url_path) = {
            let perma_attributes = permalink_attributes(&front, rel_path);
            let url_path =
                permalink::explode_permalink(front.permalink.as_str(), &perma_attributes)
                    .with_context(|_| {
                        failure::format_err!("Failed to create permalink `{}`", front.permalink)
                    })?;
            let file_path = permalink::format_url_as_file(&url_path);
            (file_path, url_path)
        };

        let doc_attributes = document_attributes(&front, rel_path, url_path.as_ref());

        Ok(Document {
            url_path,
            file_path,
            content,
            attributes: doc_attributes,
            front,
        })
    }

    /// Metadata for generating RSS feeds
    pub fn to_rss(&self, root_url: &str) -> Result<rss::Item> {
        let link = format!("{}/{}", root_url, &self.url_path);
        let guid = rss::GuidBuilder::default()
            .value(link.clone())
            .permalink(true)
            .build()
            .map_err(failure::err_msg)?;

        let item = rss::ItemBuilder::default()
            .title(Some(self.front.title.clone()))
            .link(Some(link))
            .guid(Some(guid))
            .pub_date(self.front.published_date.map(|date| date.to_rfc2822()))
            .description(self.description_to_str())
            .build()
            .map_err(failure::err_msg)?;
        Ok(item)
    }

    /// Metadata for generating JSON feeds
    pub fn to_jsonfeed(&self, root_url: &str) -> jsonfeed::Item {
        let link = format!("{}/{}", root_url, &self.url_path);

        jsonfeed::Item {
            id: link.clone(),
            url: Some(link),
            title: Some(self.front.title.clone()),
            content: jsonfeed::Content::Html(
                self.description_to_str().unwrap_or_else(|| "".into()),
            ),
            date_published: self.front.published_date.map(|date| date.to_rfc2822()),
            // TODO completely implement categories, see Issue 131
            tags: Some(self.front.categories.clone()),
            ..Default::default()
        }
    }

    pub fn to_sitemap<T: std::io::Write>(
        &self,
        root_url: &str,
        writer: &mut sitemap::writer::UrlSetWriter<T>,
    ) -> Result<()> {
        let link = format!("{}/{}", root_url, &self.url_path);
        let mut url = sitemap::structs::UrlEntry::builder();
        url = url.loc(link);
        if let Some(date) = self.front.published_date {
            url = url.lastmod(*date);
        }
        writer.url(url)?;
        Ok(())
    }

    fn description_to_str(&self) -> Option<String> {
        self.front
            .description
            .clone()
            .or_else(|| {
                self.attributes.get("excerpt").and_then(|excerpt| {
                    if excerpt.is_nil() {
                        None
                    } else {
                        Some(excerpt.render().to_string())
                    }
                })
            })
            .or_else(|| {
                self.attributes
                    .get("content")
                    .map(|s| s.render().to_string())
            })
    }

    /// Renders liquid templates into HTML in the context of current document.
    ///
    /// Takes `content` string and returns rendered HTML. This function doesn't
    /// take `"extends"` attribute into account. This function can be used for
    /// rendering content or excerpt.
    fn render_html(&self, content: &str, context: &RenderContex) -> Result<String> {
        let html = if self.front.templated {
            let template = context.parser.parse(content)?;
            template.render(context.globals)?
        } else {
            content.to_owned()
        };

        let html = match self.front.format {
            cobalt_model::SourceFormat::Raw => html,
            cobalt_model::SourceFormat::Markdown => context.markdown.parse(&html)?,
            cobalt_model::SourceFormat::Vimwiki => context.vimwiki.parse(&html)?,
        };

        Ok(html)
    }

    /// Renders the excerpt and adds it to attributes of the document.
    ///
    /// The excerpt is either taken from the `excerpt` frontmatter setting, if
    /// given, or extracted from the content, if `excerpt_separator` is not
    /// empty. When neither condition applies, the excerpt is set to the `Nil`
    /// value.
    pub fn render_excerpt(&mut self, context: &RenderContex) -> Result<()> {
        let value = if let Some(excerpt_str) = self.front.excerpt.as_ref() {
            let excerpt = self.render_html(excerpt_str, context)?;
            Value::scalar(excerpt)
        } else if self.front.excerpt_separator.is_empty() {
            Value::Nil
        } else {
            let excerpt = extract_excerpt(
                &self.content,
                self.front.format,
                &self.front.excerpt_separator,
            );
            let excerpt = self.render_html(&excerpt, context)?;
            Value::scalar(excerpt)
        };

        self.attributes.insert("excerpt".into(), value);
        Ok(())
    }

    /// Renders the content and adds it to attributes of the document.
    ///
    /// When we say "content" we mean only this document without extended layout.
    pub fn render_content(&mut self, context: &RenderContex) -> Result<()> {
        let content_html = self.render_html(&self.content, context)?;
        self.attributes
            .insert("content".into(), Value::scalar(content_html));
        Ok(())
    }

    /// Renders the document to an HTML string.
    ///
    /// Side effects:
    ///
    /// * layout may be inserted to layouts cache
    pub fn render(
        &mut self,
        context: &RenderContex,
        layouts: &HashMap<String, String>,
    ) -> Result<String> {
        if let Some(ref layout) = self.front.layout {
            let layout_data_ref = layouts.get(layout).ok_or_else(|| {
                failure::format_err!(
                    "Layout {} does not exist (referenced in {}).",
                    layout,
                    self.file_path.display()
                )
            })?;

            let template = context
                .parser
                .parse(layout_data_ref)
                .with_context(|_| failure::format_err!("Failed to parse layout {:?}", layout))?;
            let content_html = template
                .render(context.globals)
                .with_context(|_| failure::format_err!("Failed to render layout {:?}", layout))?;
            let content_html = minify_if_enabled(content_html, context, &self.file_path)?;
            Ok(content_html)
        } else {
            let path = &[
                kstring::KStringCow::from_static("page").into(),
                kstring::KStringCow::from_static("content").into(),
            ];
            let content_html = liquid::model::try_find(context.globals, path)
                .ok_or_else(|| failure::err_msg("Internal error: page isn't in globals"))?
                .render()
                .to_string();

            let content_html = minify_if_enabled(content_html, context, &self.file_path)?;
            Ok(content_html)
        }
    }
}

fn extract_excerpt_raw(content: &str, excerpt_separator: &str) -> String {
    content
        .split(excerpt_separator)
        .next()
        .unwrap_or(content)
        .to_owned()
}

fn extract_excerpt_markdown(content: &str, excerpt_separator: &str) -> String {
    lazy_static! {
        static ref MARKDOWN_REF: Regex = Regex::new(r"(?m:^ {0,3}\[[^\]]+\]:.+$)").unwrap();
    }

    let mut trail = String::new();

    if MARKDOWN_REF.is_match(content) {
        for mat in MARKDOWN_REF.find_iter(content) {
            trail.push_str(mat.as_str());
            trail.push('\n');
        }
    }
    trail + content.split(excerpt_separator).next().unwrap_or(content)
}

fn extract_excerpt(
    content: &str,
    format: cobalt_model::SourceFormat,
    excerpt_separator: &str,
) -> String {
    match format {
        cobalt_model::SourceFormat::Markdown => {
            extract_excerpt_markdown(content, excerpt_separator)
        }
        cobalt_model::SourceFormat::Vimwiki | cobalt_model::SourceFormat::Raw => {
            extract_excerpt_raw(content, excerpt_separator)
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn format_path_variable_file() {
        let input = Path::new("/hello/world/file.liquid");
        let actual = format_path_variable(input);
        assert_eq!(actual, "hello/world");
    }

    #[test]
    fn format_path_variable_relative() {
        let input = Path::new("hello/world/file.liquid");
        let actual = format_path_variable(input);
        assert_eq!(actual, "hello/world");

        let input = Path::new("./hello/world/file.liquid");
        let actual = format_path_variable(input);
        assert_eq!(actual, "hello/world");
    }
}
<|endoftext|>


<commit_before>//! CSS angle values.

use std::f64::consts::*;

use cssparser::{Parser, Token};

use crate::error::*;
use crate::parsers::{finite_f32, Parse};

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Angle(f64);

impl Angle {
    pub fn new(rad: f64) -> Angle {
        Angle(Angle::normalize(rad))
    }

    pub fn from_degrees(deg: f64) -> Angle {
        Angle(Angle::normalize(deg.to_radians()))
    }

    pub fn from_vector(vx: f64, vy: f64) -> Angle {
        let rad = vy.atan2(vx);

        if rad.is_nan() {
            Angle(0.0)
        } else {
            Angle(Angle::normalize(rad))
        }
    }

    pub fn radians(self) -> f64 {
        self.0
    }

    pub fn bisect(self, other: Angle) -> Angle {
        let half_delta = (other.0 - self.0) * 0.5;

        if FRAC_PI_2 < half_delta.abs() {
            Angle(Angle::normalize(self.0 + half_delta - PI))
        } else {
            Angle(Angle::normalize(self.0 + half_delta))
        }
    }

    // Normalizes an angle to [0.0, 2*PI)
    fn normalize(rad: f64) -> f64 {
        let res = rad % (PI * 2.0);
        if res.abs() < std::f64::EPSILON {
            res.abs()
        } else if res < 0.0 {
            res + PI * 2.0
        } else {
            res
        }
    }
}

// angle:
// https://www.w3.org/TR/SVG/types.html#DataTypeAngle
//
// angle ::= number ("deg" | "grad" | "rad")?
//
impl Parse for Angle {
    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Angle, ParseError<'i>> {
        let angle = {
            let loc = parser.current_source_location();

            let token = parser.next()?;

            match *token {
                Token::Number { value, .. } => {
                    let degrees = finite_f32(value).map_err(|e| loc.new_custom_error(e))?;
                    Angle::from_degrees(f64::from(degrees))
                }

                Token::Dimension {
                    value, ref unit, ..
                } => {
                    let value = f64::from(finite_f32(value).map_err(|e| loc.new_custom_error(e))?);

                    match unit.as_ref() {
                        "deg" => Angle::from_degrees(value),
                        "grad" => Angle::from_degrees(value * 360.0 / 400.0),
                        "rad" => Angle::new(value),
                        _ => {
                            return Err(loc.new_unexpected_token_error(token.clone()));
                        }
                    }
                }

                _ => return Err(loc.new_unexpected_token_error(token.clone())),
            }
        };

        parser.expect_exhausted()?;

        Ok(angle)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use float_cmp::ApproxEq;
    use std::f64;

    #[test]
    fn parses_angle() {
        assert_eq!(Angle::parse_str("0"), Ok(Angle::new(0.0)));
        assert_eq!(Angle::parse_str("15"), Ok(Angle::from_degrees(15.0)));
        assert_eq!(Angle::parse_str("180.5deg"), Ok(Angle::from_degrees(180.5)));
        assert_eq!(Angle::parse_str("1rad"), Ok(Angle::new(1.0)));
        assert_eq!(
            Angle::parse_str("-400grad"),
            Ok(Angle::from_degrees(-360.0))
        );

        assert!(Angle::parse_str("").is_err());
        assert!(Angle::parse_str("foo").is_err());
        assert!(Angle::parse_str("300foo").is_err());
    }

    fn test_bisection_angle(
        expected: f64,
        incoming_vx: f64,
        incoming_vy: f64,
        outgoing_vx: f64,
        outgoing_vy: f64,
    ) {
        let i = Angle::from_vector(incoming_vx, incoming_vy);
        let o = Angle::from_vector(outgoing_vx, outgoing_vy);
        let bisected = i.bisect(o);
        assert!(expected.approx_eq(bisected.radians(), (2.0 * PI * f64::EPSILON, 1)));
    }

    #[test]
    fn bisection_angle_is_correct_from_incoming_counterclockwise_to_outgoing() {
        // 1st quadrant
        test_bisection_angle(FRAC_PI_4, 1.0, 0.0, 0.0, 1.0);

        // 2nd quadrant
        test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, 0.0, 1.0, -1.0, 0.0);

        // 3rd quadrant
        test_bisection_angle(PI + FRAC_PI_4, -1.0, 0.0, 0.0, -1.0);

        // 4th quadrant
        test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 0.0, -1.0, 1.0, 0.0);
    }

    #[test]
    fn bisection_angle_is_correct_from_incoming_clockwise_to_outgoing() {
        // 1st quadrant
        test_bisection_angle(FRAC_PI_4, 0.0, 1.0, 1.0, 0.0);

        // 2nd quadrant
        test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, -1.0, 0.0, 0.0, 1.0);

        // 3rd quadrant
        test_bisection_angle(PI + FRAC_PI_4, 0.0, -1.0, -1.0, 0.0);

        // 4th quadrant
        test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 1.0, 0.0, 0.0, -1.0);
    }

    #[test]
    fn bisection_angle_is_correct_for_more_than_quarter_turn_angle() {
        test_bisection_angle(0.0, 0.1, -1.0, 0.1, 1.0);

        test_bisection_angle(FRAC_PI_2, 1.0, 0.1, -1.0, 0.1);

        test_bisection_angle(PI, -0.1, 1.0, -0.1, -1.0);

        test_bisection_angle(PI + FRAC_PI_2, -1.0, -0.1, 1.0, -0.1);
    }
}
<commit_msg>Angle.parse: don't expect_exhausted<commit_after>//! CSS angle values.

use std::f64::consts::*;

use cssparser::{Parser, Token};

use crate::error::*;
use crate::parsers::{finite_f32, Parse};

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Angle(f64);

impl Angle {
    pub fn new(rad: f64) -> Angle {
        Angle(Angle::normalize(rad))
    }

    pub fn from_degrees(deg: f64) -> Angle {
        Angle(Angle::normalize(deg.to_radians()))
    }

    pub fn from_vector(vx: f64, vy: f64) -> Angle {
        let rad = vy.atan2(vx);

        if rad.is_nan() {
            Angle(0.0)
        } else {
            Angle(Angle::normalize(rad))
        }
    }

    pub fn radians(self) -> f64 {
        self.0
    }

    pub fn bisect(self, other: Angle) -> Angle {
        let half_delta = (other.0 - self.0) * 0.5;

        if FRAC_PI_2 < half_delta.abs() {
            Angle(Angle::normalize(self.0 + half_delta - PI))
        } else {
            Angle(Angle::normalize(self.0 + half_delta))
        }
    }

    // Normalizes an angle to [0.0, 2*PI)
    fn normalize(rad: f64) -> f64 {
        let res = rad % (PI * 2.0);
        if res.abs() < std::f64::EPSILON {
            res.abs()
        } else if res < 0.0 {
            res + PI * 2.0
        } else {
            res
        }
    }
}

// angle:
// https://www.w3.org/TR/SVG/types.html#DataTypeAngle
//
// angle ::= number ("deg" | "grad" | "rad")?
//
impl Parse for Angle {
    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Angle, ParseError<'i>> {
        let angle = {
            let loc = parser.current_source_location();

            let token = parser.next()?;

            match *token {
                Token::Number { value, .. } => {
                    let degrees = finite_f32(value).map_err(|e| loc.new_custom_error(e))?;
                    Angle::from_degrees(f64::from(degrees))
                }

                Token::Dimension {
                    value, ref unit, ..
                } => {
                    let value = f64::from(finite_f32(value).map_err(|e| loc.new_custom_error(e))?);

                    match unit.as_ref() {
                        "deg" => Angle::from_degrees(value),
                        "grad" => Angle::from_degrees(value * 360.0 / 400.0),
                        "rad" => Angle::new(value),
                        _ => {
                            return Err(loc.new_unexpected_token_error(token.clone()));
                        }
                    }
                }

                _ => return Err(loc.new_unexpected_token_error(token.clone())),
            }
        };

        Ok(angle)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use float_cmp::ApproxEq;
    use std::f64;

    #[test]
    fn parses_angle() {
        assert_eq!(Angle::parse_str("0"), Ok(Angle::new(0.0)));
        assert_eq!(Angle::parse_str("15"), Ok(Angle::from_degrees(15.0)));
        assert_eq!(Angle::parse_str("180.5deg"), Ok(Angle::from_degrees(180.5)));
        assert_eq!(Angle::parse_str("1rad"), Ok(Angle::new(1.0)));
        assert_eq!(
            Angle::parse_str("-400grad"),
            Ok(Angle::from_degrees(-360.0))
        );

        assert!(Angle::parse_str("").is_err());
        assert!(Angle::parse_str("foo").is_err());
        assert!(Angle::parse_str("300foo").is_err());
    }

    fn test_bisection_angle(
        expected: f64,
        incoming_vx: f64,
        incoming_vy: f64,
        outgoing_vx: f64,
        outgoing_vy: f64,
    ) {
        let i = Angle::from_vector(incoming_vx, incoming_vy);
        let o = Angle::from_vector(outgoing_vx, outgoing_vy);
        let bisected = i.bisect(o);
        assert!(expected.approx_eq(bisected.radians(), (2.0 * PI * f64::EPSILON, 1)));
    }

    #[test]
    fn bisection_angle_is_correct_from_incoming_counterclockwise_to_outgoing() {
        // 1st quadrant
        test_bisection_angle(FRAC_PI_4, 1.0, 0.0, 0.0, 1.0);

        // 2nd quadrant
        test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, 0.0, 1.0, -1.0, 0.0);

        // 3rd quadrant
        test_bisection_angle(PI + FRAC_PI_4, -1.0, 0.0, 0.0, -1.0);

        // 4th quadrant
        test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 0.0, -1.0, 1.0, 0.0);
    }

    #[test]
    fn bisection_angle_is_correct_from_incoming_clockwise_to_outgoing() {
        // 1st quadrant
        test_bisection_angle(FRAC_PI_4, 0.0, 1.0, 1.0, 0.0);

        // 2nd quadrant
        test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, -1.0, 0.0, 0.0, 1.0);

        // 3rd quadrant
        test_bisection_angle(PI + FRAC_PI_4, 0.0, -1.0, -1.0, 0.0);

        // 4th quadrant
        test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 1.0, 0.0, 0.0, -1.0);
    }

    #[test]
    fn bisection_angle_is_correct_for_more_than_quarter_turn_angle() {
        test_bisection_angle(0.0, 0.1, -1.0, 0.1, 1.0);

        test_bisection_angle(FRAC_PI_2, 1.0, 0.1, -1.0, 0.1);

        test_bisection_angle(PI, -0.1, 1.0, -0.1, -1.0);

        test_bisection_angle(PI + FRAC_PI_2, -1.0, -0.1, 1.0, -0.1);
    }
}
<|endoftext|>


<commit_before>//! Helper to crudely rewrite types from the spec's format to our own.
//!
//! This reads a file containing table and record descriptions (from the spec)
//! and converts them to the form that we use, writing the result to stdout.
//!
//! Input should be in the format:
//!
//!
//! ```
//! Gpos1_0
//! uint16      majorVersion       Major version of the GPOS table, = 1
//! uint16      minorVersion       Minor version of the GPOS table, = 0
//! Offset16    scriptListOffset   Offset to ScriptList table, from beginning of GPOS table
//! Offset16    featureListOffset  Offset to FeatureList table, from beginning of GPOS table
//! Offset16    lookupListOffset   Offset to LookupList table, from beginning of GPOS table
//!
//! Gpos1_1
//! uint16      majorVersion            Major version of the GPOS table, = 1
//! uint16      minorVersion            Minor version of the GPOS table, = 1
//! Offset16    scriptListOffset        Offset to ScriptList table, from beginning of GPOS table
//! Offset16    featureListOffset       Offset to FeatureList table, from beginning of GPOS table
//! Offset16    lookupListOffset        Offset to LookupList table, from beginning of GPOS table
//! Offset32    featureVariationsOffset Offset to FeatureVariations table, from beginning of GPOS table (may be NULL)
//! ```
//!
//! - different records/tables are separated by newlines.
//! - the first line should be a single word, used as the name of the type
//! - other lines are just copy pasted
//!
//! *limitations:* this doesn't handle lifetimes, and doesn't generate annotations.
//! You will need to clean up the output.

use std::fmt::Write;

macro_rules! exit_with_msg {
    ($disp:expr) => {{
        eprintln!("ERROR: {}", $disp);
        std::process::exit(1);
    }};
}
fn main() {
    let in_path = std::env::args().nth(1).expect("expected path argument");
    let input = std::fs::read_to_string(in_path).expect("failed to read path");
    let mut lines = input.lines().map(str::trim).filter(|l| !l.starts_with('#'));
    while let Some(item) = generate_one_item(&mut lines) {
        println!("{}", item);
    }
}

/// parse a single table or record.
///
/// Returns `Some` on success, `None` if there are no more items, and terminates
/// if something goes wrong.
fn generate_one_item<'a>(lines: impl Iterator<Item = &'a str>) -> Option<String> {
    let mut lines = lines.skip_while(|line| line.is_empty());

    let name = match lines.next() {
        Some(line) if line.split_whitespace().nth(1).is_none() => line,
        Some(line) => exit_with_msg!(format!("expected table or record name, found '{}'", line)),
        _ => return None,
    };

    let fields = lines.map_while(parse_field).collect::<Vec<_>>();
    let lifetime_str = if fields.iter().any(|x| x.maybe_count.is_some()) {
        "<'a>"
    } else {
        ""
    };
    let mut result = format!("{}{} {{\n", name, lifetime_str);
    for line in &fields {
        writeln!(&mut result, "{}", line).unwrap();
    }
    result.push_str("}\n");
    Some(result)
}

struct Field<'a> {
    name: String,
    maybe_count: Option<String>,
    typ: &'a str,
    comment: &'a str,
}

impl<'a> std::fmt::Display for Field<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        format_comment(f, "    ", self.comment)?;
        if self.name.contains("reserved") {
            writeln!(f, "    #[hidden]")?;
        }
        if let Some(count) = &self.maybe_count {
            writeln!(f, "    #[count({})]", count)?;
            write!(f, "    {}: [{}],", self.name, self.typ)?;
        } else {
            write!(f, "    {}: {},", self.name, self.typ)?;
        }
        Ok(())
    }
}

fn parse_field(line: &str) -> Option<Field> {
    if line.is_empty() {
        return None;
    }
    let mut iter = line.splitn(3, |c: char| c.is_ascii_whitespace());
    let (typ, ident, comment) = match (iter.next(), iter.next(), iter.next()) {
        (Some(a), Some(b), Some(c)) => (a, b, c),
        _ => exit_with_msg!(format!(
            "line could not be parsed as type/name/comment: '{}'",
            line
        )),
    };
    let typ = normalize_type(typ);
    let (name, maybe_count) = split_ident(ident);
    let name = decamalize(name);
    let maybe_count = maybe_count.map(decamalize);
    Some(Field {
        name,
        maybe_count,
        typ,
        comment,
    })
}

/// takes an ident and splits it into the name and an optional count (if the item
/// is an array)
fn split_ident(input: &str) -> (&str, Option<&str>) {
    match input.split_once('[') {
        Some((front, back)) => (front, Some(back.trim_end_matches(']'))),
        None => (input, None),
    }
}

fn normalize_type(input: &str) -> &str {
    match input {
        "uint8" => "Uint8",
        "uint16" => "Uint16",
        "uint24" => "Uint24",
        "uint32" => "Uint32",
        "int8" => "Int8",
        "int16" => "Int16",
        "int32" => "Int32",
        "FWORD" => "FWord",
        "UFWORD" => "UfWord",
        "F2DOT14" => "F2Dot14",
        "LONGDATETIME" => "LongDateTime",
        other => other,
    }
}

fn decamalize(input: &str) -> String {
    //taken from serde: https://github.com/serde-rs/serde/blob/7e19ae8c9486a3bbbe51f1befb05edee94c454f9/serde_derive/src/internals/case.rs#L69-L76
    let mut snake = String::new();
    for (i, ch) in input.char_indices() {
        if i > 0 && ch.is_uppercase() {
            snake.push('_');
        }
        snake.push(ch.to_ascii_lowercase());
    }
    snake
}

fn format_comment(
    f: &mut std::fmt::Formatter<'_>,
    whitespace: &str,
    input: &str,
) -> std::fmt::Result {
    const LINE_LEN: usize = 72;

    let mut cur_len = 0;

    for token in input.split_inclusive(' ') {
        if cur_len == 0 || cur_len + token.len() > LINE_LEN {
            if cur_len > 0 {
                writeln!(f)?;
            }
            write!(f, "{}/// ", whitespace)?;
            cur_len = whitespace.len() + 4;
        }
        write!(f, "{}", token)?;
        cur_len += token.len();
    }
    if cur_len > 0 {
        writeln!(f)?;
    }
    Ok(())
}
<commit_msg>Generate macro block in codegen<commit_after>//! Helper to crudely rewrite types from the spec's format to our own.
//!
//! This reads a file containing table and record descriptions (from the spec)
//! and converts them to the form that we use, writing the result to stdout.
//!
//! Input should be in the format:
//!
//!
//! ```
//! Gpos1_0
//! uint16      majorVersion       Major version of the GPOS table, = 1
//! uint16      minorVersion       Minor version of the GPOS table, = 0
//! Offset16    scriptListOffset   Offset to ScriptList table, from beginning of GPOS table
//! Offset16    featureListOffset  Offset to FeatureList table, from beginning of GPOS table
//! Offset16    lookupListOffset   Offset to LookupList table, from beginning of GPOS table
//!
//! Gpos1_1
//! uint16      majorVersion            Major version of the GPOS table, = 1
//! uint16      minorVersion            Minor version of the GPOS table, = 1
//! Offset16    scriptListOffset        Offset to ScriptList table, from beginning of GPOS table
//! Offset16    featureListOffset       Offset to FeatureList table, from beginning of GPOS table
//! Offset16    lookupListOffset        Offset to LookupList table, from beginning of GPOS table
//! Offset32    featureVariationsOffset Offset to FeatureVariations table, from beginning of GPOS table (may be NULL)
//! ```
//!
//! - different records/tables are separated by newlines.
//! - the first line should be a single word, used as the name of the type
//! - other lines are just copy pasted
//!
//! *limitations:* this doesn't handle lifetimes, and doesn't generate annotations.
//! You will need to clean up the output.

use std::fmt::Write;

macro_rules! exit_with_msg {
    ($disp:expr) => {{
        eprintln!("ERROR: {}", $disp);
        std::process::exit(1);
    }};
}

static MACRO_CALL: &str = "toy_table_macro::tables!";

fn main() {
    let in_path = std::env::args().nth(1).expect("expected path argument");
    let input = std::fs::read_to_string(in_path).expect("failed to read path");
    let mut lines = input.lines().map(str::trim).filter(|l| !l.starts_with('#'));
    println!("{} {{", MACRO_CALL);
    while let Some(item) = generate_one_item(&mut lines) {
        println!("{}", item);
    }
    println!("}}");
}

/// parse a single table or record.
///
/// Returns `Some` on success, `None` if there are no more items, and terminates
/// if something goes wrong.
fn generate_one_item<'a>(lines: impl Iterator<Item = &'a str>) -> Option<String> {
    let mut lines = lines.skip_while(|line| line.is_empty());

    let name = match lines.next() {
        Some(line) if line.split_whitespace().nth(1).is_none() => line,
        Some(line) => exit_with_msg!(format!("expected table or record name, found '{}'", line)),
        _ => return None,
    };

    let fields = lines.map_while(parse_field).collect::<Vec<_>>();
    let lifetime_str = if fields.iter().any(|x| x.maybe_count.is_some()) {
        "<'a>"
    } else {
        ""
    };
    let mut result = format!("{}{} {{\n", name, lifetime_str);
    for line in &fields {
        writeln!(&mut result, "{}", line).unwrap();
    }
    result.push_str("}\n");
    Some(result)
}

struct Field<'a> {
    name: String,
    maybe_count: Option<String>,
    typ: &'a str,
    comment: &'a str,
}

impl<'a> std::fmt::Display for Field<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        format_comment(f, "    ", self.comment)?;
        if self.name.contains("reserved") {
            writeln!(f, "    #[hidden]")?;
        }
        if let Some(count) = &self.maybe_count {
            writeln!(f, "    #[count({})]", count)?;
            write!(f, "    {}: [{}],", self.name, self.typ)?;
        } else {
            write!(f, "    {}: {},", self.name, self.typ)?;
        }
        Ok(())
    }
}

fn parse_field(line: &str) -> Option<Field> {
    if line.is_empty() {
        return None;
    }
    let mut iter = line.splitn(3, |c: char| c.is_ascii_whitespace());
    let (typ, ident, comment) = match (iter.next(), iter.next(), iter.next()) {
        (Some(a), Some(b), Some(c)) => (a, b, c),
        _ => exit_with_msg!(format!(
            "line could not be parsed as type/name/comment: '{}'",
            line
        )),
    };
    let typ = normalize_type(typ);
    let (name, maybe_count) = split_ident(ident);
    let name = decamalize(name);
    let maybe_count = maybe_count.map(decamalize);
    Some(Field {
        name,
        maybe_count,
        typ,
        comment,
    })
}

/// takes an ident and splits it into the name and an optional count (if the item
/// is an array)
fn split_ident(input: &str) -> (&str, Option<&str>) {
    match input.split_once('[') {
        Some((front, back)) => (front, Some(back.trim_end_matches(']'))),
        None => (input, None),
    }
}

fn normalize_type(input: &str) -> &str {
    match input {
        "uint8" => "Uint8",
        "uint16" => "Uint16",
        "uint24" => "Uint24",
        "uint32" => "Uint32",
        "int8" => "Int8",
        "int16" => "Int16",
        "int32" => "Int32",
        "FWORD" => "FWord",
        "UFWORD" => "UfWord",
        "F2DOT14" => "F2Dot14",
        "LONGDATETIME" => "LongDateTime",
        other => other,
    }
}

fn decamalize(input: &str) -> String {
    //taken from serde: https://github.com/serde-rs/serde/blob/7e19ae8c9486a3bbbe51f1befb05edee94c454f9/serde_derive/src/internals/case.rs#L69-L76
    let mut snake = String::new();
    for (i, ch) in input.char_indices() {
        if i > 0 && ch.is_uppercase() {
            snake.push('_');
        }
        snake.push(ch.to_ascii_lowercase());
    }
    snake
}

fn format_comment(
    f: &mut std::fmt::Formatter<'_>,
    whitespace: &str,
    input: &str,
) -> std::fmt::Result {
    const LINE_LEN: usize = 72;

    let mut cur_len = 0;

    for token in input.split_inclusive(' ') {
        if cur_len == 0 || cur_len + token.len() > LINE_LEN {
            if cur_len > 0 {
                writeln!(f)?;
            }
            write!(f, "{}/// ", whitespace)?;
            cur_len = whitespace.len() + 4;
        }
        write!(f, "{}", token)?;
        cur_len += token.len();
    }
    if cur_len > 0 {
        writeln!(f)?;
    }
    Ok(())
}
<|endoftext|>