├── 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