Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract parser #748

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c2b55e3
Extract askama_parser from askama_derive
couchand Nov 5, 2022
52f962a
wip cleanup public api of askama_parser
couchand Nov 5, 2022
7374f83
clippy
couchand Nov 5, 2022
6a4be68
fmt
couchand Nov 5, 2022
9dc3c3b
clippy
couchand Nov 5, 2022
a38e713
config docs
couchand Nov 5, 2022
0a39676
remove lifetime param on config
couchand Nov 5, 2022
cb22f0b
encapsulate config file loading
couchand Nov 5, 2022
53a7e37
mod config docs
couchand Nov 5, 2022
4bacb18
unpub that fn
couchand Nov 5, 2022
0e3d8c4
fix doctest
couchand Nov 5, 2022
931b3e7
fix features
couchand Nov 5, 2022
42c171c
input docs
couchand Nov 5, 2022
96fdc54
compileerror docs
couchand Nov 5, 2022
e39c4eb
parser::node docs
couchand Nov 5, 2022
98ab5b9
docs parser
couchand Nov 5, 2022
827c481
more parser docs
couchand Nov 6, 2022
ffa8bf1
include comment text in node
couchand Nov 6, 2022
5c12c43
fmt initial dev
couchand Nov 6, 2022
0d4dc57
pass syntax into fmt
couchand Nov 6, 2022
4aa7f82
fmt if block, pretend we have pretty formatting
couchand Nov 6, 2022
deb198f
match fmt
couchand Nov 7, 2022
91170e3
fmt for loop
couchand Nov 7, 2022
0395f8e
fmt break and continue
couchand Nov 7, 2022
cc07214
refactor fmt block tags
couchand Nov 7, 2022
0d9a805
extract structured helper
couchand Nov 23, 2022
1d05299
fmt call
couchand Nov 23, 2022
d643bf2
fmt extends
couchand Nov 23, 2022
c50bd27
fmt blockdef
couchand Nov 23, 2022
3ee4187
fmt include
couchand Nov 23, 2022
6fbfb9b
parse extends as strlit
couchand Nov 23, 2022
3146809
test fixes
couchand Nov 23, 2022
f2df418
more fixes
couchand Nov 23, 2022
08ee164
fmt import
couchand Nov 23, 2022
e945935
fmt macro
couchand Nov 23, 2022
cd26092
fmt raw
couchand Nov 23, 2022
ec1ab7c
fmt exprs
couchand Nov 23, 2022
0ec6eeb
all targets
couchand Nov 23, 2022
aec4c53
fmt infallible
couchand Nov 23, 2022
aa7f3d1
fmt fmt
couchand Nov 23, 2022
6bf2c96
todos resolved
couchand Nov 23, 2022
46a62e1
gen fixes
couchand Nov 23, 2022
fb35a54
remove vestigial tests
couchand Nov 23, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ members = [
"askama",
"askama_actix",
"askama_axum",
"askama_gotham",
"askama_derive",
"askama_escape",
"askama_fmt",
"askama_gotham",
"askama_mendes",
"askama_parser",
"askama_rocket",
"askama_tide",
"askama_warp",
Expand Down
3 changes: 2 additions & 1 deletion askama_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ edition = "2018"
proc-macro = true

[features]
config = ["serde", "toml"]
config = ["serde", "toml", "askama_parser/config"]
humansize = []
markdown = []
urlencode = []
Expand All @@ -30,6 +30,7 @@ with-tide = []
with-warp = []

[dependencies]
askama_parser = { version = "0.1.0", path = "../askama_parser" }
mime = "0.3"
mime_guess = "2"
nom = "7"
Expand Down
177 changes: 10 additions & 167 deletions askama_derive/src/generator.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
use crate::config::{get_template_source, read_config_file, Config, WhitespaceHandling};
use crate::config::{get_template_source, Config, WhitespaceHandling};
use crate::heritage::{Context, Heritage};
use crate::input::{Print, Source, TemplateInput};
use crate::parser::{parse, Cond, CondTest, Expr, Loop, Node, Target, When, Whitespace, Ws};
use crate::CompileError;

use askama_parser::generator::{find_used_templates, TemplateArgs};
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use quote::quote;

use std::collections::hash_map::{Entry, HashMap};
use std::path::{Path, PathBuf};
use std::path::Path;
use std::{cmp, hash, mem, str};

/// The actual implementation for askama_derive::Template
pub(crate) fn derive_template(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
match build_template(&ast) {
Ok(source) => source.parse().unwrap(),
Err(e) => e.into_compile_error(),
Err(e) => e.into_compile_error().into(),
}
}

Expand All @@ -29,8 +30,7 @@ pub(crate) fn derive_template(input: TokenStream) -> TokenStream {
/// value as passed to the `template()` attribute.
fn build_template(ast: &syn::DeriveInput) -> Result<String, CompileError> {
let template_args = TemplateArgs::new(ast)?;
let config_toml = read_config_file(template_args.config_path.as_deref())?;
let config = Config::new(&config_toml)?;
let config = Config::from_file(template_args.config_path.as_deref())?;
let input = TemplateInput::new(ast, &config, template_args)?;
let source: String = match input.source {
Source::Source(ref s) => s.clone(),
Expand Down Expand Up @@ -66,7 +66,7 @@ fn build_template(ast: &syn::DeriveInput) -> Result<String, CompileError> {
&contexts,
heritage.as_ref(),
MapChain::new(),
config.whitespace,
config.whitespace(),
)
.build(&contexts[input.path.as_path()])?;
if input.print == Print::Code || input.print == Print::All {
Expand All @@ -75,161 +75,6 @@ fn build_template(ast: &syn::DeriveInput) -> Result<String, CompileError> {
Ok(code)
}

#[derive(Default)]
pub(crate) struct TemplateArgs {
pub(crate) source: Option<Source>,
pub(crate) print: Print,
pub(crate) escaping: Option<String>,
pub(crate) ext: Option<String>,
pub(crate) syntax: Option<String>,
pub(crate) config_path: Option<String>,
}

impl TemplateArgs {
fn new(ast: &'_ syn::DeriveInput) -> Result<Self, CompileError> {
// Check that an attribute called `template()` exists once and that it is
// the proper type (list).
let mut template_args = None;
for attr in &ast.attrs {
let ident = match attr.path.get_ident() {
Some(ident) => ident,
None => continue,
};

if ident == "template" {
if template_args.is_some() {
return Err("duplicated 'template' attribute".into());
}

match attr.parse_meta() {
Ok(syn::Meta::List(syn::MetaList { nested, .. })) => {
template_args = Some(nested);
}
Ok(_) => return Err("'template' attribute must be a list".into()),
Err(e) => return Err(format!("unable to parse attribute: {}", e).into()),
}
}
}
let template_args =
template_args.ok_or_else(|| CompileError::from("no attribute 'template' found"))?;

let mut args = Self::default();
// Loop over the meta attributes and find everything that we
// understand. Return a CompileError if something is not right.
// `source` contains an enum that can represent `path` or `source`.
for item in template_args {
let pair = match item {
syn::NestedMeta::Meta(syn::Meta::NameValue(ref pair)) => pair,
_ => {
return Err(format!(
"unsupported attribute argument {:?}",
item.to_token_stream()
)
.into())
}
};
let ident = match pair.path.get_ident() {
Some(ident) => ident,
None => unreachable!("not possible in syn::Meta::NameValue(…)"),
};

if ident == "path" {
if let syn::Lit::Str(ref s) = pair.lit {
if args.source.is_some() {
return Err("must specify 'source' or 'path', not both".into());
}
args.source = Some(Source::Path(s.value()));
} else {
return Err("template path must be string literal".into());
}
} else if ident == "source" {
if let syn::Lit::Str(ref s) = pair.lit {
if args.source.is_some() {
return Err("must specify 'source' or 'path', not both".into());
}
args.source = Some(Source::Source(s.value()));
} else {
return Err("template source must be string literal".into());
}
} else if ident == "print" {
if let syn::Lit::Str(ref s) = pair.lit {
args.print = s.value().parse()?;
} else {
return Err("print value must be string literal".into());
}
} else if ident == "escape" {
if let syn::Lit::Str(ref s) = pair.lit {
args.escaping = Some(s.value());
} else {
return Err("escape value must be string literal".into());
}
} else if ident == "ext" {
if let syn::Lit::Str(ref s) = pair.lit {
args.ext = Some(s.value());
} else {
return Err("ext value must be string literal".into());
}
} else if ident == "syntax" {
if let syn::Lit::Str(ref s) = pair.lit {
args.syntax = Some(s.value())
} else {
return Err("syntax value must be string literal".into());
}
} else if ident == "config" {
if let syn::Lit::Str(ref s) = pair.lit {
args.config_path = Some(s.value())
} else {
return Err("config value must be string literal".into());
}
} else {
return Err(format!("unsupported attribute key {:?} found", ident).into());
}
}

Ok(args)
}
}

fn find_used_templates(
input: &TemplateInput<'_>,
map: &mut HashMap<PathBuf, String>,
source: String,
) -> Result<(), CompileError> {
let mut dependency_graph = Vec::new();
let mut check = vec![(input.path.clone(), source)];
while let Some((path, source)) = check.pop() {
for n in parse(&source, input.syntax)? {
match n {
Node::Extends(Expr::StrLit(extends)) => {
let extends = input.config.find_template(extends, Some(&path))?;
let dependency_path = (path.clone(), extends.clone());
if dependency_graph.contains(&dependency_path) {
return Err(format!(
"cyclic dependecy in graph {:#?}",
dependency_graph
.iter()
.map(|e| format!("{:#?} --> {:#?}", e.0, e.1))
.collect::<Vec<String>>()
)
.into());
}
dependency_graph.push(dependency_path);
let source = get_template_source(&extends)?;
check.push((extends, source));
}
Node::Import(_, import, _) => {
let import = input.config.find_template(import, Some(&path))?;
let source = get_template_source(&import)?;
check.push((import, source));
}
_ => {}
}
}
map.insert(path, source);
}
Ok(())
}

struct Generator<'a> {
// The template input state: original struct AST and attributes
input: &'a TemplateInput<'a>,
Expand Down Expand Up @@ -597,7 +442,7 @@ impl<'a> Generator<'a> {
Node::Lit(lws, val, rws) => {
self.visit_lit(lws, val, rws);
}
Node::Comment(ws) => {
Node::Comment(ws, _) => {
self.write_comment(ws);
}
Node::Expr(ws, ref val) => {
Expand All @@ -612,7 +457,7 @@ impl<'a> Generator<'a> {
Node::Cond(ref conds, ws) => {
self.write_cond(ctx, buf, conds, ws)?;
}
Node::Match(ws1, ref expr, ref arms, ws2) => {
Node::Match(ws1, ref expr, _, ref arms, ws2) => {
self.write_match(ctx, buf, ws1, expr, arms, ws2)?;
}
Node::Loop(ref loop_block) => {
Expand Down Expand Up @@ -1425,9 +1270,7 @@ impl<'a> Generator<'a> {
Some(name) => self
.input
.config
.escapers
.iter()
.find_map(|(escapers, escaper)| escapers.contains(name).then_some(escaper))
.find_escaper(name)
.ok_or_else(|| CompileError::from("invalid escaper for escape filter"))?,
None => self.input.escaper,
};
Expand Down
8 changes: 4 additions & 4 deletions askama_derive/src/heritage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::config::Config;
use crate::parser::{Expr, Loop, Macro, Node};
use crate::parser::{Loop, Macro, Node};
use crate::CompileError;

pub(crate) struct Heritage<'a> {
Expand Down Expand Up @@ -44,7 +44,7 @@ pub(crate) struct Context<'a> {

impl Context<'_> {
pub(crate) fn new<'n>(
config: &Config<'_>,
config: &Config,
path: &Path,
nodes: &'n [Node<'n>],
) -> Result<Context<'n>, CompileError> {
Expand All @@ -58,7 +58,7 @@ impl Context<'_> {
while let Some(nodes) = nested.pop() {
for n in nodes {
match n {
Node::Extends(Expr::StrLit(extends_path)) if top => match extends {
Node::Extends(extends_path) if top => match extends {
Some(_) => return Err("multiple extend blocks found".into()),
None => {
extends = Some(config.find_template(extends_path, Some(path))?);
Expand Down Expand Up @@ -93,7 +93,7 @@ impl Context<'_> {
nested.push(body);
nested.push(else_block);
}
Node::Match(_, _, arms, _) => {
Node::Match(_, _, _, arms, _) => {
for (_, _, arm) in arms {
nested.push(arm);
}
Expand Down
53 changes: 2 additions & 51 deletions askama_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,18 @@
#![deny(elided_lifetimes_in_paths)]
#![deny(unreachable_pub)]

use std::borrow::Cow;
use std::fmt;

use proc_macro::TokenStream;
use proc_macro2::Span;

mod config;
use askama_parser::{config, input, parser, CompileError};

mod generator;
mod heritage;
mod input;
mod parser;

#[proc_macro_derive(Template, attributes(template))]
pub fn derive_template(input: TokenStream) -> TokenStream {
generator::derive_template(input)
}

#[derive(Debug, Clone)]
struct CompileError {
msg: Cow<'static, str>,
span: Span,
}

impl CompileError {
fn new<S: Into<Cow<'static, str>>>(s: S, span: Span) -> Self {
Self {
msg: s.into(),
span,
}
}

fn into_compile_error(self) -> TokenStream {
syn::Error::new(self.span, self.msg)
.to_compile_error()
.into()
}
}

impl std::error::Error for CompileError {}

impl fmt::Display for CompileError {
#[inline]
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str(&self.msg)
}
}

impl From<&'static str> for CompileError {
#[inline]
fn from(s: &'static str) -> Self {
Self::new(s, Span::call_site())
}
}

impl From<String> for CompileError {
#[inline]
fn from(s: String) -> Self {
Self::new(s, Span::call_site())
}
}

// This is used by the code generator to decide whether a named filter is part of
// Askama or should refer to a local `filters` module. It should contain all the
// filters shipped with Askama, even the optional ones (since optional inclusion
Expand Down
13 changes: 13 additions & 0 deletions askama_fmt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "askama_fmt"
version = "0.1.0"
description = "Formatter for Askama template syntax"
homepage = "https://github.com/djc/askama"
repository = "https://github.com/djc/askama"
license = "MIT/Apache-2.0"
workspace = ".."
readme = "README.md"
edition = "2018"

[dependencies]
askama_parser = { version = "0.1.0", path = "../askama_parser", features = ["config"] }
Loading