Repository: mitsuhiko/minijinja (language: rust)

Issue:
2.0: Make Syntax extensible and move whitespace control into it
For future support of line statements and similar features it would be nice if `Syntax` was extensible.  Today it's not.  Also `trim_blocks` and `lstrip_blocks` are potentially features that might be better held on the `Syntax` struct than the engine itself.


Discussion / hints:

Fix patch:
diff --git a/examples/generate-yaml/src/main.rs b/examples/generate-yaml/src/main.rs
index 56461de2..93864edd 100644
--- a/examples/generate-yaml/src/main.rs
+++ b/examples/generate-yaml/src/main.rs
@@ -2,19 +2,19 @@
 use std::collections::BTreeMap;
 use std::env;
 
-use minijinja::{context, Environment, Syntax};
+use minijinja::syntax::SyntaxConfig;
+use minijinja::{context, Environment};
 
 fn main() {
     let mut env = Environment::new();
-    env.set_syntax(Syntax {
-        block_start: "{%".into(),
-        block_end: "%}".into(),
-        variable_start: "${{".into(),
-        variable_end: "}}".into(),
-        comment_start: "{#".into(),
-        comment_end: "#}".into(),
-    })
-    .unwrap();
+    env.set_syntax(
+        SyntaxConfig::builder()
+            .block_delimiters("{%", "%}")
+            .variable_delimiters("${{", "}}")
+            .block_delimiters("{#", "#}")
+            .build()
+            .unwrap(),
+    );
     env.add_template("template.yml", include_str!("template.yaml"))
         .unwrap();
     let tmpl = env.get_template("template.yml").unwrap();
diff --git a/minijinja-py/src/environment.rs b/minijinja-py/src/environment.rs
index ffcadcd6..9023c0cd 100644
--- a/minijinja-py/src/environment.rs
+++ b/minijinja-py/src/environment.rs
@@ -3,8 +3,9 @@ use std::ffi::c_void;
 use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
 use std::sync::Mutex;
 
+use minijinja::syntax::SyntaxConfig;
 use minijinja::value::{Rest, Value};
-use minijinja::{context, escape_formatter, AutoEscape, Error, State, Syntax, UndefinedBehavior};
+use minijinja::{context, escape_formatter, AutoEscape, Error, State, UndefinedBehavior};
 use pyo3::conversion::AsPyPointer;
 use pyo3::exceptions::PyRuntimeError;
 use pyo3::prelude::*;
@@ -20,6 +21,37 @@ thread_local! {
     static CURRENT_ENV: AtomicPtr<c_void> = const { AtomicPtr::new(std::ptr::null_mut()) };
 }
 
+struct Syntax {
+    block_start: String,
+    block_end: String,
+    variable_start: String,
+    variable_end: String,
+    comment_start: String,
+    comment_end: String,
+}
+
+impl Default for Syntax {
+    fn default() -> Self {
+        Self {
+            block_start: "{%".into(),
+            block_end: "%}".into(),
+            variable_start: "{{".into(),
+            variable_end: "}}".into(),
+            comment_start: "{%".into(),
+            comment_end: "%}".into(),
+        }
+    }
+}
+impl Syntax {
+    fn compile(&self) -> Result<SyntaxConfig, Error> {
+        SyntaxConfig::builder()
+            .block_delimiters(self.block_start.clone(), self.block_end.clone())
+            .variable_delimiters(self.variable_start.clone(), self.variable_end.clone())
+            .comment_delimiters(self.comment_start.clone(), self.comment_end.clone())
+            .build()
+    }
+}
+
 macro_rules! syntax_setter {
     ($slf:expr, $value:expr, $field:ident, $default:expr) => {{
         let value = $value;
@@ -33,8 +65,8 @@ macro_rules! syntax_setter {
         if let Some(ref mut syntax) = inner.syntax {
             if syntax.$field != value {
                 syntax.$field = value.into();
-                let new_syntax = syntax.clone();
-                inner.env.set_syntax(new_syntax).map_err(to_py_error)?;
+                let syntax_config = syntax.compile().map_err(to_py_error)?;
+                inner.env.set_syntax(syntax_config);
             }
         }
         Ok(())
diff --git a/minijinja/src/compiler/lexer.rs b/minijinja/src/compiler/lexer.rs
index e535527b..35e47948 100644
--- a/minijinja/src/compiler/lexer.rs
+++ b/minijinja/src/compiler/lexer.rs
@@ -3,16 +3,9 @@ use std::ops::ControlFlow;
 
 use crate::compiler::tokens::{Span, Token};
 use crate::error::{Error, ErrorKind};
+use crate::syntax::SyntaxConfig;
 use crate::utils::{memchr, memstr, unescape};
 
-#[cfg(feature = "custom_syntax")]
-pub use crate::custom_syntax::SyntaxConfig;
-
-/// Non configurable syntax config
-#[cfg(not(feature = "custom_syntax"))]
-#[derive(Debug, Clone, Default)]
-pub struct SyntaxConfig;
-
 /// Internal config struct to control whitespace in the engine.
 #[derive(Copy, Clone, Debug, Default)]
 pub struct WhitespaceConfig {
@@ -112,24 +105,24 @@ fn find_start_marker(a: &str, _syntax_config: &SyntaxConfig) -> Option<(usize, W
     find_start_marker_memchr(a)
 }
 
-fn match_start_marker(rest: &str, syntax_config: &SyntaxConfig) -> Option<(StartMarker, usize)> {
+fn match_start_marker(rest: &str, syntax: &SyntaxConfig) -> Option<(StartMarker, usize)> {
     #[cfg(not(feature = "custom_syntax"))]
     {
-        let _ = syntax_config;
+        let _ = syntax;
         match_start_marker_default(rest)
     }
 
     #[cfg(feature = "custom_syntax")]
     {
-        if syntax_config.aho_corasick.is_none() {
+        if syntax.aho_corasick.is_none() {
             return match_start_marker_default(rest);
         }
 
-        for delimiter in syntax_config.start_delimiters_order {
+        for delimiter in syntax.start_delimiters_order {
             let marker = match delimiter {
-                StartMarker::Variable => &syntax_config.syntax.variable_start as &str,
-                StartMarker::Block => &syntax_config.syntax.block_start as &str,
-                StartMarker::Comment => &syntax_config.syntax.comment_start as &str,
+                StartMarker::Variable => syntax.variable_delimiters().0,
+                StartMarker::Block => syntax.block_delimiters().0,
+                StartMarker::Comment => syntax.comment_delimiters().0,
             };
             if rest.get(..marker.len()) == Some(marker) {
                 return Some((delimiter, marker.len()));
@@ -149,22 +142,6 @@ fn match_start_marker_default(rest: &str) -> Option<(StartMarker, usize)> {
     }
 }
 
-macro_rules! syntax_token_getter {
-    ($ident:ident, $default:expr) => {
-        #[inline]
-        fn $ident(&self) -> &str {
-            #[cfg(feature = "custom_syntax")]
-            {
-                &self.syntax_config.syntax.$ident
-            }
-            #[cfg(not(feature = "custom_syntax"))]
-            {
-                $default
-            }
-        }
-    };
-}
-
 #[cfg(feature = "unicode")]
 fn lex_identifier(s: &str) -> usize {
     s.chars()
@@ -491,10 +468,21 @@ impl<'s> Tokenizer<'s> {
         }
     }
 
-    syntax_token_getter!(variable_end, "}}");
-    syntax_token_getter!(block_start, "{%");
-    syntax_token_getter!(block_end, "%}");
-    syntax_token_getter!(comment_end, "#}");
+    fn variable_end(&self) -> &str {
+        self.syntax_config.variable_delimiters().1
+    }
+
+    fn block_start(&self) -> &str {
+        self.syntax_config.block_delimiters().0
+    }
+
+    fn block_end(&self) -> &str {
+        self.syntax_config.block_delimiters().1
+    }
+
+    fn comment_end(&self) -> &str {
+        self.syntax_config.comment_delimiters().1
+    }
 
     fn tokenize_root(&mut self) -> Result<ControlFlow<(Token<'s>, Span)>, Error> {
         if let Some((marker, skip)) = match_start_marker(self.rest, &self.syntax_config) {
@@ -789,18 +777,12 @@ mod tests {
     #[test]
     #[cfg(feature = "custom_syntax")]
     fn test_find_marker_custom_syntax() {
-        use crate::Syntax;
-
-        let syntax = Syntax {
-            block_start: "%{".into(),
-            block_end: "}%".into(),
-            variable_start: "[[".into(),
-            variable_end: "]]".into(),
-            comment_start: "/*".into(),
-            comment_end: "*/".into(),
-        };
-
-        let syntax_config = syntax.compile().expect("failed to create syntax config");
+        let syntax_config = SyntaxConfig::builder()
+            .block_delimiters("%{", "}%")
+            .variable_delimiters("[[", "]]")
+            .comment_delimiters("/*", "*/")
+            .build()
+            .unwrap();
 
         assert_eq!(
             find_start_marker("%{", &syntax_config),
diff --git a/minijinja/src/compiler/parser.rs b/minijinja/src/compiler/parser.rs
index 4236cf56..44f37314 100644
--- a/minijinja/src/compiler/parser.rs
+++ b/minijinja/src/compiler/parser.rs
@@ -3,9 +3,10 @@ use std::collections::BTreeSet;
 use std::fmt;
 
 use crate::compiler::ast::{self, Spanned};
-use crate::compiler::lexer::{SyntaxConfig, Tokenizer, WhitespaceConfig};
+use crate::compiler::lexer::{Tokenizer, WhitespaceConfig};
 use crate::compiler::tokens::{Span, Token};
 use crate::error::{Error, ErrorKind};
+use crate::syntax::SyntaxConfig;
 use crate::value::Value;
 
 const MAX_RECURSION: usize = 150;
diff --git a/minijinja/src/custom_syntax.rs b/minijinja/src/custom_syntax.rs
deleted file mode 100644
index b080243a..00000000
--- a/minijinja/src/custom_syntax.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-use aho_corasick::AhoCorasick;
-use std::borrow::Cow;
-use std::sync::Arc;
-
-use crate::compiler::lexer::StartMarker;
-use crate::error::{Error, ErrorKind};
-
-/// The delimiter configuration for the environment and the parser.
-///
-/// MiniJinja allows you to override the syntax configuration for
-/// templates by setting different delimiters.  The end markers can
-/// be shared, but the start markers need to be distinct.  It would
-/// thus not be valid to configure `{{` to be the marker for both
-/// variables and blocks.
-///
-/// ```
-/// # use minijinja::{Environment, Syntax};
-/// let mut environment = Environment::new();
-/// environment.set_syntax(Syntax {
-///     block_start: "\\BLOCK{".into(),
-///     block_end: "}".into(),
-///     variable_start: "\\VAR{".into(),
-///     variable_end: "}".into(),
-///     comment_start: "\\#{".into(),
-///     comment_end: "}".into(),
-/// }).unwrap();
-/// ```
-#[derive(Debug, Clone, Eq, PartialEq)]
-#[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
-pub struct Syntax {
-    /// The start of a block. By default it is `{%`.
-    pub block_start: Cow<'static, str>,
-    /// The end of a block. By default it is `%}`.
-    pub block_end: Cow<'static, str>,
-    /// The start of a variable. By default it is `{{`.
-    pub variable_start: Cow<'static, str>,
-    /// The end of a variable. By default it is `}}`.
-    pub variable_end: Cow<'static, str>,
-    /// The start of a comment. By default it is `{#`.
-    pub comment_start: Cow<'static, str>,
-    /// The end of a comment. By default it is `#}`.
-    pub comment_end: Cow<'static, str>,
-}
-
-const DEFAULT_SYNTAX: Syntax = Syntax {
-    block_start: Cow::Borrowed("{%"),
-    block_end: Cow::Borrowed("%}"),
-    variable_start: Cow::Borrowed("{{"),
-    variable_end: Cow::Borrowed("}}"),
-    comment_start: Cow::Borrowed("{#"),
-    comment_end: Cow::Borrowed("#}"),
-};
-
-impl Default for Syntax {
-    fn default() -> Self {
-        DEFAULT_SYNTAX
-    }
-}
-
-impl Syntax {
-    /// Creates a new syntax configuration with custom delimiters.
-    pub(crate) fn compile(self) -> Result<SyntaxConfig, Error> {
-        if self == DEFAULT_SYNTAX {
-            return Ok(SyntaxConfig::default());
-        }
-
-        ok!(self.check_delimiters());
-
-        let mut delimiter_order = [
-            StartMarker::Variable,
-            StartMarker::Block,
-            StartMarker::Comment,
-        ];
-        delimiter_order.sort_by_key(|marker| {
-            std::cmp::Reverse(match marker {
-                StartMarker::Variable => self.variable_start.len(),
-                StartMarker::Block => self.block_start.len(),
-                StartMarker::Comment => self.comment_start.len(),
-            })
-        });
-
-        let aho_corasick = ok!(AhoCorasick::builder()
-            .match_kind(aho_corasick::MatchKind::LeftmostLongest)
-            .build([
-                &self.variable_start as &str,
-                &self.block_start as &str,
-                &self.comment_start as &str,
-            ])
-            .map_err(|_| ErrorKind::InvalidDelimiter.into()));
-        Ok(Arc::new(SyntaxConfigInternal {
-            syntax: self,
-            aho_corasick: Some(aho_corasick),
-            start_delimiters_order: delimiter_order,
-        }))
-    }
-
-    /// block, variable and comment start strings must be different
-    fn check_delimiters(&self) -> Result<(), Error> {
-        if self.block_start != self.variable_start
-            && self.block_start != self.comment_start
-            && self.variable_start != self.comment_start
-        {
-            Ok(())
-        } else {
-            Err(ErrorKind::InvalidDelimiter.into())
-        }
-    }
-}
-
-/// Internal configuration for the environment and the parser.
-#[derive(Debug)]
-pub struct SyntaxConfigInternal {
-    pub(crate) syntax: Syntax,
-    pub(crate) start_delimiters_order: [StartMarker; 3],
-    pub(crate) aho_corasick: Option<aho_corasick::AhoCorasick>,
-}
-
-/// Configurable syntax config
-pub type SyntaxConfig = Arc<SyntaxConfigInternal>;
-
-impl Default for SyntaxConfigInternal {
-    fn default() -> Self {
-        SyntaxConfigInternal {
-            syntax: Syntax::default(),
-            start_delimiters_order: [
-                StartMarker::Variable,
-                StartMarker::Block,
-                StartMarker::Comment,
-            ],
-            aho_corasick: None,
-        }
-    }
-}
diff --git a/minijinja/src/environment.rs b/minijinja/src/environment.rs
index 066115fc..a1257be3 100644
--- a/minijinja/src/environment.rs
+++ b/minijinja/src/environment.rs
@@ -7,7 +7,6 @@ use serde::Serialize;
 
 use crate::compiler::codegen::CodeGenerator;
 use crate::compiler::instructions::Instructions;
-use crate::compiler::lexer::SyntaxConfig;
 use crate::compiler::parser::parse_expr;
 use crate::error::{attach_basic_debug_info, Error, ErrorKind};
 use crate::expression::Expression;
@@ -600,22 +599,17 @@ impl<'source> Environment<'source> {
     /// This setting is used whenever a template is loaded into the environment.
     /// Changing it at a later point only affects future templates loaded.
     ///
-    /// See [`Syntax`](crate::Syntax) for more information.
+    /// See [`SyntaxConfig`](crate::syntax::SyntaxConfig) for more information.
     #[cfg(feature = "custom_syntax")]
     #[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
-    pub fn set_syntax(&mut self, syntax: crate::custom_syntax::Syntax) -> Result<(), Error> {
-        self.templates.template_config.syntax_config = ok!(syntax.compile());
-        Ok(())
+    pub fn set_syntax(&mut self, syntax: crate::syntax::SyntaxConfig) {
+        self.templates.template_config.syntax_config = syntax;
     }
 
-    /// Returns the current syntax.
+    /// Returns the current syntax config.
     #[cfg(feature = "custom_syntax")]
     #[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
-    pub fn syntax(&self) -> &crate::custom_syntax::Syntax {
-        &self._syntax_config().syntax
-    }
-
-    fn _syntax_config(&self) -> &SyntaxConfig {
+    pub fn syntax(&self) -> &crate::syntax::SyntaxConfig {
         &self.templates.template_config.syntax_config
     }
 
diff --git a/minijinja/src/lib.rs b/minijinja/src/lib.rs
index 90f73aad..2a16fea3 100644
--- a/minijinja/src/lib.rs
+++ b/minijinja/src/lib.rs
@@ -227,9 +227,6 @@ mod loader;
 #[cfg(feature = "loader")]
 pub use loader::path_loader;
 
-#[cfg(feature = "custom_syntax")]
-mod custom_syntax;
-
 #[cfg(feature = "debug")]
 mod debug;
 
@@ -244,9 +241,6 @@ pub use self::utils::{AutoEscape, HtmlEscape, UndefinedBehavior};
 /// Re-export for convenience.
 pub use self::value::Value;
 
-#[cfg(feature = "custom_syntax")]
-pub use self::custom_syntax::Syntax;
-
 pub use self::macros::__context;
 pub use self::vm::State;
 
@@ -262,7 +256,7 @@ pub mod machinery {
     pub use crate::compiler::ast;
     pub use crate::compiler::codegen::CodeGenerator;
     pub use crate::compiler::instructions::{Instruction, Instructions};
-    pub use crate::compiler::lexer::{tokenize, SyntaxConfig, Tokenizer, WhitespaceConfig};
+    pub use crate::compiler::lexer::{tokenize, Tokenizer, WhitespaceConfig};
     pub use crate::compiler::parser::{parse, parse_expr};
     pub use crate::compiler::tokens::{Span, Token};
     pub use crate::template::{CompiledTemplate, TemplateConfig};
@@ -281,10 +275,4 @@ pub mod machinery {
     pub fn make_string_output(s: &mut String) -> Output<'_> {
         Output::with_string(s)
     }
-
-    /// Creates a syntax config from a given [`Syntax`](crate::Syntax).
-    #[cfg(feature = "custom_syntax")]
-    pub fn make_syntax_config(syntax: crate::Syntax) -> Result<SyntaxConfig, crate::Error> {
-        syntax.compile()
-    }
 }
diff --git a/minijinja/src/syntax.rs b/minijinja/src/syntax.rs
index 9cc3e49b..c2906e5c 100644
--- a/minijinja/src/syntax.rs
+++ b/minijinja/src/syntax.rs
@@ -1,4 +1,11 @@
-//! Documents the syntax for templates.
+#![cfg_attr(
+    feature = "custom_syntax",
+    doc = "Documents the syntax for templates and provides ways to reconfigure it."
+)]
+#![cfg_attr(
+    not(feature = "custom_syntax"),
+    doc = "Documents the syntax for templates."
+)]
 //!
 //! <details><summary><strong style="cursor: pointer">Table of Contents</strong></summary>
 //!
@@ -720,23 +727,22 @@
 # Custom Delimiters
 
 When MiniJinja has been compiled with the `custom_syntax` feature (see
-[`Syntax`](crate::Syntax)), it's possible to reconfigure the delimiters of the
+[`SyntaxConfig`]), it's possible to reconfigure the delimiters of the
 templates.  This is generally not recommended but it's useful for situations
 where Jinja templates are used to generate files with a syntax that would be
 conflicting for Jinja.  With custom delimiters it can for instance be more
 convenient to generate LaTeX files:
 
 ```
-# use minijinja::{Environment, Syntax};
+# use minijinja::{Environment, syntax::SyntaxConfig};
 let mut environment = Environment::new();
-environment.set_syntax(Syntax {
-    block_start: "\\BLOCK{".into(),
-    block_end: "}".into(),
-    variable_start: "\\VAR{".into(),
-    variable_end: "}".into(),
-    comment_start: "\\#{".into(),
-    comment_end: "}".into(),
-}).unwrap();
+environment.set_syntax(SyntaxConfig::builder()
+    .block_delimiters("\\BLOCK{", "}")
+    .variable_delimiters("\\VAR{", "}")
+    .comment_delimiters("\\#{", "}")
+    .build()
+    .unwrap()
+);
 ```
 
 And then a template might look like this instead:
@@ -830,3 +836,215 @@ And then a template might look like this instead:
 //!
 //! By default, MiniJinja also removes trailing newlines.  To keep single trailing newlines,
 //! configure MiniJinja to [`keep_trailing_newline`](crate::Environment::set_keep_trailing_newline).
+
+#[cfg(feature = "custom_syntax")]
+mod imp {
+    use crate::compiler::lexer::StartMarker;
+    use crate::error::{Error, ErrorKind};
+    use aho_corasick::AhoCorasick;
+    use std::borrow::Cow;
+    use std::sync::Arc;
+
+    #[derive(Debug, PartialEq, Clone)]
+    pub(crate) struct Delims {
+        block_start: Cow<'static, str>,
+        block_end: Cow<'static, str>,
+        variable_start: Cow<'static, str>,
+        variable_end: Cow<'static, str>,
+        comment_start: Cow<'static, str>,
+        comment_end: Cow<'static, str>,
+    }
+
+    const DEFAULT_DELIMS: Delims = Delims {
+        block_start: Cow::Borrowed("{%"),
+        block_end: Cow::Borrowed("%}"),
+        variable_start: Cow::Borrowed("{{"),
+        variable_end: Cow::Borrowed("}}"),
+        comment_start: Cow::Borrowed("{#"),
+        comment_end: Cow::Borrowed("#}"),
+    };
+
+    /// Builder helper to reconfigure the syntax.
+    ///
+    /// A new builder is returned by [`SyntaxConfig::builder`].
+    #[derive(Debug)]
+    #[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
+    pub struct SyntaxConfigBuilder {
+        delims: Arc<Delims>,
+    }
+
+    impl SyntaxConfigBuilder {
+        /// Sets the block start and end delimiters.
+        pub fn block_delimiters<S, E>(&mut self, s: S, e: E) -> &mut Self
+        where
+            S: Into<Cow<'static, str>>,
+            E: Into<Cow<'static, str>>,
+        {
+            let delims = Arc::make_mut(&mut self.delims);
+            delims.block_start = s.into();
+            delims.block_end = e.into();
+            self
+        }
+
+        /// Sets the variable start and end delimiters.
+        pub fn variable_delimiters<S, E>(&mut self, s: S, e: E) -> &mut Self
+        where
+            S: Into<Cow<'static, str>>,
+            E: Into<Cow<'static, str>>,
+        {
+            let delims = Arc::make_mut(&mut self.delims);
+            delims.variable_start = s.into();
+            delims.variable_end = e.into();
+            self
+        }
+
+        /// Sets the comment start and end delimiters.
+        pub fn comment_delimiters<S, E>(&mut self, s: S, e: E) -> &mut Self
+        where
+            S: Into<Cow<'static, str>>,
+            E: Into<Cow<'static, str>>,
+        {
+            let delims = Arc::make_mut(&mut self.delims);
+            delims.comment_start = s.into();
+            delims.comment_end = e.into();
+            self
+        }
+
+        /// Builds the final syntax config.
+        pub fn build(&self) -> Result<SyntaxConfig, Error> {
+            let delims = self.delims.clone();
+            if *delims == DEFAULT_DELIMS {
+                return Ok(SyntaxConfig::default());
+            } else if delims.block_start == delims.variable_start
+                || delims.block_start == delims.comment_start
+                || delims.variable_start == delims.comment_start
+            {
+                return Err(ErrorKind::InvalidDelimiter.into());
+            }
+            let mut start_delimiters_order = [
+                StartMarker::Variable,
+                StartMarker::Block,
+                StartMarker::Comment,
+            ];
+            start_delimiters_order.sort_by_key(|marker| {
+                std::cmp::Reverse(match marker {
+                    StartMarker::Variable => delims.variable_start.len(),
+                    StartMarker::Block => delims.block_start.len(),
+                    StartMarker::Comment => delims.comment_start.len(),
+                })
+            });
+            let aho_corasick = ok!(AhoCorasick::builder()
+                .match_kind(aho_corasick::MatchKind::LeftmostLongest)
+                .build([
+                    &delims.variable_start as &str,
+                    &delims.block_start as &str,
+                    &delims.comment_start as &str,
+                ])
+                .map_err(|_| ErrorKind::InvalidDelimiter.into()));
+            Ok(SyntaxConfig {
+                delims,
+                start_delimiters_order,
+                aho_corasick: Some(aho_corasick),
+            })
+        }
+    }
+
+    /// The delimiter configuration for the environment and the parser.
+    ///
+    /// Custom syntax configurations require the `custom_syntax` feature.
+    /// Otherwise just the default syntax is available.
+    ///
+    /// You can  override the syntax configuration for templates by setting different
+    /// delimiters.  The end markers can be shared, but the start markers need to be
+    /// distinct.  It would thus not be valid to configure `{{` to be the marker for
+    /// both variables and blocks.
+    ///
+    /// ```
+    /// # use minijinja::{Environment, syntax::SyntaxConfig};
+    /// let mut environment = Environment::new();
+    /// environment.set_syntax(
+    ///     SyntaxConfig::builder()
+    ///         .block_delimiters("\\BLOCK{", "}")
+    ///         .variable_delimiters("\\VAR{", "}")
+    ///         .comment_delimiters("\\#{", "}")
+    ///         .build()
+    ///         .unwrap()
+    /// );
+    #[derive(Clone, Debug)]
+    pub struct SyntaxConfig {
+        delims: Arc<Delims>,
+        pub(crate) start_delimiters_order: [StartMarker; 3],
+        pub(crate) aho_corasick: Option<aho_corasick::AhoCorasick>,
+    }
+
+    impl Default for SyntaxConfig {
+        fn default() -> Self {
+            Self {
+                delims: Arc::new(DEFAULT_DELIMS),
+                start_delimiters_order: [
+                    StartMarker::Variable,
+                    StartMarker::Block,
+                    StartMarker::Comment,
+                ],
+                aho_corasick: None,
+            }
+        }
+    }
+
+    impl SyntaxConfig {
+        /// Creates a syntax builder.
+        #[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
+        pub fn builder() -> SyntaxConfigBuilder {
+            SyntaxConfigBuilder {
+                delims: Arc::new(DEFAULT_DELIMS),
+            }
+        }
+
+        /// Returns the block delimiters.
+        #[inline(always)]
+        pub fn block_delimiters(&self) -> (&str, &str) {
+            (&self.delims.block_start, &self.delims.block_end)
+        }
+
+        /// Returns the variable delimiters.
+        #[inline(always)]
+        pub fn variable_delimiters(&self) -> (&str, &str) {
+            (&self.delims.variable_start, &self.delims.variable_end)
+        }
+
+        /// Returns the comment delimiters.
+        #[inline(always)]
+        pub fn comment_delimiters(&self) -> (&str, &str) {
+            (&self.delims.comment_start, &self.delims.comment_end)
+        }
+    }
+}
+
+#[cfg(not(feature = "custom_syntax"))]
+mod imp {
+    /// The default delimiter configuration for the environment and the parser.
+    #[derive(Clone, Default, Debug)]
+    pub struct SyntaxConfig;
+
+    impl SyntaxConfig {
+        /// Returns the block delimiters.
+        #[inline(always)]
+        pub fn block_delimiters(&self) -> (&str, &str) {
+            ("{%", "%}")
+        }
+
+        /// Returns the variable delimiters.
+        #[inline(always)]
+        pub fn variable_delimiters(&self) -> (&str, &str) {
+            ("{{", "}}")
+        }
+
+        /// Returns the comment delimiters.
+        #[inline(always)]
+        pub fn comment_delimiters(&self) -> (&str, &str) {
+            ("{#", "#}")
+        }
+    }
+}
+
+pub use self::imp::*;
diff --git a/minijinja/src/template.rs b/minijinja/src/template.rs
index eac48ef3..d528570a 100644
--- a/minijinja/src/template.rs
+++ b/minijinja/src/template.rs
@@ -7,12 +7,13 @@ use serde::Serialize;
 
 use crate::compiler::codegen::CodeGenerator;
 use crate::compiler::instructions::Instructions;
-use crate::compiler::lexer::{SyntaxConfig, WhitespaceConfig};
+use crate::compiler::lexer::WhitespaceConfig;
 use crate::compiler::meta::find_undeclared;
 use crate::compiler::parser::parse;
 use crate::environment::Environment;
 use crate::error::{attach_basic_debug_info, Error};
 use crate::output::{Output, WriteWrapper};
+use crate::syntax::SyntaxConfig;
 use crate::utils::AutoEscape;
 use crate::value::{self, Value};
 use crate::vm::{prepare_blocks, Context, State, Vm};


Test patch:
diff --git a/minijinja/tests/test_lexer.rs b/minijinja/tests/test_lexer.rs
index 290b948c..474c1564 100644
--- a/minijinja/tests/test_lexer.rs
+++ b/minijinja/tests/test_lexer.rs
@@ -1,8 +1,6 @@
 #![cfg(feature = "unstable_machinery")]
-use minijinja::machinery::{
-    make_syntax_config, tokenize, Span, SyntaxConfig, Token, WhitespaceConfig,
-};
-use minijinja::Syntax;
+use minijinja::machinery::{tokenize, Span, Token, WhitespaceConfig};
+use minijinja::syntax::SyntaxConfig;
 
 use std::fmt::Write;
 
@@ -20,19 +18,16 @@ struct TestSettings {
 impl TestSettings {
     pub fn into_configs(self) -> (SyntaxConfig, WhitespaceConfig) {
         (
-            make_syntax_config(if let Some(ref markers) = self.markers {
-                Syntax {
-                    block_start: markers[0].to_string().into(),
-                    block_end: markers[1].to_string().into(),
-                    variable_start: markers[2].to_string().into(),
-                    variable_end: markers[3].to_string().into(),
-                    comment_start: markers[4].to_string().into(),
-                    comment_end: markers[5].to_string().into(),
-                }
+            if let Some(ref markers) = self.markers {
+                SyntaxConfig::builder()
+                    .block_delimiters(markers[0].to_string(), markers[1].to_string())
+                    .variable_delimiters(markers[2].to_string(), markers[3].to_string())
+                    .comment_delimiters(markers[4].to_string(), markers[5].to_string())
+                    .build()
+                    .unwrap()
             } else {
-                Syntax::default()
-            })
-            .unwrap(),
+                SyntaxConfig::default()
+            },
             WhitespaceConfig {
                 keep_trailing_newline: self.keep_trailing_newline,
                 lstrip_blocks: self.lstrip_blocks,
diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs
index cf6953e2..709377f2 100644
--- a/minijinja/tests/test_templates.rs
+++ b/minijinja/tests/test_templates.rs
@@ -407,15 +407,14 @@ fn test_flattening_sub_item_shielded_print() {
 #[cfg(feature = "custom_syntax")]
 fn test_custom_syntax() {
     let mut env = Environment::new();
-    env.set_syntax(minijinja::Syntax {
-        block_start: "{".into(),
-        block_end: "}".into(),
-        variable_start: "${".into(),
-        variable_end: "}".into(),
-        comment_start: "{*".into(),
-        comment_end: "*}".into(),
-    })
-    .unwrap();
+    env.set_syntax(
+        minijinja::syntax::SyntaxConfig::builder()
+            .block_delimiters("{", "}")
+            .variable_delimiters("${", "}")
+            .comment_delimiters("{*", "*}")
+            .build()
+            .unwrap(),
+    );
 
     // this on the other hand is okay
     let value = env