Merge branch 'feature/config_file'

This commit is contained in:
Kogia-sima 2020-06-15 07:31:34 +09:00
commit 4803792d4c
11 changed files with 369 additions and 62 deletions

10
Cargo.lock generated
View File

@ -1064,6 +1064,7 @@ dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)",
"yaml-rust 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1410,6 +1411,14 @@ dependencies = [
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "yaml-rust"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
"checksum actix-connect 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c"
@ -1562,3 +1571,4 @@ dependencies = [
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
"checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
"checksum yaml-rust 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d"

View File

@ -1,4 +1,6 @@
# Derive Options
# Configuration
## Derive options
You can control the rendering behaviour via `template` attribute.
@ -13,7 +15,7 @@ struct TemplateStruct {
`template` attribute accepts the following options.
- `path`: path to template file. This options is always required.
- `escape`: Enable HTML escaping (default: `false`)
- `escape`: Enable HTML escaping (default: `true`)
- `delimiter`: Replace the '%' character used for the tag delimiter (default: '%')
- `rm_whitespace`: try to strip whitespaces as much as possible without collapsing HTML structure (default: `false`). This option might not work correctly if your templates have inline `script` tag.
@ -28,3 +30,32 @@ struct TemplateStruct {
...
}
```
## Configuration file
Sailfish allows global and local configuration in a file named `sailfish.yml`. Sailfish looks for this file in same directory as `Cargo.toml` and all parent directories.
If, for example, `Cargo.toml` exists in `/foo/bar/baz` directory, then the following configuration files would be scanned in this order.
- `/foo/bar/baz/sailfish.yml`
- `/foo/bar/sailfish.yml`
- `/foo/sailfish.yml`
- `/sailfish.yml`
If a key is specified in multiple configuration files, the value in the deeper directory takes precedence over ancestor directories.
If a key is specified in both configuration file and derive options, then the value specified in the derive options takes precedence over the configuration file.
### Configuration file format
Configuration files are written in the YAML 1.2 format. The default configuration is described below.
```
template_dir: "templates"
escape: true
delimiter: "%"
optimization:
rm_whitespace: false
```
You can specify another template directory in `template_dir` option. Other options are same as derive options.

View File

@ -44,6 +44,6 @@ nav:
- 'Welcome': 'index.md'
- 'Installation': 'installation.md'
- 'Getting Started': 'getting-started.md'
- 'Options': 'options.md'
- 'Configuration': 'options.md'
- 'Syntax':
- 'Overview': 'syntax/overview.md'

View File

@ -1,5 +1,5 @@
1
<% include!("/includes/include-parent.stpl"); %>
<% include!("includes/include-parent.stpl"); %>
4
<% include!("./included.stpl"); %>
5

View File

@ -17,12 +17,14 @@ name = "sailfish_compiler"
doctest = false
[features]
default = []
default = ["config"]
procmacro = []
config = ["yaml-rust"]
[dependencies]
memchr = "2.3.3"
quote = { version = "1.0.6", default-features = false }
yaml-rust = { version = "0.4.4", optional = true }
[dependencies.syn]
version = "1.0.21"

View File

@ -41,7 +41,6 @@ impl Compiler {
pub fn compile_file(
&self,
template_dir: &Path,
input: &Path,
output: &Path,
) -> Result<CompilationReport, Error> {
@ -64,7 +63,7 @@ impl Compiler {
let mut tsource = self.translate_file_contents(input)?;
let mut report = CompilationReport { deps: Vec::new() };
let r = resolver.resolve(template_dir, &*input, &mut tsource.ast)?;
let r = resolver.resolve(&*input, &mut tsource.ast)?;
report.deps = r.deps;
optimizer.optimize(&mut tsource.ast);

View File

@ -4,8 +4,10 @@ use std::path::{Path, PathBuf};
pub struct Config {
pub delimiter: char,
pub escape: bool,
pub cache_dir: PathBuf,
pub rm_whitespace: bool,
pub template_dirs: Vec<PathBuf>,
#[doc(hidden)]
pub cache_dir: PathBuf,
#[doc(hidden)]
pub _non_exhaustive: (),
}
@ -13,6 +15,7 @@ pub struct Config {
impl Default for Config {
fn default() -> Self {
Self {
template_dirs: Vec::new(),
delimiter: '%',
escape: true,
cache_dir: Path::new(env!("OUT_DIR")).join("cache"),
@ -22,4 +25,211 @@ impl Default for Config {
}
}
// TODO: Global configration file
#[cfg(feature = "config")]
mod config {
use std::fs;
use yaml_rust::yaml::{Yaml, YamlLoader};
use super::*;
use crate::error::*;
impl Config {
pub fn search_file_and_read(base: &Path) -> Result<Config, Error> {
// search config file
let mut path = PathBuf::new();
let mut config = Config::default();
for component in base.iter() {
path.push(component);
path.push("sailfish.yml");
if path.is_file() {
let config_file =
ConfigFile::read_from_file(&*path).map_err(|mut e| {
e.source_file = Some(path.to_owned());
e
})?;
if let Some(template_dirs) = config_file.template_dirs {
for template_dir in template_dirs.into_iter().rev() {
if template_dir.is_absolute() {
config.template_dirs.push(template_dir);
} else {
config
.template_dirs
.push(path.parent().unwrap().join(template_dir));
}
}
}
if let Some(delimiter) = config_file.delimiter {
config.delimiter = delimiter;
}
if let Some(escape) = config_file.escape {
config.escape = escape;
}
if let Some(rm_whitespace) = config_file.rm_whitespace {
config.rm_whitespace = rm_whitespace;
}
}
path.pop();
}
Ok(config)
}
}
#[derive(Default)]
struct ConfigFile {
template_dirs: Option<Vec<PathBuf>>,
delimiter: Option<char>,
escape: Option<bool>,
rm_whitespace: Option<bool>,
}
impl ConfigFile {
fn read_from_file(path: &Path) -> Result<Self, Error> {
let mut config = Self::default();
let content = fs::read_to_string(path)
.chain_err(|| format!("Failed to read configuration file {:?}", path))?;
let entries = YamlLoader::load_from_str(&*content)
.map_err(|e| ErrorKind::ConfigError(e.to_string()))?;
drop(content);
for entry in entries {
config.visit_global(entry)?
}
Ok(config)
}
fn visit_global(&mut self, entry: Yaml) -> Result<(), Error> {
let hash = entry.into_hash().ok_or_else(|| {
ErrorKind::ConfigError("Invalid configuration format".to_owned())
})?;
for (k, v) in hash {
match k {
Yaml::String(ref s) => match &**s {
"template_dir" => self.visit_template_dir(v)?,
"delimiter" => self.visit_delimiter(v)?,
"escape" => self.visit_escape(v)?,
"optimization" => self.visit_optimization(v)?,
_ => return Err(Self::error(format!("Unknown key ({})", s))),
},
_ => {
return Err(Self::error("Invalid configuration format"));
}
}
}
Ok(())
}
fn visit_template_dir(&mut self, value: Yaml) -> Result<(), Error> {
if self.template_dirs.is_some() {
return Err(Self::error("Duplicate key (template_dir)"));
}
match value {
Yaml::String(s) => self.template_dirs = Some(vec![PathBuf::from(s)]),
Yaml::Array(v) => {
let mut template_dirs = Vec::new();
for e in v {
if let Yaml::String(s) = e {
template_dirs.push(PathBuf::from(s));
} else {
return Err(Self::error(
"Arguments of `template_dir` must be string",
));
}
}
self.template_dirs = Some(template_dirs);
}
_ => {
return Err(Self::error(
"Arguments of `template_dir` must be string",
));
}
}
Ok(())
}
fn visit_delimiter(&mut self, value: Yaml) -> Result<(), Error> {
if self.delimiter.is_some() {
return Err(Self::error("Duplicate key (delimiter)"));
}
if let Yaml::String(s) = value {
if s.chars().count() == 1 {
self.delimiter = Some(s.chars().next().unwrap());
Ok(())
} else {
Err(Self::error("`escape` must be single character"))
}
} else {
Err(Self::error("`escape` must be single character"))
}
}
fn visit_escape(&mut self, value: Yaml) -> Result<(), Error> {
if self.escape.is_some() {
return Err(Self::error("Duplicate key (escape)"));
}
if let Yaml::Boolean(b) = value {
self.escape = Some(b);
Ok(())
} else {
Err(Self::error("`escape` must be boolean"))
}
}
fn visit_optimization(&mut self, entry: Yaml) -> Result<(), Error> {
let hash = entry.into_hash().ok_or_else(|| {
ErrorKind::ConfigError("Invalid configuration format".to_owned())
})?;
for (k, v) in hash {
match k {
Yaml::String(ref s) => match &**s {
"rm_whitespace" => self.visit_rm_whitespace(v)?,
_ => {
return Err(Self::error(format!(
"Unknown key (optimization.{})",
s
)));
}
},
_ => {
return Err(Self::error("Invalid configuration format"));
}
}
}
Ok(())
}
fn visit_rm_whitespace(&mut self, value: Yaml) -> Result<(), Error> {
if self.rm_whitespace.is_some() {
return Err(Self::error("Duplicate key (rm_whitespace)"));
}
if let Yaml::Boolean(b) = value {
self.rm_whitespace = Some(b);
Ok(())
} else {
Err(Self::error("`rm_whitespace` must be boolean"))
}
}
fn error<T: Into<String>>(msg: T) -> Error {
make_error!(ErrorKind::ConfigError(msg.into()))
}
}
}

View File

@ -10,6 +10,7 @@ pub enum ErrorKind {
FmtError(fmt::Error),
IoError(io::Error),
RustSyntaxError(syn::Error),
ConfigError(String),
ParseError(String),
AnalyzeError(String),
Unimplemented(String),
@ -22,6 +23,7 @@ impl fmt::Display for ErrorKind {
ErrorKind::FmtError(ref e) => e.fmt(f),
ErrorKind::IoError(ref e) => e.fmt(f),
ErrorKind::RustSyntaxError(ref e) => write!(f, "Rust Syntax Error: {}", e),
ErrorKind::ConfigError(ref e) => write!(f, "Invalid configuration: {}", e),
ErrorKind::ParseError(ref msg) => write!(f, "Parse error: {}", msg),
ErrorKind::AnalyzeError(ref msg) => write!(f, "Analyzation error: {}", msg),
ErrorKind::Unimplemented(ref msg) => f.write_str(&**msg),

View File

@ -98,13 +98,7 @@ impl DeriveTemplateOptions {
}
}
fn compile(
template_dir: &Path,
input_file: &Path,
output_file: &Path,
options: &DeriveTemplateOptions,
) -> Result<CompilationReport, Error> {
let mut config = Config::default();
fn merge_config_options(config: &mut Config, options: &DeriveTemplateOptions) {
if let Some(ref delimiter) = options.delimiter {
config.delimiter = delimiter.value();
}
@ -114,9 +108,66 @@ fn compile(
if let Some(ref rm_whitespace) = options.rm_whitespace {
config.rm_whitespace = rm_whitespace.value;
}
}
fn resolve_template_file(path: &str, template_dirs: &[PathBuf]) -> Option<PathBuf> {
for template_dir in template_dirs.iter().rev() {
let p = template_dir.join(path);
if p.is_file() {
return Some(p);
}
}
let mut fallback = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect(
"Internal error: environmental variable `CARGO_MANIFEST_DIR` is not set.",
));
fallback.push("templates");
fallback.push(path);
if fallback.is_file() {
return Some(fallback);
}
None
}
fn filename_hash(path: &Path) -> String {
use std::fmt::Write;
const FNV_PRIME: u64 = 1_099_511_628_211;
const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
let mut hash = String::with_capacity(16);
if let Some(n) = path.file_name() {
let mut filename = &*n.to_string_lossy();
if let Some(p) = filename.find('.') {
filename = &filename[..p];
}
hash.push_str(filename);
hash.push('-');
}
// calculate 64bit hash
let mut h = FNV_OFFSET_BASIS;
for b in (&*path.to_string_lossy()).bytes() {
h = h.wrapping_mul(FNV_PRIME);
h ^= b as u64;
}
// convert 64bit hash into ascii
let _ = write!(hash, "{:016x}", h);
hash
}
fn compile(
input_file: &Path,
output_file: &Path,
config: Config,
) -> Result<CompilationReport, Error> {
let compiler = Compiler::with_config(config);
compiler.compile_file(template_dir, input_file, &*output_file)
compiler.compile_file(input_file, &*output_file)
}
fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error> {
@ -130,13 +181,20 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
}
}
let mut template_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect(
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect(
"Internal error: environmental variable `CARGO_MANIFEST_DIR` is not set.",
));
template_dir.push("templates");
#[cfg(feature = "config")]
let mut config = Config::search_file_and_read(&*manifest_dir)
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
#[cfg(not(feature = "config"))]
let mut config = Config::default();
if env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") {
template_dir = template_dir
let template_dir = env::current_dir()
.unwrap()
.ancestors()
.find(|p| p.join("LICENSE").exists())
.unwrap()
@ -144,40 +202,30 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
.join("tests")
.join("fails")
.join("templates");
config.template_dirs.push(template_dir);
}
let input_file = match all_options.path {
Some(ref path) => template_dir.join(path.value()),
None => {
return Err(syn::Error::new(
Span::call_site(),
"`path` option must be specified.",
)
.into())
}
let input_file = {
let path = all_options.path.as_ref().ok_or_else(|| {
syn::Error::new(Span::call_site(), "`path` option must be specified.")
})?;
resolve_template_file(&*path.value(), &*config.template_dirs).ok_or_else(
|| {
syn::Error::new(
path.span(),
format!("Template file {:?} not found", path.value()),
)
},
)?
};
let filename = match input_file.file_name() {
Some(f) => f,
None => {
return Err(syn::Error::new(
Span::call_site(),
format!("Invalid file name: {:?}", input_file),
))
}
};
let out_dir =
if std::env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") {
template_dir.parent().unwrap().join("out")
} else {
PathBuf::from(env!("OUT_DIR"))
};
let out_dir = PathBuf::from(env!("OUT_DIR"));
let mut output_file = out_dir.clone();
output_file.push("templates");
output_file.push(filename);
output_file.push(filename_hash(&*input_file));
let report = compile(&*template_dir, &*input_file, &*output_file, &all_options)
merge_config_options(&mut config, &all_options);
let report = compile(&*input_file, &*output_file, config)
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
let input_file_string = input_file.to_string_lossy();

View File

@ -27,15 +27,14 @@ pub struct ResolveReport {
pub deps: Vec<PathBuf>,
}
struct ResolverImpl<'s, 'h> {
template_dir: &'s Path,
struct ResolverImpl<'h> {
path_stack: Vec<PathBuf>,
deps: Vec<PathBuf>,
error: Option<Error>,
include_handler: Arc<dyn 'h + Fn(&Path) -> Result<Block, Error>>,
}
impl<'s, 'h> ResolverImpl<'s, 'h> {
impl<'h> ResolverImpl<'h> {
fn resolve_include(&mut self, i: &ExprMacro) -> Result<Expr, Error> {
let arg = match syn::parse2::<LitStr>(i.mac.tokens.clone()) {
Ok(l) => l.value(),
@ -50,8 +49,8 @@ impl<'s, 'h> ResolverImpl<'s, 'h> {
// resolve include! for rust file
if arg.ends_with(".rs") {
let absolute_path = if arg.starts_with('/') {
self.template_dir.join(&arg[1..])
let absolute_path = if Path::new(&*arg).is_absolute() {
PathBuf::from(&arg[1..])
} else {
self.path_stack.last().unwrap().parent().unwrap().join(arg)
};
@ -61,9 +60,9 @@ impl<'s, 'h> ResolverImpl<'s, 'h> {
// resolve the template file path
// TODO: How should arguments be interpreted on Windows?
let child_template_file = if arg.starts_with('/') {
let child_template_file = if Path::new(&*arg).is_absolute() {
// absolute imclude
self.template_dir.join(&arg[1..])
PathBuf::from(&arg[1..])
} else {
// relative include
self.path_stack
@ -95,7 +94,7 @@ impl<'s, 'h> ResolverImpl<'s, 'h> {
}
}
impl<'s, 'h> VisitMut for ResolverImpl<'s, 'h> {
impl<'h> VisitMut for ResolverImpl<'h> {
fn visit_expr_mut(&mut self, i: &mut Expr) {
return_if_some!(self.error);
let em = matches_or_else!(*i, Expr::Macro(ref mut em), em, {
@ -134,23 +133,20 @@ impl<'h> Resolver<'h> {
#[inline]
pub fn include_handler(
self,
mut self,
new: Arc<dyn 'h + Fn(&Path) -> Result<Block, Error>>,
) -> Resolver<'h> {
Self {
include_handler: new,
}
self.include_handler = new;
self
}
#[inline]
pub fn resolve(
&self,
template_dir: &Path,
input_file: &Path,
ast: &mut Block,
) -> Result<ResolveReport, Error> {
let mut child = ResolverImpl {
template_dir,
path_stack: vec![input_file.to_owned()],
deps: Vec::new(),
error: None,

View File

@ -20,6 +20,15 @@ proc-macro = true
test = false
doctest = false
[features]
default = ["config"]
config = ["sailfish-compiler/config"]
[dependencies]
sailfish-compiler = { path = "../sailfish-compiler", version = "0.0.5", features = ["procmacro"] }
proc-macro2 = "1.0.17"
[dependencies.sailfish-compiler]
path = "../sailfish-compiler"
version = "0.0.5"
default-features = false
features = ["procmacro"]