├── php_tests
├── tests
│ ├── fixtures
│ │ ├── func.twig
│ │ ├── snapshots
│ │ │ ├── func.txt
│ │ │ ├── array.txt
│ │ │ ├── assocArray.txt
│ │ │ ├── include.txt
│ │ │ ├── strConcat.txt
│ │ │ ├── filter.txt
│ │ │ ├── arithmetic.txt
│ │ │ ├── extends_basic.txt
│ │ │ ├── logic.txt
│ │ │ ├── variable_scopes.txt
│ │ │ └── basic.txt
│ │ ├── include_inner.twig
│ │ ├── array.twig
│ │ ├── assocArray.twig
│ │ ├── filter.twig
│ │ ├── include.twig
│ │ ├── extends_base.twig
│ │ ├── strConcat.twig
│ │ ├── extension.twig
│ │ ├── arithmetic.twig
│ │ ├── basic.html.twig
│ │ ├── logic.twig
│ │ └── variableScopes.twig
│ ├── IncludesTest.php
│ ├── ExtendsTest.php
│ ├── VariableTest.php
│ ├── SmokeTest.php
│ ├── Utils
│ │ └── SnapshotTestCase.php
│ └── ExpressionsTest.php
├── composer.json
└── phpunit.xml
├── src
├── loader
│ ├── expression
│ │ ├── mod.rs
│ │ ├── ast.rs
│ │ ├── parser.rs
│ │ └── lexer.rs
│ ├── mod.rs
│ ├── ast.rs
│ └── parser.rs
├── lib.rs
└── evaluation
│ ├── config.rs
│ ├── value.rs
│ ├── environment.rs
│ ├── mod.rs
│ └── expressions.rs
├── .gitignore
├── README.md
├── .cargo
└── config.toml
├── Makefile
├── .gitpod.yml
├── Cargo.toml
├── flake.lock
└── flake.nix
/php_tests/tests/fixtures/func.twig:
--------------------------------------------------------------------------------
1 | {{ max(2,3) }}
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/func.txt:
--------------------------------------------------------------------------------
1 | 3
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/include_inner.twig:
--------------------------------------------------------------------------------
1 | inner
2 |
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/array.txt:
--------------------------------------------------------------------------------
1 | ["foo","bar"]
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/assocArray.txt:
--------------------------------------------------------------------------------
1 | {"foo":"bar"}
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/array.twig:
--------------------------------------------------------------------------------
1 | {{ ["foo", "bar"]|json_encode }}
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/include.txt:
--------------------------------------------------------------------------------
1 | pre
2 | inner
3 | post
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/strConcat.txt:
--------------------------------------------------------------------------------
1 | first second 3
2 |
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/assocArray.twig:
--------------------------------------------------------------------------------
1 | {{ {foo: "bar"}|json_encode }}
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/filter.twig:
--------------------------------------------------------------------------------
1 | {{ "foo
"|striptags }}
2 | {{ d|date }}
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/filter.txt:
--------------------------------------------------------------------------------
1 | foo
2 | January 1, 2000 00:00
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/arithmetic.txt:
--------------------------------------------------------------------------------
1 | 25
2 | 19
3 | 4
4 | 3
5 | 16.28
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/extends_basic.txt:
--------------------------------------------------------------------------------
1 | new
2 | old
3 |
4 |
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/include.twig:
--------------------------------------------------------------------------------
1 | pre
2 | {% include 'include_inner.twig' %}
3 | post
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/extends_base.twig:
--------------------------------------------------------------------------------
1 | {% block B0 %}
2 | old
3 | {% endblock %}
4 |
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/strConcat.twig:
--------------------------------------------------------------------------------
1 | {% set s = "first" %}
2 | {{ s ~ " second " ~ 3 }}
3 |
--------------------------------------------------------------------------------
/src/loader/expression/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod ast;
2 | mod lexer;
3 | mod parser;
4 |
5 | pub use parser::{parse, Operator};
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 |
4 | vendor/
5 | .idea
6 | composer.lock
7 | .envrc
8 | .direnv
9 | .phpunit.cache
10 |
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/extension.twig:
--------------------------------------------------------------------------------
1 | {% extends 'extends_base.twig' %}
2 |
3 | {% block B0 %}
4 | new
5 | {{ parent() }}
6 | {% endblock %}
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/arithmetic.twig:
--------------------------------------------------------------------------------
1 | {{ (2 + 3) * 4 + 5 }}
2 | {{ 2 + 3 * 4 + 5 }}
3 | {{ (2 + 3) * 4 / 5 }}
4 | {{ 19 // 5 }}
5 | {{ 4.2 * 3.4 + 2}}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TAPE (Twig Adjacent PHP Extension)
2 |
3 | An experimental implementation of the twig templating language in Rust.
4 |
5 | **Do Not Use** (for now)
6 |
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/logic.txt:
--------------------------------------------------------------------------------
1 | Bool Display:
2 |
3 | 1
4 | Not table:
5 | 1
6 |
7 | And table:
8 |
9 |
10 |
11 | 1
12 | Or table:
13 |
14 | 1
15 | 1
16 | 1
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/variable_scopes.txt:
--------------------------------------------------------------------------------
1 | Before B0:
2 | gm
3 | B0:
4 | g
5 | i
6 | gm touched
7 | After B0:
8 | g
9 | gm touched
10 |
11 | g
12 |
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/snapshots/basic.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 | pre
4 | a
5 | post
6 | pre
7 | b
8 | post
9 | pre
10 | c
11 | post
12 |
13 | HELLO John
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/basic.html.twig:
--------------------------------------------------------------------------------
1 | {% block base_doctype %}
2 |
3 | {% endblock %}
4 |
5 | {% for test in coll %}
6 | pre
7 | {{ test }}
8 | post
9 | {% endfor %}
10 |
11 | HELLO {{ foo.name }}
--------------------------------------------------------------------------------
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.'cfg(not(target_os = "windows"))']
2 | rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]
3 |
4 | [target.x86_64-pc-windows-msvc]
5 | linker = "rust-lld"
6 |
7 | [target.i686-pc-windows-msvc]
8 | linker = "rust-lld"
9 |
10 |
--------------------------------------------------------------------------------
/php_tests/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tape/php_tests",
3 | "type": "project",
4 | "require": {
5 | "phpunit/phpunit": "^9.5",
6 | "twig/twig": "^3.4"
7 | },
8 | "autoload": {
9 | "psr-4": {
10 | "Test\\": "tests/"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | OS := $(shell uname)
2 |
3 | ifeq ($(OS), Darwin)
4 | TAPE_LIB := ./target/debug/libtape.dylib
5 | endif
6 |
7 | ifeq ($(OS), Linux)
8 | TAPE_LIB := ./target/debug/libtape.so
9 | endif
10 |
11 | .PHONY test:
12 | cargo t
13 | cargo b
14 | php -dextension=$(TAPE_LIB) php_tests/vendor/bin/phpunit php_tests/tests --stop-on-error
15 |
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/logic.twig:
--------------------------------------------------------------------------------
1 | Bool Display:
2 | {{ false }}
3 | {{ true }}
4 | Not table:
5 | {{ not false }}
6 | {{ not true }}
7 | And table:
8 | {{ false and false }}
9 | {{ true and false }}
10 | {{ false and true }}
11 | {{ true and true }}
12 | Or table:
13 | {{ false or false }}
14 | {{ true or false }}
15 | {{ false or true }}
16 | {{ true or true }}
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | tasks:
2 | - init: cargo build && composer install -d php_tests
3 | command: make
4 |
5 | vscode:
6 | extensions:
7 | - ms-vscode.makefile-tools
8 | - matklad.rust-analyzer
9 | - mblode.twig-language
10 |
11 | github:
12 | prebuilds:
13 | master: true
14 | pullRequests: true
15 | pullRequestsFromForks: true
16 | addBadge: true
17 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tape"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | anyhow = "1.0.65"
10 | ext-php-rs = { version = "0.8", features = ["anyhow"]}
11 | nom = "7.1.1"
12 | nom_locate = "4.0.0"
13 | rust_decimal = "1.26.1"
14 |
15 | [dev-dependencies]
16 | pretty_assertions = "1.3.0"
17 |
18 | [lib]
19 | crate-type = ["cdylib"]
20 |
--------------------------------------------------------------------------------
/php_tests/tests/fixtures/variableScopes.twig:
--------------------------------------------------------------------------------
1 | {% set global= 'g' %}
2 | {% set global_mut = 'gm' %}
3 | {% set by_expr = 'foo' %}
4 | Before B0:
5 | {{ global_mut }}
6 | {% block B0 %}
7 | {% set inner = 'i' %}
8 | {% set global_mut = 'gm touched' %}
9 | {% set by_expr = global %}
10 | B0:
11 | {{ global }}
12 | {{ inner }}
13 | {{ global_mut }}
14 | {% endblock %}
15 | After B0:
16 | {{ global }}
17 | {{ global_mut }}
18 | {{ inner }}
19 | {{ by_expr }}
20 |
--------------------------------------------------------------------------------
/php_tests/tests/IncludesTest.php:
--------------------------------------------------------------------------------
1 | twig = new Environment(new ArrayLoader([]));
19 | }
20 |
21 | public function testInclude()
22 | {
23 | $result = render(__DIR__ . '/fixtures/', 'include.twig', [], $this->twig);
24 | $this->assertSnapshot('include', $result);
25 | }
26 | }
--------------------------------------------------------------------------------
/php_tests/tests/ExtendsTest.php:
--------------------------------------------------------------------------------
1 | twig = new Environment(new ArrayLoader([]));
19 | }
20 |
21 | public function testBasicExtends()
22 | {
23 | $result = render(__DIR__ . '/fixtures/', 'extension.twig', [], $this->twig);
24 | $this->assertSnapshot('extends_basic', $result);
25 | }
26 | }
--------------------------------------------------------------------------------
/php_tests/tests/VariableTest.php:
--------------------------------------------------------------------------------
1 | twig = new Environment(new ArrayLoader([]));
19 | }
20 |
21 | public function testVariableScopes()
22 | {
23 | $result = render(__DIR__ . '/fixtures/', 'variableScopes.twig', [], $this->twig);
24 | $this->assertSnapshot('variable_scopes', $result);
25 | }
26 | }
--------------------------------------------------------------------------------
/php_tests/tests/SmokeTest.php:
--------------------------------------------------------------------------------
1 | twig = new Environment(new ArrayLoader([]));
19 | }
20 |
21 | public function testBasicTest()
22 | {
23 | $result = render(__DIR__ . '/fixtures/', 'basic.html.twig', ['foo' => ['name' => 'John'], 'coll' => ['a', 'b', 'c']], $this->twig);
24 | $this->assertSnapshot('basic', $result);
25 | }
26 | }
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | mod evaluation;
2 | mod loader;
3 | use std::path::PathBuf;
4 |
5 | use evaluation::{config::Config, environment::Env};
6 | use ext_php_rs::{prelude::*, types::Zval};
7 |
8 | use anyhow::Result;
9 | use loader::Loader;
10 |
11 | #[php_function]
12 | pub fn render(
13 | base_dir: &str,
14 | template: &str,
15 | data: &mut Zval,
16 | twig_env: &mut Zval,
17 | ) -> Result {
18 | let conf = Config::new(twig_env.shallow_clone());
19 | let base_dir = PathBuf::from(base_dir);
20 | let mut loader = Loader::new(base_dir);
21 | let tpl = loader.load(template)?;
22 | evaluation::render(tpl, Env::new(data.shallow_clone(), loader, conf))
23 | }
24 |
25 | #[php_module]
26 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
27 | module
28 | }
29 |
--------------------------------------------------------------------------------
/src/loader/expression/ast.rs:
--------------------------------------------------------------------------------
1 | use super::parser::Operator;
2 |
3 | #[derive(Debug, PartialEq, Clone)]
4 | pub enum Expression {
5 | Term(Term),
6 | Str(String),
7 | Var(String),
8 | Number(i64),
9 | Float(f64),
10 | Bool(bool),
11 | Null,
12 | Array(Vec),
13 | FuncCall(FuncCall),
14 | FilterCall(FuncCall),
15 | HashMap(Vec),
16 | Parent,
17 | }
18 |
19 | #[derive(Debug, PartialEq, Clone)]
20 | pub struct Term {
21 | pub op: Operator,
22 | pub params: Vec,
23 | }
24 |
25 | #[derive(Debug, PartialEq, Clone)]
26 | pub struct FuncCall {
27 | pub name: String,
28 | pub params: Vec,
29 | }
30 |
31 | #[derive(Debug, PartialEq, Clone)]
32 | pub struct KeyValuePair {
33 | pub key: Expression,
34 | pub val: Expression,
35 | }
36 |
--------------------------------------------------------------------------------
/php_tests/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 | tests
17 |
18 |
19 |
20 |
22 |
23 | src
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/php_tests/tests/Utils/SnapshotTestCase.php:
--------------------------------------------------------------------------------
1 | getSnapshotFile($snapshotName);
10 | if (!file_exists($snapshotFile) || ($_ENV['UPDATE_SNAPSHOTS'] ?? false)) {
11 | $this->createSnapshot($snapshotFile, $actual);
12 | }
13 | static::assertEquals(file_get_contents($snapshotFile), $actual);
14 | }
15 |
16 | private function getSnapshotName()
17 | {
18 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
19 | $testClass = $trace[1]['class'];
20 | $testMethod = $trace[1]['function'];
21 | $snapshotName = $testClass . '::' . $testMethod;
22 | return $snapshotName;
23 | }
24 |
25 | private function getSnapshotFile($snapshotName)
26 | {
27 | $snapshotFile = __DIR__ . '/../fixtures/snapshots/' . $snapshotName . '.txt';
28 | return $snapshotFile;
29 | }
30 |
31 | private function createSnapshot($snapshotFile, $actual)
32 | {
33 | $dir = dirname($snapshotFile);
34 | if (!file_exists($dir)) {
35 | mkdir($dir, 0777, true);
36 | }
37 | file_put_contents($snapshotFile, $actual);
38 | }
39 | }
--------------------------------------------------------------------------------
/php_tests/tests/ExpressionsTest.php:
--------------------------------------------------------------------------------
1 | twig = new Environment(new ArrayLoader([]));
19 | }
20 |
21 | public function testArithmetic()
22 | {
23 | $result = render(__DIR__ . '/fixtures/', 'arithmetic.twig', [], $this->twig);
24 | $this->assertSnapshot('arithmetic', $result);
25 | }
26 |
27 | public function testLogic()
28 | {
29 | $result = render(__DIR__ . '/fixtures/', 'logic.twig', [], $this->twig);
30 | $this->assertSnapshot('logic', $result);
31 | }
32 |
33 | public function testStringConcat()
34 | {
35 | $result = render(__DIR__ . '/fixtures/', 'strConcat.twig', [], $this->twig);
36 | $this->assertSnapshot('strConcat', $result);
37 | }
38 |
39 | public function testFunctionCall()
40 | {
41 | $result = render(__DIR__ . '/fixtures/', 'func.twig', [], $this->twig);
42 | $this->assertSnapshot('func', $result);
43 | }
44 |
45 | public function testFilter()
46 | {
47 | $result = render(__DIR__ . '/fixtures/', 'filter.twig', ['d' => new \DateTimeImmutable('2000-01-01')], $this->twig);
48 | $this->assertSnapshot('filter', $result);
49 | }
50 |
51 | public function testArrayLiteral()
52 | {
53 | $result = render(__DIR__ . '/fixtures/', 'array.twig', [], $this->twig);
54 | $this->assertSnapshot('array', $result);
55 | }
56 |
57 | public function testHashmapLiteral()
58 | {
59 | $result = render(__DIR__ . '/fixtures/', 'assocArray.twig', [], $this->twig);
60 | $this->assertSnapshot('assocArray', $result);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/loader/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod ast;
2 | pub mod expression;
3 | pub mod parser;
4 | use std::{collections::HashMap, fs::File, io::Read, path::PathBuf};
5 |
6 | use self::ast::Content;
7 | pub use self::{
8 | ast::{Extension, Module, Template},
9 | expression::Operator,
10 | parser::{parse, Span},
11 | };
12 |
13 | use anyhow::Result;
14 |
15 | pub struct Loader {
16 | root_dir: PathBuf,
17 | modules: HashMap,
18 | }
19 |
20 | impl Loader {
21 | pub fn new(root: PathBuf) -> Self {
22 | Self {
23 | root_dir: root,
24 | modules: HashMap::default(),
25 | }
26 | }
27 |
28 | pub fn load>(&mut self, template: T) -> Result {
29 | match self.modules.get(template.as_ref()) {
30 | Some(t) => Ok(t.to_owned()),
31 | None => match self.read_file(template.as_ref())? {
32 | Module::Template(mut tpl) => {
33 | tpl = self.load_includes(tpl)?;
34 | self.modules
35 | .insert(template.as_ref().into(), Module::Template(tpl));
36 | Ok(self.modules[template.as_ref()].clone())
37 | }
38 | Module::Extension(ext) => {
39 | self.modules
40 | .insert(template.as_ref().into(), Module::Extension(ext));
41 | Ok(self.modules[template.as_ref()].clone())
42 | }
43 | },
44 | }
45 | }
46 |
47 | fn read_file(&mut self, name: &str) -> Result {
48 | let fpath = self.root_dir.join(name);
49 | let mut file = File::open(fpath)?;
50 |
51 | let mut buf = String::default();
52 | file.read_to_string(&mut buf)?;
53 |
54 | parse(name.to_string(), &buf)
55 | }
56 |
57 | fn load_includes(&mut self, template: Template) -> Result {
58 | let mut replace_fn = Box::new(|content: Content| -> Content {
59 | match content {
60 | Content::Statement(ast::Stmt::Include(name)) => {
61 | match self.read_file(&name).expect("todo!!") {
62 | Module::Template(tpl) => tpl.into_block(),
63 | _ => todo!(),
64 | }
65 | }
66 | _ => content,
67 | }
68 | });
69 |
70 | Ok(template.replace_includes(&mut replace_fn))
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "locked": {
5 | "lastModified": 1667395993,
6 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
7 | "owner": "numtide",
8 | "repo": "flake-utils",
9 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "numtide",
14 | "repo": "flake-utils",
15 | "type": "github"
16 | }
17 | },
18 | "flake-utils_2": {
19 | "locked": {
20 | "lastModified": 1659877975,
21 | "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
22 | "owner": "numtide",
23 | "repo": "flake-utils",
24 | "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
25 | "type": "github"
26 | },
27 | "original": {
28 | "owner": "numtide",
29 | "repo": "flake-utils",
30 | "type": "github"
31 | }
32 | },
33 | "nixpkgs": {
34 | "locked": {
35 | "lastModified": 1668084710,
36 | "narHash": "sha256-DJbxUK6ITJr7jJtY1d+U/VuR6ovUbVBAEj0gXysfi+I=",
37 | "owner": "NixOS",
38 | "repo": "nixpkgs",
39 | "rev": "94cbd0545705d8d062977f3e20295f216de4285b",
40 | "type": "github"
41 | },
42 | "original": {
43 | "owner": "NixOS",
44 | "repo": "nixpkgs",
45 | "type": "github"
46 | }
47 | },
48 | "nixpkgs_2": {
49 | "locked": {
50 | "lastModified": 1665296151,
51 | "narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=",
52 | "owner": "NixOS",
53 | "repo": "nixpkgs",
54 | "rev": "14ccaaedd95a488dd7ae142757884d8e125b3363",
55 | "type": "github"
56 | },
57 | "original": {
58 | "owner": "NixOS",
59 | "ref": "nixpkgs-unstable",
60 | "repo": "nixpkgs",
61 | "type": "github"
62 | }
63 | },
64 | "root": {
65 | "inputs": {
66 | "flake-utils": "flake-utils",
67 | "nixpkgs": "nixpkgs",
68 | "rust-overlay": "rust-overlay"
69 | }
70 | },
71 | "rust-overlay": {
72 | "inputs": {
73 | "flake-utils": "flake-utils_2",
74 | "nixpkgs": "nixpkgs_2"
75 | },
76 | "locked": {
77 | "lastModified": 1668048396,
78 | "narHash": "sha256-SUWQlSa/H5XKPeuF9XmWzmwIJrgK42Lak6/1jBAwyd0=",
79 | "owner": "oxalica",
80 | "repo": "rust-overlay",
81 | "rev": "859fefb532bb957f51a9b5e8e3ba2e48394c9353",
82 | "type": "github"
83 | },
84 | "original": {
85 | "owner": "oxalica",
86 | "repo": "rust-overlay",
87 | "type": "github"
88 | }
89 | }
90 | },
91 | "root": "root",
92 | "version": 7
93 | }
94 |
--------------------------------------------------------------------------------
/src/evaluation/config.rs:
--------------------------------------------------------------------------------
1 | use ext_php_rs::{call_user_func, types::Zval, convert::{IntoZvalDyn, IntoZval}, flags::DataType, ffi::_zval_struct};
2 |
3 | use anyhow::{anyhow, Result};
4 |
5 | use super::{environment::Filter, value::TaggedValue};
6 |
7 | pub struct Config {
8 | twig_env: Zval,
9 | }
10 |
11 | impl Config {
12 | pub fn new(twig_env: Zval) -> Self {
13 | Config { twig_env }
14 | }
15 |
16 | pub fn get_function(&self, name: &str) -> Result {
17 | let funtions = call_user_func!(build_callable(&self.twig_env, "getFunctions"))
18 | .map_err(|e| anyhow::anyhow!("{}", e))?;
19 | let func = if let Some(Some(f)) = funtions.array().map(|a| a.get(name)) {
20 | f
21 | } else {
22 | return Err(anyhow!("function {} not found", name));
23 | };
24 |
25 | call_user_func!(build_callable(func, "getCallable")).map_err(|e| anyhow::anyhow!("{}", e))
26 | }
27 |
28 | pub fn get_filter(&self, name: &str) -> Result {
29 | let funtions = call_user_func!(build_callable(&self.twig_env, "getFilters"))
30 | .map_err(|e| anyhow::anyhow!("{}", e))?;
31 |
32 | let func = if let Some(Some(f)) = funtions.array().map(|a| a.get(name)) {
33 | f
34 | } else {
35 | return Err(anyhow!("function {} not found", name));
36 | };
37 |
38 | let callable = call_user_func!(build_callable(func, "getCallable")).map_err(|e| anyhow::anyhow!("{}", e))?;
39 | let env = ObjAsParamHack{ inner: self.twig_env.shallow_clone() };
40 |
41 | if call_user_func!(build_callable(func, "needsEnvironment")).map_err(|e| anyhow::anyhow!("{}", e))?.bool().unwrap_or_default() {
42 |
43 | Ok(Box::new(move |params: &Vec| -> Result {
44 | let mut z_params: Vec<&dyn IntoZvalDyn> = params.iter().map(|p| p as &dyn IntoZvalDyn).collect();
45 | z_params.insert(0, &env);
46 | callable.try_call(z_params).map(|zv| TaggedValue::Zval(zv)).map_err(|err| anyhow!("{}", err))
47 | }))
48 |
49 | } else {
50 |
51 | Ok(Box::new(move |params: &Vec| -> Result {
52 | callable.try_call(params.iter().map(|p| p as &dyn IntoZvalDyn).collect()).map(|zv| TaggedValue::Zval(zv)).map_err(|err| anyhow!("{}", err))
53 | }))
54 |
55 | }
56 | }
57 | }
58 |
59 | fn build_callable(zv: &Zval, fn_name: &str) -> Zval {
60 | let mut callable = Zval::new();
61 | callable.set_array(vec![
62 | zv.shallow_clone(),
63 | Zval::try_from(fn_name).expect("could not create php string"),
64 | ]);
65 | return callable;
66 | }
67 |
68 | struct ObjAsParamHack{inner: Zval}
69 |
70 | impl Clone for ObjAsParamHack {
71 | fn clone(&self) -> Self {
72 | Self { inner: self.inner.shallow_clone() }
73 | }
74 | }
75 |
76 | impl IntoZval for ObjAsParamHack {
77 | const TYPE: DataType = DataType::Reference;
78 |
79 | fn into_zval(self, _persistend: bool) -> Result<_zval_struct, ext_php_rs::error::Error> {
80 | Ok(self.inner)
81 | }
82 |
83 | fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<(), ext_php_rs::error::Error> {
84 | let Self { inner } = self;
85 | *zv = inner;
86 | Ok(())
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "A Nix-flake-based Rust development environment";
3 |
4 | inputs = {
5 | flake-utils.url = "github:numtide/flake-utils";
6 | nixpkgs.url = "github:NixOS/nixpkgs";
7 | rust-overlay.url = "github:oxalica/rust-overlay";
8 | };
9 |
10 | outputs = {
11 | self,
12 | flake-utils,
13 | nixpkgs,
14 | rust-overlay,
15 | }:
16 | flake-utils.lib.eachDefaultSystem (system: let
17 | overlays = [
18 | (import rust-overlay)
19 | (self: super: {
20 | rustToolchain = let
21 | rust = super.rust-bin;
22 | in
23 | if builtins.pathExists ./rust-toolchain.toml
24 | then rust.fromRustupToolchainFile ./rust-toolchain.toml
25 | else if builtins.pathExists ./rust-toolchain
26 | then rust.fromRustupToolchainFile ./rust-toolchain
27 | else rust.stable.latest.default;
28 | })
29 | ];
30 |
31 | pkgs = import nixpkgs {inherit system overlays;};
32 |
33 | phpunwrapped = pkgs.php81.unwrapped.dev.overrideAttrs (attrs: {
34 | configureFlags = attrs.configureFlags ++ ["--enable-zts"];
35 | preConfigure =
36 | ''
37 | for i in main/build-defs.h.in scripts/php-config.in; do
38 | substituteInPlace $i \
39 | --replace '@CONFIGURE_COMMAND@' '(omitted)' \
40 | --replace '@PHP_LDFLAGS@' ""
41 | done
42 | export EXTENSION_DIR=$out/lib/php/extensions
43 | for i in $(find . -type f -name "*.m4"); do
44 | substituteInPlace $i \
45 | --replace 'test -x "$PKG_CONFIG"' 'type -P "$PKG_CONFIG" >/dev/null'
46 | done
47 | ./buildconf --copy --force
48 | if test -f $src/genfiles; then
49 | ./genfiles
50 | fi
51 | ''
52 | + pkgs.lib.optionalString (system == flake-utils.lib.system.aarch64-darwin) ''
53 | substituteInPlace configure --replace "-lstdc++" "-lc++"
54 | '';
55 | });
56 |
57 | php = phpunwrapped.buildEnv {
58 | extensions = {
59 | enabled,
60 | all,
61 | }:
62 | enabled
63 | ++ (with all; [
64 | redis
65 | pcov
66 | dom
67 | mbstring
68 | tokenizer
69 | xmlwriter
70 | xmlreader
71 | ]);
72 | extraConfig = "memory_limit = -1";
73 | };
74 | in {
75 | devShells.default = pkgs.mkShell {
76 | LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
77 |
78 | nativeBuildInputs = with pkgs; [
79 | php
80 | php81Packages.composer
81 | phpunwrapped
82 | stdenv.cc.libc
83 | clang
84 | rustToolchain
85 | openssl
86 | pkg-config
87 | cargo-deny
88 | cargo-edit
89 | cargo-watch
90 | rust-analyzer
91 | ];
92 |
93 | buildInputs = with pkgs; [
94 | php
95 | phpunwrapped
96 | stdenv.cc.libc
97 | clang
98 | ];
99 |
100 | shellHook = ''
101 | ${pkgs.rustToolchain}/bin/cargo --version
102 | '';
103 | };
104 | });
105 | }
106 |
--------------------------------------------------------------------------------
/src/evaluation/value.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 |
3 | use ext_php_rs::{
4 | convert::{FromZval, IntoZval},
5 | flags::DataType,
6 | types::Zval,
7 | };
8 | use rust_decimal::{prelude::FromPrimitive, Decimal};
9 | #[derive(Debug)]
10 | pub enum TaggedValue {
11 | Str(String),
12 | Zval(Zval),
13 | Usize(u64),
14 | Number(i64),
15 | Float(f64),
16 | Bool(bool),
17 | }
18 |
19 | impl Display for TaggedValue {
20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 | match self {
22 | Self::Str(s) => write!(f, "{}", &s),
23 | Self::Usize(us) => write!(f, "{}", us),
24 | Self::Number(n) => write!(f, "{}", n),
25 | Self::Bool(true) => write!(f, "{}", 1),
26 | Self::Bool(false) => write!(f, ""),
27 | Self::Float(fl) => {
28 | if let Some(dec) = Decimal::from_f64(*fl) {
29 | write!(f, "{}", dec.round_dp(6).normalize())
30 | } else {
31 | write!(f, "{}", fl)
32 | }
33 | }
34 | Self::Zval(zv) => match zv {
35 | val if val.is_long() => write!(f, "{}", val.long().unwrap()),
36 | val if val.is_double() => write!(f, "{}", val.double().unwrap()),
37 | val if val.is_string() => write!(f, "{}", val.str().unwrap()),
38 | _ => write!(f, "{}", zv.str().unwrap_or("")),
39 | },
40 | }
41 | }
42 | }
43 |
44 | impl Clone for TaggedValue {
45 | fn clone(&self) -> Self {
46 | match self {
47 | Self::Str(s) => Self::Str(s.clone()),
48 | Self::Usize(u) => Self::Usize(*u),
49 | Self::Number(n) => Self::Number(*n),
50 | Self::Float(f) => Self::Float(*f),
51 | Self::Bool(b) => Self::Bool(*b),
52 | Self::Zval(zv) => Self::Zval(zv.shallow_clone()),
53 | }
54 | }
55 | }
56 |
57 | impl Default for TaggedValue {
58 | fn default() -> Self {
59 | Self::Str(String::default())
60 | }
61 | }
62 |
63 | impl From<&str> for TaggedValue {
64 | fn from(s: &str) -> Self {
65 | TaggedValue::Str(s.to_string())
66 | }
67 | }
68 |
69 | impl From for TaggedValue {
70 | fn from(s: String) -> Self {
71 | TaggedValue::Str(s)
72 | }
73 | }
74 |
75 | impl From for TaggedValue {
76 | fn from(u: u64) -> Self {
77 | TaggedValue::Usize(u)
78 | }
79 | }
80 |
81 | impl From for TaggedValue {
82 | fn from(b: bool) -> Self {
83 | TaggedValue::Bool(b)
84 | }
85 | }
86 |
87 | impl FromZval<'_> for TaggedValue {
88 | const TYPE: ext_php_rs::flags::DataType = DataType::Mixed;
89 | fn from_zval(zval: &Zval) -> Option {
90 | Some(TaggedValue::Zval(zval.shallow_clone()))
91 | }
92 | }
93 |
94 | impl IntoZval for TaggedValue {
95 | const TYPE: DataType = DataType::Mixed;
96 |
97 | fn set_zval(self, zv: &mut Zval, persistent: bool) -> ext_php_rs::error::Result<()> {
98 | match self {
99 | Self::Str(s) => zv.set_string(&s, persistent)?,
100 | Self::Number(num) => zv.set_long(num),
101 | Self::Usize(num) => todo!("usize as long not yet possible"),
102 | Self::Bool(b) => zv.set_bool(b),
103 | Self::Float(f) => zv.set_double(f),
104 | Self::Zval(inner) => *zv = inner,
105 | };
106 | Ok(())
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/evaluation/environment.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use ext_php_rs::{convert::FromZval, types::Zval};
4 |
5 | use crate::loader::{ast::Setter, Loader, Module};
6 |
7 | use anyhow::{anyhow, Result};
8 |
9 | use super::{config::Config, expressions::Evaluate, value::TaggedValue};
10 |
11 | pub struct Env {
12 | globals: Zval,
13 | stack: Vec,
14 | loader: Loader,
15 | config: Config,
16 | }
17 |
18 | type Scope = HashMap;
19 |
20 | pub type Filter = Box) -> Result>;
21 |
22 | impl Env {
23 | pub fn new(globals: Zval, loader: Loader, config: Config) -> Self {
24 | Self {
25 | globals,
26 | stack: vec![Scope::default()],
27 | loader,
28 | config,
29 | }
30 | }
31 |
32 | pub fn get_twig_function(&self, name: &str) -> Result {
33 | self.config.get_function(name)
34 | }
35 |
36 | pub fn get_twig_filter(&self, name: &str) -> Result {
37 | self.config.get_filter(name)
38 | }
39 |
40 | pub fn load_file>(&mut self, file: T) -> Result {
41 | self.loader.load(file)
42 | }
43 |
44 | pub fn enter_new_scope(mut self) -> Self {
45 | self.stack.push(Scope::default());
46 | self
47 | }
48 | pub fn exit_scope(mut self) -> Self {
49 | self.stack.pop();
50 | self
51 | }
52 |
53 | pub fn set(&mut self, name: &str, val: TaggedValue) {
54 | let scope = self.get_scope(name);
55 | scope.insert(name.to_string(), val);
56 | }
57 |
58 | pub fn apply_setter(&mut self, setter: &Setter) {
59 | self.set(
60 | &setter.target,
61 | setter.value.eval(self).expect("fix error case"),
62 | )
63 | }
64 |
65 | pub fn get(&self, accessor: &str) -> Result {
66 | if accessor.is_empty() {
67 | return Err(anyhow!("empty varname"));
68 | }
69 |
70 | if let Some(val) = self.get_from_scope(accessor) {
71 | return Ok(val);
72 | }
73 |
74 | match Self::get_rec(&self.globals, accessor) {
75 | Some(zv) => Ok(TaggedValue::Zval(zv.shallow_clone())),
76 | None => Err(anyhow!("variable {} was not found", accessor)),
77 | }
78 | }
79 |
80 | fn get_from_scope(&self, accessor: &str) -> Option {
81 | let (key, rest) = if accessor.contains('.') {
82 | accessor.split_once('.').unwrap()
83 | } else {
84 | (accessor, "")
85 | };
86 |
87 | for scope in self.stack.iter().rev() {
88 | if let Some(val) = scope.get(key) {
89 | return match val {
90 | TaggedValue::Zval(zv) => {
91 | Self::get_rec(&zv, rest).and_then(TaggedValue::from_zval)
92 | }
93 | _ => Some(val.clone()),
94 | };
95 | }
96 | }
97 | None
98 | }
99 |
100 | fn get_scope<'env>(&'env mut self, accessor: &'_ str) -> &'env mut Scope {
101 | let key = accessor.split_once('.').map(|(k, _)| k).unwrap_or(accessor);
102 |
103 | let mut idx = self.stack.len() - 1;
104 | for (i, scope) in self.stack.iter().enumerate().rev() {
105 | if scope.contains_key(key) {
106 | idx = i;
107 | break;
108 | }
109 | }
110 | self.stack
111 | .get_mut(idx)
112 | .expect("env should always contain 1 scope")
113 | }
114 |
115 | fn get_rec<'a>(val: &'a Zval, accessor: &'_ str) -> Option<&'a Zval> {
116 | if accessor.is_empty() {
117 | return Some(val);
118 | }
119 | let (key, rest) = if accessor.contains('.') {
120 | accessor.split_once('.').unwrap()
121 | } else {
122 | (accessor, "")
123 | };
124 |
125 | if val.is_array() {
126 | let array = val.array()?;
127 | return Self::get_rec(array.get(key)?, rest);
128 | }
129 |
130 | if val.is_object() {
131 | let obj = val.object()?;
132 | return Self::get_rec(obj.get_property(key).ok()?, rest);
133 | }
134 | None
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/evaluation/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod config;
2 | pub mod environment;
3 | mod expressions;
4 | mod value;
5 | use std::{collections::HashMap, fmt::Write};
6 |
7 | use ext_php_rs::convert::FromZval;
8 |
9 | use crate::{
10 | evaluation::expressions::Evaluate,
11 | loader::{
12 | ast::{Block, BlockType, Content, Contents, IterationType, Stmt, Template},
13 | expression::ast::Expression,
14 | Extension, Module,
15 | },
16 | };
17 |
18 | use anyhow::{anyhow, Context, Result};
19 |
20 | use self::environment::Env;
21 | use self::value::TaggedValue;
22 |
23 | pub fn render(mut tpl: Module, mut env: Env) -> Result {
24 | let mut block_extensions: HashMap> = HashMap::default();
25 |
26 | while let Module::Extension(Extension { parent, blocks, .. }) = tpl {
27 | for (name, block) in blocks.into_iter() {
28 | match block_extensions.get_mut(&name) {
29 | None => {
30 | block_extensions.insert(name, block);
31 | }
32 | Some(child_block) => child_block.set_parents(block),
33 | }
34 | }
35 | tpl = env.load_file(parent)?;
36 | }
37 |
38 | match tpl {
39 | Module::Template(mut base) => {
40 | let mut out_buf = String::default();
41 | base.apply_extensions(block_extensions);
42 | base.render(&mut out_buf, env)?;
43 | Ok(out_buf)
44 | }
45 | _ => unreachable!(),
46 | }
47 | }
48 |
49 | trait Renderable {
50 | fn render(&self, out: &mut T, env: Env) -> Result;
51 | }
52 |
53 | impl Renderable for Template {
54 | fn render(&self, out: &mut T, env: Env) -> Result {
55 | self.content.render(out, env)
56 | }
57 | }
58 |
59 | impl Renderable for Contents {
60 | fn render(&self, out: &mut T, env: Env) -> Result {
61 | let mut env = env;
62 | for c in self.iter() {
63 | env = c.render(out, env)?
64 | }
65 | Ok(env)
66 | }
67 | }
68 |
69 | impl Renderable for Content {
70 | fn render(&self, out: &mut T, mut env: Env) -> Result {
71 | match self {
72 | Content::Text(str) => {
73 | write!(out, "{}", str)?;
74 | Ok(env)
75 | }
76 | Content::Print(expr) => expr.render(out, env),
77 | Content::Block(block) => block.render(out, env),
78 | Content::Statement(Stmt::Set(setter)) => {
79 | env.apply_setter(setter);
80 | Ok(env)
81 | }
82 | Content::Statement(Setter) => Ok(env),
83 | }
84 | }
85 | }
86 |
87 | impl Renderable for Expression {
88 | fn render(&self, out: &mut T, env: Env) -> Result {
89 | write!(out, "{}", self.eval(&env)?)?;
90 | Ok(env)
91 | }
92 | }
93 |
94 | impl Renderable for Block {
95 | fn render(&self, out: &mut T, env: Env) -> Result {
96 | let mut env = env.enter_new_scope();
97 | match &self.typ {
98 | BlockType::BlockName(_) => self.contents.render(out, env).map(Env::exit_scope),
99 | BlockType::Loop(l) => {
100 | let zv = if let TaggedValue::Zval(zv) = env.get(&l.iterator)? {
101 | zv
102 | } else {
103 | return Err(anyhow!("variable {} is not iterable", &l.iterator));
104 | };
105 | let collection = zv
106 | .array()
107 | .with_context(|| format!("variable {}, is not iterable", &l.iterator))?;
108 |
109 | for (idx, key, val) in collection.iter() {
110 | match &l.typ {
111 | IterationType::SingleVal(name) => {
112 | env.set(name, TaggedValue::from_zval(val).expect("php vm broke"))
113 | }
114 | IterationType::KeyVal((kname, vname)) => {
115 | env.set(kname, key.map_or_else(|| idx.into(), TaggedValue::from));
116 | env.set(vname, TaggedValue::from_zval(val).expect("php vm broke"));
117 | }
118 | };
119 |
120 | env = self.contents.render(out, env)?
121 | }
122 | Ok(env)
123 | }
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/loader/ast.rs:
--------------------------------------------------------------------------------
1 | use std::{cell::RefCell, collections::HashMap, rc::Rc};
2 |
3 | use super::expression::ast::Expression;
4 |
5 | #[derive(Debug, PartialEq, Clone)]
6 | pub enum Module {
7 | Template(Template),
8 | Extension(Extension),
9 | }
10 |
11 | #[derive(Debug, PartialEq, Clone)]
12 | pub struct Template {
13 | pub name: String,
14 | pub content: Contents,
15 | }
16 |
17 | #[derive(Debug, PartialEq, Clone)]
18 | pub struct Extension {
19 | pub name: String,
20 | pub parent: String,
21 | pub blocks: HashMap>,
22 | }
23 |
24 | pub type Contents = Vec;
25 |
26 | #[derive(Debug, PartialEq, Clone)]
27 | pub enum Content {
28 | Text(String),
29 | Print(Expression),
30 | Block(Box),
31 | Statement(Stmt),
32 | }
33 |
34 | #[derive(Debug, PartialEq, Clone)]
35 | pub struct Block {
36 | pub typ: BlockType,
37 | pub contents: Contents,
38 | }
39 |
40 | #[derive(Debug, PartialEq, Clone)]
41 | pub enum BlockType {
42 | BlockName(String),
43 | Loop(Loop),
44 | }
45 |
46 | #[derive(Debug, PartialEq, Clone)]
47 | pub struct Loop {
48 | pub typ: IterationType,
49 | pub iterator: String,
50 | }
51 |
52 | #[derive(Debug, PartialEq, Clone)]
53 | pub enum IterationType {
54 | SingleVal(String),
55 | KeyVal((String, String)),
56 | }
57 |
58 | #[derive(Debug, PartialEq, Clone)]
59 | pub enum Stmt {
60 | Set(Setter),
61 | Include(String),
62 | }
63 |
64 | #[derive(Debug, PartialEq, Clone)]
65 | pub struct Setter {
66 | pub target: String,
67 | pub value: Expression,
68 | }
69 |
70 | impl Template {
71 | pub fn replace_includes(mut self, replace: &mut dyn FnMut(Content) -> Content) -> Template {
72 | self.content = self
73 | .content
74 | .into_iter()
75 | .map(|c| replace_includes(c, replace))
76 | .collect();
77 | self
78 | }
79 |
80 | pub fn into_block(self) -> Content {
81 | let Self { name, content } = self;
82 | Content::Block(Box::new(Block {
83 | typ: BlockType::BlockName(name),
84 | contents: content,
85 | }))
86 | }
87 |
88 | pub fn apply_extensions(&mut self, mut extensions: HashMap>) {
89 | extend_blocks(&mut self.content, &mut extensions);
90 | }
91 | }
92 |
93 | fn replace_includes(content: Content, replace: &mut dyn FnMut(Content) -> Content) -> Content {
94 | match content {
95 | Content::Statement(Stmt::Include(_)) => replace(content),
96 | Content::Block(mut block) => {
97 | block.contents = block
98 | .contents
99 | .into_iter()
100 | .map(|c| replace_includes(c, replace))
101 | .collect();
102 | Content::Block(block)
103 | }
104 | _ => content,
105 | }
106 | }
107 |
108 | fn extend_blocks(content: &mut Contents, extensions: &mut HashMap>) {
109 | for elem in content.iter_mut() {
110 | if let Content::Block(ref mut base) = elem {
111 | if let Some(child) = base.get_name().and_then(|name| extensions.remove(name)) {
112 | let parent = std::mem::replace(base, child);
113 | base.set_parents(parent)
114 | }
115 | extend_blocks(&mut base.contents, extensions);
116 | }
117 | }
118 | }
119 |
120 | impl Block {
121 | pub fn get_name(&self) -> Option<&str> {
122 | match &self.typ {
123 | BlockType::BlockName(name) => Some(name),
124 | _ => None,
125 | }
126 | }
127 |
128 | pub fn set_parents(&mut self, parent: Box) {
129 | for elem in self.contents.iter_mut() {
130 | match elem {
131 | Content::Print(Expression::Parent) => *elem = Content::Block(parent.clone()),
132 | Content::Block(block) => block.set_parents(parent.clone()),
133 | _ => (),
134 | }
135 | }
136 | }
137 | }
138 |
139 | pub fn get_blocks(
140 | content: Contents,
141 | mut blocks: HashMap>,
142 | ) -> HashMap> {
143 | for elem in content.into_iter() {
144 | match elem {
145 | Content::Block(block) if block.get_name().is_some() => {
146 | blocks.insert(block.get_name().unwrap().to_string(), block);
147 | }
148 | _ => (),
149 | };
150 | }
151 | blocks
152 | }
153 |
--------------------------------------------------------------------------------
/src/evaluation/expressions.rs:
--------------------------------------------------------------------------------
1 | use crate::loader::{expression::ast::Expression, Operator};
2 |
3 | use super::{
4 | environment::Env,
5 | value::{self, TaggedValue},
6 | };
7 |
8 | use anyhow::{anyhow, Result};
9 | use ext_php_rs::{
10 | call_user_func,
11 | convert::{IntoZval, IntoZvalDyn},
12 | types::{ZendHashTable, Zval}
13 | };
14 | use std::fmt::Write;
15 |
16 | pub trait Evaluate {
17 | fn eval(&self, env: &Env) -> Result;
18 | }
19 |
20 | trait Apply {
21 | fn apply(&self, params: Vec) -> Result;
22 | }
23 |
24 | impl Evaluate for Expression {
25 | fn eval(&self, env: &Env) -> Result {
26 | match self {
27 | Expression::Var(name) => Ok(env.get(name).unwrap_or_default()),
28 | Expression::Str(s) => Ok(TaggedValue::Str(s.to_string())),
29 | Expression::Number(n) => Ok(TaggedValue::Number(*n)),
30 | Expression::Float(f) => Ok(TaggedValue::Float(*f)),
31 | Expression::Bool(b) => Ok(TaggedValue::Bool(*b)),
32 |
33 | Expression::Term(term) => {
34 | let params: Result> =
35 | term.params.iter().map(|p| p.eval(env)).collect();
36 | term.op.apply(params?)
37 | }
38 |
39 | Expression::Array(exprs) => {
40 | let mut arr = ZendHashTable::new();
41 | for expr in exprs {
42 | arr.push(expr.eval(env)?).map_err(|err| anyhow!("{:?}", err))?;
43 | }
44 | Ok(TaggedValue::Zval(arr.as_zval(false).map_err(|err| anyhow!("{:?}", err))?))
45 | }
46 |
47 | Expression::HashMap(elements) => {
48 | let mut arr = ZendHashTable::new();
49 | for kv_pair in elements {
50 | arr.insert(&kv_pair.key.eval(env)?.to_string(), kv_pair.val.eval(env)?).map_err(|err| anyhow!("{:?}", err))?;
51 | }
52 | Ok(TaggedValue::Zval(arr.as_zval(false).map_err(|err| anyhow!("{:?}", err))?))
53 | },
54 |
55 | Expression::FuncCall(fc) => {
56 | let f = env.get_twig_function(&fc.name)?;
57 |
58 | let params: Vec = fc
59 | .params
60 | .iter()
61 | .map(|p| p.eval(env))
62 | .collect::>>()?;
63 | f.try_call(params.iter().map(|p| p as &dyn IntoZvalDyn).collect())
64 | .map(|zv| TaggedValue::Zval(zv))
65 | .map_err(|err| anyhow!("{}", err))
66 | }
67 |
68 | Expression::FilterCall(fc) => {
69 | let filter = env.get_twig_filter(&fc.name)?;
70 |
71 | let params: Vec = fc
72 | .params
73 | .iter()
74 | .map(|p| p.eval(env))
75 | .collect::>>()?;
76 |
77 | filter(¶ms)
78 | }
79 |
80 | _ => todo!("implement me: {:?}", self),
81 | }
82 | }
83 | }
84 |
85 | impl Apply for Operator {
86 | fn apply(&self, params: Vec) -> Result {
87 | match self {
88 | Self::Add => add(¶ms),
89 | Self::Mul => mul(¶ms),
90 | Self::Div => div(¶ms),
91 | Self::Divi => divi(¶ms),
92 | Self::And => and(¶ms),
93 | Self::Or => or(¶ms),
94 | Self::Not => not(¶ms),
95 | Self::StrConcat => str_concat(¶ms),
96 | _ => Err(anyhow!("missing apply for operator: {:?}", self)),
97 | }
98 | }
99 | }
100 |
101 | fn add(params: &[TaggedValue]) -> Result {
102 | match params {
103 | [TaggedValue::Number(lhs), TaggedValue::Number(rhs)] => Ok(TaggedValue::Number(lhs + rhs)),
104 | [TaggedValue::Float(lhs), TaggedValue::Number(rhs)] => {
105 | Ok(TaggedValue::Float(lhs + *rhs as f64))
106 | }
107 | [TaggedValue::Number(lhs), TaggedValue::Float(rhs)] => {
108 | Ok(TaggedValue::Float(*lhs as f64 + rhs))
109 | }
110 | [TaggedValue::Float(lhs), TaggedValue::Float(rhs)] => Ok(TaggedValue::Float(lhs + rhs)),
111 | _ => Err(anyhow!("add not implemented for {:?}", params)),
112 | }
113 | }
114 |
115 | fn mul(params: &[TaggedValue]) -> Result {
116 | match params {
117 | [TaggedValue::Number(lhs), TaggedValue::Number(rhs)] => Ok(TaggedValue::Number(lhs * rhs)),
118 | [TaggedValue::Float(lhs), TaggedValue::Number(rhs)] => {
119 | Ok(TaggedValue::Float(lhs * *rhs as f64))
120 | }
121 | [TaggedValue::Number(lhs), TaggedValue::Float(rhs)] => {
122 | Ok(TaggedValue::Float(*lhs as f64 * rhs))
123 | }
124 | [TaggedValue::Float(lhs), TaggedValue::Float(rhs)] => Ok(TaggedValue::Float(lhs * rhs)),
125 | _ => Err(anyhow!("add not implemented for {:?}", params)),
126 | }
127 | }
128 |
129 | fn div(params: &[TaggedValue]) -> Result {
130 | match params {
131 | [TaggedValue::Number(lhs), TaggedValue::Number(rhs)] => {
132 | Ok(TaggedValue::Float(*lhs as f64 / *rhs as f64))
133 | }
134 | [TaggedValue::Float(lhs), TaggedValue::Number(rhs)] => {
135 | Ok(TaggedValue::Float(lhs / *rhs as f64))
136 | }
137 | [TaggedValue::Number(lhs), TaggedValue::Float(rhs)] => {
138 | Ok(TaggedValue::Float(*lhs as f64 / rhs))
139 | }
140 | [TaggedValue::Float(lhs), TaggedValue::Float(rhs)] => Ok(TaggedValue::Float(lhs / rhs)),
141 | _ => Err(anyhow!("add not implemented for {:?}", params)),
142 | }
143 | }
144 |
145 | fn divi(params: &[TaggedValue]) -> Result {
146 | match params {
147 | [TaggedValue::Number(lhs), TaggedValue::Number(rhs)] => Ok(TaggedValue::Number(lhs / rhs)),
148 | _ => Err(anyhow!("add not implemented for {:?}", params)),
149 | }
150 | }
151 |
152 | fn and(params: &[TaggedValue]) -> Result {
153 | match params {
154 | [TaggedValue::Bool(lhs), TaggedValue::Bool(rhs)] => Ok(TaggedValue::Bool(*lhs && *rhs)),
155 | _ => Err(anyhow!("add not implemented for {:?}", params)),
156 | }
157 | }
158 |
159 | fn or(params: &[TaggedValue]) -> Result {
160 | match params {
161 | [TaggedValue::Bool(lhs), TaggedValue::Bool(rhs)] => Ok(TaggedValue::Bool(*lhs || *rhs)),
162 | _ => Err(anyhow!("add not implemented for {:?}", params)),
163 | }
164 | }
165 |
166 | fn not(params: &[TaggedValue]) -> Result {
167 | match params {
168 | [TaggedValue::Bool(b)] => Ok(TaggedValue::Bool(*b == false)),
169 | _ => Err(anyhow!("add not implemented for {:?}", params)),
170 | }
171 | }
172 |
173 | fn str_concat(params: &[TaggedValue]) -> Result {
174 | let mut buf = String::default();
175 | match params {
176 | [lhs, rhs] => {
177 | write!(buf, "{}", lhs);
178 | write!(buf, "{}", rhs);
179 | Ok(())
180 | }
181 | _ => Err(anyhow!("add not implemented for {:?}", params)),
182 | }?;
183 | Ok(TaggedValue::Str(buf))
184 | }
185 |
--------------------------------------------------------------------------------
/src/loader/parser.rs:
--------------------------------------------------------------------------------
1 | use super::{
2 | ast::{
3 | get_blocks, Block, BlockType, Content, Extension, IterationType, Loop, Module, Setter,
4 | Stmt, Template,
5 | },
6 | expression,
7 | };
8 |
9 | use std::collections::HashMap;
10 |
11 | use anyhow::{anyhow, Result};
12 | use nom::{
13 | branch::alt,
14 | bytes::complete::{tag, take_till, take_until, take_while, take_while1},
15 | character::complete::{line_ending, multispace0, multispace1, space0},
16 | combinator::{eof, opt},
17 | multi::many_till,
18 | sequence::{delimited, tuple},
19 | IResult,
20 | };
21 |
22 | use nom_locate::LocatedSpan;
23 | pub type Span<'a> = LocatedSpan<&'a str>;
24 |
25 | pub fn parse(name: String, input: &str) -> Result {
26 | let input = Span::new(input);
27 | if let Ok((rest, parent)) = parse_extends(input) {
28 | match parse_contents(rest) {
29 | Ok((_, content)) => {
30 | let ext = Extension {
31 | name,
32 | parent,
33 | blocks: get_blocks(content, HashMap::default()),
34 | };
35 | Ok(Module::Extension(ext))
36 | }
37 | Err(err) => Err(anyhow!("error parsing {}: {}", name, err)),
38 | }
39 | } else {
40 | match parse_contents(input) {
41 | Ok((_, content)) => Ok(Module::Template(Template { name, content })),
42 | Err(err) => Err(anyhow!("error parsing {}: {}", name, err)),
43 | }
44 | }
45 | }
46 |
47 | fn parse_extends(i: Span) -> IResult {
48 | let (rest, (.., parent)) = delimited(
49 | parse_block_tag_l,
50 | tuple((tag("extends"), multispace1, parse_quoted)),
51 | parse_block_tag_r,
52 | )(i)?;
53 | Ok((rest, parent.to_string()))
54 | }
55 |
56 | fn parse_block_tag_l(i: Span) -> IResult {
57 | let (rest, _) = tuple((space0, tag("{%"), multispace1))(i)?;
58 | Ok((rest, ()))
59 | }
60 |
61 | fn parse_block_tag_r(i: Span) -> IResult {
62 | let (rest, _) = tuple((multispace0, tag("%}"), opt(line_ending)))(i)?;
63 | Ok((rest, ()))
64 | }
65 |
66 | fn parse_quoted(i: Span) -> IResult {
67 | alt((
68 | delimited(
69 | nom::character::complete::char('\''),
70 | take_while(|c| c != '\''),
71 | nom::character::complete::char('\''),
72 | ),
73 | delimited(
74 | nom::character::complete::char('"'),
75 | take_while(|c| c != '"'),
76 | nom::character::complete::char('"'),
77 | ),
78 | ))(i)
79 | }
80 |
81 | fn parse_contents(i: Span) -> IResult> {
82 | let (_, (contents, _)) = many_till(parse_content, eof)(i)?;
83 | Ok((Span::new(""), contents))
84 | }
85 |
86 | fn parse_content(i: Span) -> IResult {
87 | alt((parse_print, parse_statement, parse_block, parse_text))(i)
88 | }
89 |
90 | fn parse_text(i: Span) -> IResult {
91 | let (rest, text) = take_while1(|c| c != '{')(i)?;
92 | Ok((rest, Content::Text(text.to_string())))
93 | }
94 |
95 | fn parse_print(i: Span) -> IResult {
96 | let (rest, expr) = delimited(parse_print_tag_l, take_until("}}"), parse_print_tag_r)(i)?;
97 | let (_, expr) = expression::parse(expr)?;
98 | Ok((rest, Content::Print(expr)))
99 | }
100 |
101 | fn parse_print_tag_l(i: Span) -> IResult {
102 | let (rest, _) = tuple((tag("{{"), multispace1))(i)?;
103 | Ok((rest, ()))
104 | }
105 |
106 | fn parse_print_tag_r(i: Span) -> IResult {
107 | let (rest, _) = tuple((multispace0, tag("}}")))(i)?;
108 | Ok((rest, ()))
109 | }
110 |
111 | fn parse_statement(i: Span) -> IResult {
112 | let (rest, statement) = delimited(
113 | parse_block_tag_l,
114 | alt((parse_set_statement, parse_include_statement)),
115 | parse_block_tag_r,
116 | )(i)?;
117 | Ok((rest, Content::Statement(statement)))
118 | }
119 |
120 | fn parse_set_statement(i: Span) -> IResult {
121 | let (rest, (.., target, _, _, expr)) = tuple((
122 | tag("set"),
123 | multispace1,
124 | take_till(|c| c == '='),
125 | nom::character::complete::char('='),
126 | multispace0,
127 | take_until("%}"),
128 | ))(i)?;
129 | let (_, expr) = expression::parse(expr)?;
130 | Ok((
131 | rest,
132 | Stmt::Set(Setter {
133 | target: target.trim().to_string(),
134 | value: expr,
135 | }),
136 | ))
137 | }
138 |
139 | fn parse_include_statement(i: Span) -> IResult {
140 | let (rest, (.., target)) = tuple((tag("include"), multispace1, parse_quoted))(i)?;
141 | Ok((rest, Stmt::Include(target.to_string())))
142 | }
143 |
144 | fn parse_block(i: Span) -> IResult {
145 | let (rest, typ) = parse_block_type(i)?;
146 | match typ {
147 | BlockType::BlockName(_) => {
148 | let (rest, (contents, _)) = many_till(
149 | parse_content,
150 | tuple((tag("{% endblock %}"), opt(line_ending))),
151 | )(rest)?;
152 | Ok((rest, Content::Block(Box::new(Block { typ, contents }))))
153 | }
154 | BlockType::Loop(_) => {
155 | let (rest, (contents, _)) = many_till(
156 | parse_content,
157 | tuple((tag("{% endfor %}"), opt(line_ending))),
158 | )(rest)?;
159 | Ok((rest, Content::Block(Box::new(Block { typ, contents }))))
160 | }
161 | }
162 | }
163 |
164 | fn parse_block_type(i: Span) -> IResult {
165 | delimited(
166 | parse_block_tag_l,
167 | alt((parse_block_name, parse_loop)),
168 | parse_block_tag_r,
169 | )(i)
170 | }
171 |
172 | fn parse_block_name(i: Span) -> IResult {
173 | let (rest, (.., name)) = tuple((tag("block"), multispace1, (take_till(|c| c == ' '))))(i)?;
174 | Ok((rest, BlockType::BlockName(name.to_string())))
175 | }
176 |
177 | fn parse_loop(i: Span) -> IResult {
178 | let (rest, (.., iter_type, _, _, iterator)) = tuple((
179 | tag("for"),
180 | multispace1,
181 | alt((parse_key_value, parse_single_var)),
182 | tag("in"),
183 | multispace1,
184 | take_till(|c| c == ' '),
185 | ))(i)?;
186 | Ok((
187 | rest,
188 | BlockType::Loop(Loop {
189 | typ: iter_type,
190 | iterator: iterator.to_string(),
191 | }),
192 | ))
193 | }
194 |
195 | fn parse_single_var(i: Span) -> IResult {
196 | let (rest, varname) = take_until("in")(i)?;
197 | Ok((rest, IterationType::SingleVal(varname.trim().to_string())))
198 | }
199 |
200 | fn parse_key_value(i: Span) -> IResult {
201 | let (rest, (keyname, .., valname)) = tuple((
202 | take_till(|c| c == ',' || c == '%'),
203 | nom::character::complete::char(','),
204 | multispace0,
205 | take_until("in"),
206 | ))(i)?;
207 | Ok((
208 | rest,
209 | IterationType::KeyVal((keyname.trim().to_string(), valname.trim().to_string())),
210 | ))
211 | }
212 |
213 | #[cfg(test)]
214 | mod tests {
215 | use super::*;
216 | use pretty_assertions::assert_eq;
217 |
218 | #[test]
219 | fn test_parse_quotes() {
220 | let single = Span::new(r#"'foo'"#);
221 | let double = Span::new(r#""foo""#);
222 | assert_eq!(unspan_twice(parse_quoted(single)), ("", "foo"));
223 | assert_eq!(unspan_twice(parse_quoted(double)), ("", "foo"));
224 | }
225 |
226 | #[test]
227 | fn test_parse_extends() {
228 | let extends = Span::new("{% extends 'parent.html.twig' %}");
229 | assert_eq!(
230 | unspan(parse_extends(extends)),
231 | ("", "parent.html.twig".to_string())
232 | )
233 | }
234 |
235 | #[test]
236 | fn test_parse_text() {
237 | let input = Span::new(r#"first{# comment #}"#);
238 | assert_eq!(
239 | unspan(parse_text(input)),
240 | ("{# comment #}", Content::Text("first".to_string()))
241 | )
242 | }
243 |
244 | fn unspan(span: IResult) -> (&str, O) {
245 | let (rest, out) = span.unwrap();
246 | (rest.fragment(), out)
247 | }
248 |
249 | fn unspan_twice<'a, 'b>(span: IResult, Span<'b>>) -> (&'a str, &'b str) {
250 | let (rest, out) = span.unwrap();
251 | (rest.fragment(), out.fragment())
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/loader/expression/parser.rs:
--------------------------------------------------------------------------------
1 | use std::collections::VecDeque;
2 |
3 | use nom::{combinator::map_res, IResult};
4 |
5 | use anyhow::{anyhow, Result};
6 |
7 | use crate::loader::{expression::ast::FuncCall, Span};
8 |
9 | use super::{
10 | ast::{Expression, Term, KeyValuePair},
11 | lexer::{lex_exprs, Token},
12 | };
13 |
14 | #[derive(Debug, PartialEq, Clone, Copy)]
15 | pub enum Operator {
16 | Ternary,
17 | BAnd,
18 | BOr,
19 | BXor,
20 | Or,
21 | And,
22 | Eq,
23 | Neq,
24 | Starship,
25 | Lt,
26 | Gt,
27 | Gte,
28 | Lte,
29 | In,
30 | Matches,
31 | StartsWith,
32 | EndsWith,
33 | Range,
34 | Add,
35 | Sub,
36 | StrConcat,
37 | Mul,
38 | Div,
39 | Divi,
40 | Modulo,
41 | Is,
42 | Exp,
43 | NullCoal,
44 | Filter,
45 | ArrayIndex,
46 | Get,
47 | Not,
48 | }
49 |
50 | pub fn parse(input: Span) -> IResult {
51 | map_res(lex_exprs, parse_to_expression)(input)
52 | }
53 |
54 | pub fn parse_to_expression(tokens: Vec) -> Result {
55 | let mut tokens = VecDeque::from(tokens);
56 | parse_rec(&mut tokens, 0)
57 | }
58 |
59 | // see https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html
60 | fn parse_rec(tokens: &mut VecDeque, min_bp: u8) -> Result {
61 | let lhs = tokens.pop_front().unwrap_or(Token::Null);
62 |
63 | let mut lhs = match lhs {
64 | Token::Null => {
65 | return Ok(Expression::Null);
66 | }
67 | Token::Parens(par_tokens) => parse_to_expression(par_tokens)?,
68 | Token::Float(f) => Expression::Float(f),
69 | Token::Number(n) => Expression::Number(n),
70 | Token::Parent() => Expression::Parent,
71 | Token::Str(s) => Expression::Str(s),
72 | Token::Var(v) => Expression::Var(v),
73 | Token::Bool(b) => Expression::Bool(b),
74 |
75 | Token::Array(toks) => Expression::Array(toks
76 | .into_iter()
77 | .map(parse_to_expression)
78 | .collect::>>()?),
79 |
80 | Token::HashMap(kvs) => Expression::HashMap(kvs.into_iter().map(|kv_pair| -> Result {
81 | Ok(KeyValuePair{
82 | key: parse_to_expression(kv_pair.key)?,
83 | val: parse_to_expression(kv_pair.value)?
84 | })
85 | }).collect::>>()?),
86 |
87 | Token::FuncCall(fc) => Expression::FuncCall(FuncCall {
88 | name: fc.name,
89 | params: fc
90 | .params
91 | .into_iter()
92 | .map(parse_to_expression)
93 | .collect::>>()?,
94 | }),
95 |
96 | Token::Op(op) => {
97 | if let Some(bp) = op.bp_prefix() {
98 | Expression::Term(Term {
99 | op,
100 | params: vec![parse_rec(tokens, bp)?],
101 | })
102 | } else {
103 | return Err(anyhow!("not a prefix op: {:?}", op));
104 | }
105 | }
106 |
107 | _ => todo!("lhs not an atom"),
108 | };
109 | loop {
110 | let op = match tokens.pop_front() {
111 | None => break,
112 | Some(Token::Op(op)) => op,
113 | Some(x) => todo!("two atoms next to eachother {:?} {:?}", lhs, x),
114 | };
115 |
116 | let (l_bp, r_bp) = op.bp_infix();
117 |
118 | if l_bp < min_bp {
119 | tokens.push_front(Token::Op(op));
120 | break;
121 | }
122 |
123 | if op == Operator::Filter {
124 | let Some(filter) = tokens.pop_front() else {
125 | return Err(anyhow!("unexpected end of expression"));
126 | };
127 |
128 | lhs = match filter {
129 | Token::Var(name) => Expression::FilterCall(FuncCall {
130 | name,
131 | params: vec![lhs],
132 | }),
133 | Token::FuncCall(fc) => {
134 | let super::lexer::FuncCall { name, params } = fc;
135 | let mut params: Vec = params
136 | .into_iter()
137 | .map(parse_to_expression)
138 | .collect::>>()?;
139 |
140 | params.insert(0, lhs);
141 |
142 | Expression::FilterCall(FuncCall { name, params })
143 | }
144 | _ => return Err(anyhow!("illegal filter name: {:?}", filter)),
145 | };
146 |
147 | break;
148 | }
149 |
150 | let rhs = parse_rec(tokens, r_bp)?;
151 | lhs = Expression::Term(Term {
152 | op,
153 | params: vec![lhs, rhs],
154 | })
155 | }
156 |
157 | Ok(lhs)
158 | }
159 |
160 | trait BindingPower {
161 | fn bp_infix(&self) -> (u8, u8);
162 | fn bp_prefix(&self) -> Option;
163 | }
164 |
165 | impl BindingPower for Operator {
166 | fn bp_infix(&self) -> (u8, u8) {
167 | match self {
168 | Self::Get => (63, 62),
169 | &Self::ArrayIndex => unreachable!("operator is postfix"),
170 | Self::Filter => (61, 60),
171 | Self::NullCoal => (59, 58),
172 | Self::Exp => (57, 56),
173 | Self::Is => (55, 54),
174 | Self::Modulo => (53, 52),
175 | Self::Divi => (51, 50),
176 | Self::Div => (49, 48),
177 | Self::Mul => (47, 46),
178 | Self::StrConcat => (45, 44),
179 | Self::Sub => (43, 42),
180 | Self::Add => (41, 40),
181 | Self::Range => (39, 38),
182 | Self::EndsWith => (37, 36),
183 | Self::StartsWith => (35, 34),
184 | Self::Matches => (33, 32),
185 | Self::In => (31, 30),
186 | Self::Lte => (27, 26),
187 | Self::Gte => (25, 24),
188 | Self::Gt => (23, 22),
189 | Self::Lt => (21, 20),
190 | Self::Starship => (19, 18),
191 | Self::Not => unreachable!("operator is prefix"),
192 | Self::Neq => (15, 14),
193 | Self::Eq => (13, 12),
194 | Self::And => (11, 10),
195 | Self::Or => (9, 8),
196 | Self::BOr => (7, 6),
197 | Self::BXor => (5, 4),
198 | Self::BAnd => (3, 2),
199 | Self::Ternary => todo!("ternary not yet supported"),
200 | }
201 | }
202 |
203 | fn bp_prefix(&self) -> Option {
204 | match self {
205 | Self::Not => Some(16),
206 | _ => None,
207 | }
208 | }
209 | }
210 |
211 | #[cfg(test)]
212 | mod tests {
213 | use crate::loader::expression::{ast::FuncCall, lexer};
214 |
215 | use super::*;
216 | use pretty_assertions::assert_eq;
217 |
218 | #[test]
219 | fn test_infix_arithmetic() {
220 | let tokens = vec![
221 | Token::Number(1),
222 | Token::Op(Operator::Add),
223 | Token::Number(2),
224 | Token::Op(Operator::Mul),
225 | Token::Number(3),
226 | ];
227 |
228 | assert_eq!(
229 | parse_to_expression(tokens).unwrap(),
230 | Expression::Term(Term {
231 | op: Operator::Add,
232 | params: vec![
233 | Expression::Number(1),
234 | Expression::Term(Term {
235 | op: Operator::Mul,
236 | params: vec![Expression::Number(2), Expression::Number(3),]
237 | })
238 | ]
239 | })
240 | )
241 | }
242 |
243 | #[test]
244 | fn test_parenthesis() {
245 | let tokens = vec![
246 | Token::Number(1),
247 | Token::Op(Operator::Mul),
248 | Token::Parens(vec![
249 | Token::Number(2),
250 | Token::Op(Operator::Add),
251 | Token::Number(3),
252 | ]),
253 | ];
254 |
255 | assert_eq!(
256 | parse_to_expression(tokens).unwrap(),
257 | Expression::Term(Term {
258 | op: Operator::Mul,
259 | params: vec![
260 | Expression::Number(1),
261 | Expression::Term(Term {
262 | op: Operator::Add,
263 | params: vec![Expression::Number(2), Expression::Number(3)]
264 | })
265 | ]
266 | })
267 | )
268 | }
269 |
270 | #[test]
271 | fn test_function_call() {
272 | let tokens = vec![Token::FuncCall(lexer::FuncCall {
273 | name: "foo".to_string(),
274 | params: vec![vec![
275 | Token::Number(1),
276 | Token::Op(Operator::Add),
277 | Token::Number(2),
278 | ]],
279 | })];
280 |
281 | assert_eq!(
282 | parse_to_expression(tokens).unwrap(),
283 | Expression::FuncCall(FuncCall {
284 | name: "foo".to_string(),
285 | params: vec![Expression::Term(Term {
286 | op: Operator::Add,
287 | params: vec![Expression::Number(1), Expression::Number(2)]
288 | })]
289 | })
290 | )
291 | }
292 |
293 | #[test]
294 | fn test_not() {
295 | let tokens = vec![
296 | Token::Op(Operator::Not),
297 | Token::Number(2),
298 | Token::Op(Operator::Lte),
299 | Token::Number(3),
300 | Token::Op(Operator::And),
301 | Token::Number(4),
302 | Token::Op(Operator::Gte),
303 | Token::Number(5),
304 | ];
305 |
306 | assert_eq!(
307 | parse_to_expression(tokens).unwrap(),
308 | Expression::Term(Term {
309 | op: Operator::And,
310 | params: vec![
311 | Expression::Term(Term {
312 | op: Operator::Not,
313 | params: vec![Expression::Term(Term {
314 | op: Operator::Lte,
315 | params: vec![Expression::Number(2), Expression::Number(3),]
316 | }),]
317 | }),
318 | Expression::Term(Term {
319 | op: Operator::Gte,
320 | params: vec![Expression::Number(4), Expression::Number(5)]
321 | })
322 | ]
323 | })
324 | )
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/src/loader/expression/lexer.rs:
--------------------------------------------------------------------------------
1 | use nom::{
2 | branch::alt,
3 | bytes::complete::{tag, take_while, take_while1},
4 | character::complete::{digit1, multispace0, multispace1, one_of},
5 | combinator::{eof, map_res, not, opt, peek},
6 | error::{make_error, ErrorKind, ParseError},
7 | multi::{many_till, separated_list0, separated_list1},
8 | number::complete::float,
9 | sequence::{delimited, preceded, separated_pair, terminated, tuple},
10 | Err, IResult,
11 | };
12 |
13 | use crate::loader::Span;
14 |
15 | use super::parser::Operator;
16 |
17 | #[derive(Debug, PartialEq, Clone)]
18 | pub enum Token {
19 | Str(String),
20 | Var(String),
21 | Number(i64),
22 | Float(f64),
23 | Bool(bool),
24 | Null,
25 | Array(Vec>),
26 | HashMap(Vec),
27 | Parens(Vec),
28 | FuncCall(FuncCall),
29 | Op(Operator),
30 | Parent(), //TODO remove
31 | }
32 |
33 | #[derive(Debug, PartialEq, Clone)]
34 | pub struct KVTokensPair {
35 | pub key: Vec,
36 | pub value: Vec,
37 | }
38 |
39 | #[derive(Debug, PartialEq, Clone)]
40 | pub struct FuncCall {
41 | pub name: String,
42 | pub params: Vec>,
43 | }
44 |
45 | pub fn lex_exprs(i: Span) -> IResult> {
46 | let (rest, (exprs, _)) = preceded(multispace0, many_till(lex_exprs_elem, eof))(i)?;
47 | Ok((rest, exprs))
48 | }
49 |
50 | fn lex_exprs_elem(i: Span) -> IResult {
51 | terminated(lex_expr, opt(multispace1))(i)
52 | }
53 |
54 | fn lex_expr(i: Span) -> IResult {
55 | alt((
56 | lex_operator,
57 | lex_bool,
58 | lex_parent_call,
59 | lex_hash_map,
60 | lex_parens,
61 | lex_array,
62 | lex_number,
63 | lex_float,
64 | lex_string_literal,
65 | lex_func_call,
66 | lex_var,
67 | ))(i)
68 | }
69 |
70 | fn lex_parent_call(i: Span) -> IResult {
71 | let (rest, _) = tag::<&str, Span, nom::error::Error>("parent()" /* value */)(i)?;
72 | Ok((rest, Token::Parent()))
73 | }
74 |
75 | fn lex_bool(i: Span) -> IResult {
76 | let (rest, word) = alt((tag("true"), tag("false")))(i)?;
77 | Ok((rest, Token::Bool(*word.fragment() == "true")))
78 | }
79 |
80 | fn lex_string_literal(i: Span) -> IResult {
81 | let (rest, plain_str) = lex_quoted(i)?;
82 | Ok((rest, Token::Str(plain_str.to_string())))
83 | }
84 |
85 | fn lex_quoted(i: Span) -> IResult {
86 | alt((
87 | delimited(
88 | nom::character::complete::char('\''),
89 | take_while(|c| c != '\''),
90 | nom::character::complete::char('\''),
91 | ),
92 | delimited(
93 | nom::character::complete::char('"'),
94 | take_while(|c| c != '"'),
95 | nom::character::complete::char('"'),
96 | ),
97 | ))(i)
98 | }
99 |
100 | fn lex_var(i: Span) -> IResult {
101 | let is_identifier = |c| -> bool {
102 | ('a'..='z').contains(&c)
103 | || ('A'..='Z').contains(&c)
104 | || c == '_'
105 | || (0x7f as char <= c && c <= 0xff as char)
106 | };
107 | let (rest, (part1, part2)) = tuple((
108 | take_while1(is_identifier),
109 | take_while(|c| is_identifier(c) || ('0'..='9').contains(&c) || c == '.'),
110 | ))(i)?;
111 | let mut accessor = part1.to_string();
112 | accessor.push_str(part2.trim());
113 | Ok((rest, Token::Var(accessor)))
114 | }
115 |
116 | fn lex_float(i: Span) -> IResult {
117 | let (rest, f) = float(i)?;
118 | Ok((rest, Token::Float(f.into())))
119 | }
120 |
121 | #[derive(Debug, PartialEq)]
122 | pub enum TwigError {
123 | NumberErr,
124 | Nom(I, ErrorKind),
125 | }
126 |
127 | impl ParseError for TwigError {
128 | fn from_error_kind(input: I, kind: ErrorKind) -> Self {
129 | TwigError::Nom(input, kind)
130 | }
131 |
132 | fn append(_: I, _: ErrorKind, other: Self) -> Self {
133 | other
134 | }
135 | }
136 |
137 | fn lex_number(i: Span) -> IResult {
138 | //TODO add negative numbers
139 | let (rest, (number, ..)) = tuple((digit1, not(one_of("e."))))(i)?;
140 |
141 | match str::parse(&number) {
142 | Ok(num) => Ok((rest, Token::Number(num))),
143 | Err(_) => Err(nom::Err::Error(make_error(i, ErrorKind::Digit))),
144 | }
145 | }
146 |
147 | fn lex_parens(i: Span) -> IResult {
148 | let (rest, (.., (child_exprs, ..))) = tuple((
149 | nom::character::complete::char('('),
150 | many_till(
151 | lex_exprs_elem,
152 | tuple((multispace0, nom::character::complete::char(')'))),
153 | ),
154 | ))(i)?;
155 | Ok((rest, Token::Parens(child_exprs)))
156 | }
157 |
158 | fn lex_array(i: Span) -> IResult {
159 | let (rest, content) = delimited(
160 | tuple((tag("["), multispace0)),
161 | separated_list0(tuple((tag(","), multispace0)), many_till(lex_exprs_elem, peek(alt((tag(","), tag("]")))))),
162 | tag("]")
163 | )(i)?;
164 | let expr_list = content.into_iter().map(|(expr, ..)| expr).collect();
165 | Ok((rest, Token::Array(expr_list)))
166 | }
167 |
168 | fn lex_hash_map(i: Span) -> IResult {
169 | let (rest, kv_pairs) = delimited(
170 | tuple((tag("{"), multispace0)),
171 | separated_list0(
172 | tuple((multispace0, tag(","), multispace0)),
173 | lex_key_value_pair,
174 | ),
175 | tuple((multispace0, tag("}"))),
176 | )(i)?;
177 | Ok((rest, Token::HashMap(kv_pairs)))
178 | }
179 |
180 | fn lex_key_value_pair(i: Span) -> IResult {
181 | let (rest, (key, value)) = separated_pair(
182 | alt((lex_parens, lex_string_literal, lex_var)),
183 | tuple((multispace0, tag(":"), multispace0)),
184 | separated_list1(multispace1, lex_expr),
185 | )(i)?;
186 | //hash keys are allowed to be unqouted
187 | let key = match key {
188 | Token::Var(v) => vec![Token::Str(v)],
189 | Token::Str(a) => vec![Token::Str(a)],
190 | Token::Parens(tokens) => tokens,
191 | _ => todo!(),
192 | };
193 | Ok((rest, KVTokensPair { key, value }))
194 | }
195 |
196 | fn lex_func_call(i: Span) -> IResult {
197 | let is_identifier = |c| -> bool {
198 | ('a'..='z').contains(&c)
199 | || ('A'..='Z').contains(&c)
200 | || c == '_'
201 | || (0x7f as char <= c && c <= 0xff as char)
202 | };
203 |
204 | let (rest, (_, name, param_strs)) = tuple((
205 | multispace0,
206 | take_while1(is_identifier),
207 | delimited(
208 | tag("("),
209 | separated_list0(tag(","), take_while1(|c| c != ',' && c != ')')),
210 | tag(")"),
211 | ),
212 | ))(i)?;
213 | let mut params = Vec::with_capacity(param_strs.len());
214 |
215 | for expr in param_strs.into_iter() {
216 | let (_, tokens) = lex_exprs(expr)?;
217 | params.push(tokens);
218 | }
219 |
220 | Ok((
221 | rest,
222 | Token::FuncCall(FuncCall {
223 | name: name.to_string(),
224 | params,
225 | }),
226 | ))
227 | }
228 |
229 | fn lex_operator(i: Span) -> IResult {
230 | let (rest, (_, op)) = tuple((
231 | multispace0,
232 | alt((lex_multi_char_operator, lex_single_operator)),
233 | ))(i)?;
234 | Ok((rest, Token::Op(op)))
235 | }
236 |
237 | fn lex_multi_char_operator(i: Span) -> IResult {
238 | let (rest, op) = alt((
239 | tag("//"),
240 | tag("in "),
241 | tag("not "),
242 | tag("is "),
243 | tag("matches "),
244 | tag("starts with "),
245 | tag("ends with "),
246 | tag("and "),
247 | tag("or "),
248 | tag("b-and "),
249 | tag("b-or "),
250 | tag("b-xor "),
251 | tag("**"),
252 | tag("??"),
253 | tag(".."),
254 | tag("=="),
255 | tag("!="),
256 | tag("<="),
257 | tag(">="),
258 | tag("<=>"),
259 | ))(i)?;
260 | Ok((
261 | rest,
262 | match *op {
263 | "//" => Operator::Divi,
264 | "in " => Operator::In,
265 | "not " => Operator::Not,
266 | "is " => Operator::Is,
267 | "matches " => Operator::Matches,
268 | "starts with " => Operator::StartsWith,
269 | "ends with " => Operator::EndsWith,
270 | "and " => Operator::And,
271 | "or " => Operator::Or,
272 | "b-and " => Operator::BAnd,
273 | "b-or " => Operator::BOr,
274 | "b-xor " => Operator::BXor,
275 | "**" => Operator::Exp,
276 | "??" => Operator::NullCoal,
277 | ".." => Operator::Range,
278 | "==" => Operator::Eq,
279 | "!=" => Operator::Neq,
280 | "<=" => Operator::Lte,
281 | ">=" => Operator::Gte,
282 | "<=>" => Operator::Starship,
283 | _ => unreachable!(),
284 | },
285 | ))
286 | }
287 |
288 | fn lex_single_operator(i: Span) -> IResult {
289 | let (rest, char) = one_of("+-*/~%|")(i)?;
290 | match char {
291 | '+' => Ok((rest, Operator::Add)),
292 | '-' => Ok((rest, Operator::Sub)),
293 | '*' => Ok((rest, Operator::Mul)),
294 | '/' => Ok((rest, Operator::Div)),
295 | '~' => Ok((rest, Operator::StrConcat)),
296 | '%' => Ok((rest, Operator::Modulo)),
297 | '|' => Ok((rest, Operator::Filter)),
298 | _ => unreachable!(),
299 | }
300 | }
301 |
302 | #[cfg(test)]
303 | mod tests {
304 | use std::vec;
305 |
306 | use super::*;
307 | use pretty_assertions::assert_eq;
308 |
309 | #[test]
310 | fn test_lex_var() {
311 | let var = Span::new("foo.bar ");
312 | assert_eq!(
313 | unspan(lex_var(var)),
314 | (" ", Token::Var("foo.bar".to_string()))
315 | )
316 | }
317 |
318 | #[test]
319 | fn test_lex_str() {
320 | let single_quote = Span::new("'foo'");
321 | let double_quote = Span::new(r#""foo""#);
322 |
323 | assert_eq!(
324 | unspan(lex_string_literal(single_quote)),
325 | ("", Token::Str("foo".to_string()))
326 | );
327 |
328 | assert_eq!(
329 | unspan(lex_string_literal(double_quote)),
330 | ("", Token::Str("foo".to_string()))
331 | );
332 | }
333 |
334 | #[test]
335 | fn test_lex_array() {
336 | let expectation = Token::Array(
337 | vec![
338 | vec![Token::Var("var".to_string())],
339 | vec![Token::Str(",str".to_string())],
340 | vec![Token::Number(1)]
341 | ]);
342 |
343 | let tests = vec![
344 | Span::new("[ var, ',str',1]"),
345 | Span::new("[var, ',str',1]"),
346 | Span::new("[var,',str',1 ]"),
347 | Span::new("[var,',str',1]"),
348 | ];
349 |
350 | for test in tests {
351 | assert_eq!(
352 | unspan(lex_array(test)),
353 | (
354 | "",
355 | expectation.clone()
356 | ));
357 | }
358 |
359 | let nested = Span::new("[[[1]]]");
360 | assert_eq!(
361 | unspan(lex_array(nested)),
362 | (
363 | "",
364 | Token::Array(vec![vec![Token::Array(vec![vec![Token::Array(vec![vec![Token::Number(1)]])]])]])
365 | )
366 | )
367 | }
368 |
369 | #[test]
370 | fn test_lex_hashmap() {
371 | let hm = Span::new("{ key:'bar','key1' : var, (var): 1}");
372 | assert_eq!(
373 | unspan(lex_hash_map(hm)),
374 | (
375 | "",
376 | Token::HashMap(vec![
377 | KVTokensPair {
378 | key: vec![Token::Str("key".to_string())],
379 | value: vec![Token::Str("bar".to_string())]
380 | },
381 | KVTokensPair {
382 | key: vec![Token::Str("key1".to_string())],
383 | value: vec![Token::Var("var".to_string())]
384 | },
385 | KVTokensPair {
386 | key: vec![Token::Var("var".to_string())],
387 | value: vec![Token::Number(1)]
388 | }
389 | ])
390 | )
391 | )
392 | }
393 |
394 | #[test]
395 | fn test_lex_func_call() {
396 | let expr = Span::new(r#"foo(1, "two" )"#);
397 |
398 | assert_eq!(
399 | unspan(lex_exprs(expr)),
400 | (
401 | "",
402 | vec![Token::FuncCall(FuncCall {
403 | name: "foo".to_string(),
404 | params: vec![vec![Token::Number(1)], vec![Token::Str("two".to_string())]]
405 | })]
406 | )
407 | )
408 | }
409 |
410 | #[test]
411 | fn test_lex_expressions() {
412 | let expr = Span::new("2 + 3 * 4 == 14 and 'foo' in ['foo', 'bar']");
413 | assert_eq!(
414 | unspan(lex_exprs(expr)),
415 | (
416 | "",
417 | vec![
418 | Token::Number(2),
419 | Token::Op(Operator::Add),
420 | Token::Number(3),
421 | Token::Op(Operator::Mul),
422 | Token::Number(4),
423 | Token::Op(Operator::Eq),
424 | Token::Number(14),
425 | Token::Op(Operator::And),
426 | Token::Str("foo".to_string()),
427 | Token::Op(Operator::In),
428 | Token::Array(vec![
429 | vec![Token::Str("foo".to_string())],
430 | vec![Token::Str("bar".to_string())],
431 | ])
432 | ]
433 | )
434 | )
435 | }
436 |
437 | #[test]
438 | fn test_lex_bool() {
439 | let t = Span::new("true");
440 | assert_eq!(unspan(lex_bool(t)), ("", Token::Bool(true)));
441 |
442 | let f = Span::new("false");
443 | assert_eq!(unspan(lex_bool(f)), ("", Token::Bool(false)));
444 | }
445 |
446 | fn unspan(span: IResult) -> (&str, O) {
447 | let (rest, out) = span.unwrap();
448 | (rest.fragment(), out)
449 | }
450 | }
451 |
--------------------------------------------------------------------------------