diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..34bc0c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +/tests export-ignore +/.scape export-ignore +/LOG.md export-ignore +/SPECS.md export-ignore +/MILESTONES.md export-ignore +/phpunit.xml export-ignore +.gitignore export-ignore +.gitattributes export-ignore diff --git a/.gitignore b/.gitignore index 623aa48..cbd36dc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ crashlytics-build.properties fabric.properties composer.phar /vendor/ -.junie/ -.phpunit.cache/ \ No newline at end of file +.junie +.phpunit.cache/ +.scape \ No newline at end of file diff --git a/MILESTONES.md b/MILESTONES.md index 03c6f0c..95bb42a 100644 --- a/MILESTONES.md +++ b/MILESTONES.md @@ -2,49 +2,58 @@ This document outlines the phased roadmap for building Scape Templates. Each milestone is designed to ensure "Excellence over Features," focusing on a solid foundation before adding complexity. +## Table of Contents +- [Phase 1: Core Architecture & Environment](#phase-1-core-architecture--environment) +- [Phase 2: Lexical Analysis & Tokenization](#phase-2-lexical-analysis--tokenization) +- [Phase 3: The AST & Parser](#phase-3-the-ast--parser) +- [Phase 4: The Interpreter (Rendering Engine)](#phase-4-the-interpreter-rendering-engine) +- [Phase 5: Inheritance & Reusability](#phase-5-inheritance--reusability) +- [Phase 6: Extensibility & Performance](#phase-6-extensibility--performance) +- [Phase 7: Final Polish & Release](#phase-7-final-polish--release) + ## Phase 1: Core Architecture & Environment *Focus: Establishing the contracts, error handling, and runtime configuration.* -- [ ] Define and implement `Scape\Interfaces\FilterInterface` and `Scape\Interfaces\HostProviderInterface`. -- [ ] Implement the Exception hierarchy in `Scape\Exceptions`. -- [ ] Implement `Scape\Config` to handle environment variables (`SCAPE_*_DIR`) and programmatic overrides. -- [ ] Create the `Scape\Engine` boilerplate with the `render()` method signature. +- [x] Define and implement `Scape\Interfaces\FilterInterface` and `Scape\Interfaces\HostProviderInterface`. +- [x] Implement the Exception hierarchy in `Scape\Exceptions`. +- [x] Implement `Scape\Config` to handle environment variables (`SCAPE_*_DIR`) and programmatic overrides. +- [x] Create the `Scape\Engine` boilerplate with the `render()` method signature. ## Phase 2: Lexical Analysis & Tokenization *Focus: Turning template strings into a stream of tokens that the parser can understand.* -- [ ] Implement `Scape\Parser\Lexer` to identify interpolation `{{ }}`, raw `{{{ }}}`, logic `{( )}`, and block `{[ ]}` tags. -- [ ] Support white-space independence within tags. -- [ ] Implement the white-space control rule (logic tags consuming one trailing newline). -- [ ] Comprehensive unit tests for all tag variations. +- [x] Implement `Scape\Parser\Lexer` to identify interpolation `{{ }}`, raw `{{{ }}}`, logic `{( )}`, and block `{[ ]}` tags. +- [x] Support white-space independence within tags. +- [x] Implement the white-space control rule (logic tags consuming one trailing newline). +- [x] Comprehensive unit tests for all tag variations. ## Phase 3: The AST & Parser *Focus: Building the Abstract Syntax Tree (AST) representing the template's structure.* -- [ ] Implement `Scape\Parser\Parser` to convert tokens into an AST. -- [ ] Define AST Nodes (Text, Variable, Loop, Block, Include, Filter). -- [ ] Implement the `foreach` grammar (with optional keys). -- [ ] Implement data access logic (dot-notation for objects, brackets for arrays). +- [x] Implement `Scape\Parser\Parser` to convert tokens into an AST. +- [x] Define AST Nodes (Text, Variable, Loop, Block, Include, Filter). +- [x] Implement the `foreach` grammar (with optional keys). +- [x] Implement data access logic (dot-notation for objects, brackets for arrays). ## Phase 4: The Interpreter (Rendering Engine) *Focus: Turning the AST and data into the final HTML output.* -- [ ] Implement the AST Interpreter to walk the tree and resolve variables. -- [ ] Implement standard HTML escaping (`ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5`). -- [ ] Implement Loop logic with `index`, `pos`, and positional rendering (`first`, `inner`, `last`). -- [ ] Implement `debug` vs `production` variable access modes. +- [x] Implement the AST Interpreter to walk the tree and resolve variables. +- [x] Implement standard HTML escaping (`ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5`). +- [x] Implement Loop logic with `index`, `pos`, and positional rendering (`first`, `inner`, `last`). +- [x] Implement `debug` vs `production` variable access modes. ## Phase 5: Inheritance & Reusability *Focus: Blocks, Layouts, and Partials.* -- [ ] Implement `{[ extends ]}` and the Block override system (including `{[ parent ]}`). -- [ ] Implement `{[ include ]}` with data scoping rules (`with context`, `with data_source`, inline arrays). -- [ ] Implement the recursion limit check (default 20). -- [ ] Implement the 404 fallback mechanism. +- [x] Implement `{[ extends ]}` and the Block override system (including `{[ parent ]}`). +- [x] Implement `{[ include ]}` with data scoping rules (`with context`, `with data_source`, inline arrays). +- [x] Implement the recursion limit check (default 20). +- [x] Implement the 404 fallback mechanism. ## Phase 6: Extensibility & Performance *Focus: Filters, Host IoC, and Caching.* -- [ ] Implement the Filter pipeline (piping and arguments). -- [ ] Implement `uses` and `load_filter` mechanisms. -- [ ] Implement the `host` namespace delegation. -- [ ] Implement AST Caching (local storage in `.scape/cache`) with `mtime` invalidation for dev mode. +- [x] Implement the Filter pipeline (piping and arguments). +- [x] Implement `uses` and `load_filter` mechanisms. +- [x] Implement the `host` namespace delegation. +- [x] Implement AST Caching (local storage in `.scape/cache`) with `mtime` invalidation for dev mode. ## Phase 7: Final Polish & Release -- [ ] Final project-wide code style audit. -- [ ] Ensure 100% test coverage for core rendering logic. -- [ ] Draft the final README (Getting Started and examples). +- [x] Final project-wide code style audit. +- [x] Ensure 100% test coverage for core rendering logic. +- [x] Draft the final README (Getting Started and examples). diff --git a/README.md b/README.md index f9a06c3..1154bb8 100644 --- a/README.md +++ b/README.md @@ -1 +1,94 @@ # Scape Templates + +A lightweight, standalone PHP template engine designed for simplicity, security, and performance. Scape focuses on being an **Output Engine first**, marrying pre-processed data with design while enforcing a "logic-light" philosophy. + +## Features + +- **Dot Notation & Bracket Access**: Effortlessly access nested objects and arrays. +- **Inheritance & Blocks**: Define base layouts and override sections in child templates. +- **Partials & Includes**: Reuse template snippets with controlled data scoping. +- **Filter Pipeline**: Transform data using built-in or custom filters (e.g., `{{ var | lower | ucfirst }}`). + - Built-in filters: `lower`, `upper`, `ucfirst`, `currency`, `float`, `date`, `truncate`, `default`, `json`, `url_encode`, `join`, `first`, `last`, `word_count`, `keys`. + - Filters can be used in variable interpolations, `foreach` loops, and `include` tags. +- **Secure by Default**: Automatic contextual HTML escaping for all variables. +- **AST Caching**: High performance via Abstract Syntax Tree caching with automatic dev-mode invalidation. +- **Host Integration (IoC)**: Easy integration with frameworks through the reserved `host` namespace. +- **Logic-Light**: Encourages separation of concerns by supporting only necessary logic like `foreach`. + +## Installation + +```bash +composer require getphred/scape +``` + +## Quick Start + +```php +use Scape\Engine; + +$engine = new Engine([ + 'templates_dir' => __DIR__ . '/templates', + 'mode' => 'debug' // or 'production' +]); + +echo $engine->render('index', [ + 'title' => 'Welcome to Scape', + 'user' => ['name' => 'Funky'] +]); +``` + +### Basic Syntax + +#### Interpolation (Escaped) +`{{ user.name }}` + +#### Raw Interpolation +`{{{ raw_html }}}` + +#### Loops +```html +{( foreach item in items )} +
  • {{ item }}
  • +{( endforeach )} +``` + +#### Filtering +`{{ price | currency('USD') }}` + +#### Advanced Expressions +`{( foreach key in user_data | keys )}` + +`{[ include 'partial' with data | first ]}` + +#### Inheritance +`layout.scape.php`: +```html + + {[ block 'title' ]}Default Title{[ endblock ]} + {[ block 'content' ]}{[ endblock ]} + +``` + +`page.scape.php`: +```html +{[ extends 'layout' ]} +{[ block 'title' ]}My Page{[ endblock ]} +{[ block 'content' ]} +

    Hello World

    +{[ endblock ]} +``` + +## Configuration + +Scape uses environment variables or programmatic configuration: + +- `SCAPE_TEMPLATES_DIR`: Default `./templates` +- `SCAPE_LAYOUTS_DIR`: Default `./templates/layouts` +- `SCAPE_PARTIALS_DIR`: Default `./templates/partials` +- `SCAPE_FILTERS_DIR`: Default `./filters` +- `SCAPE_CACHE_DIR`: Default `./.scape/cache` +- `SCAPE_MODE`: `production` (default) or `debug` + +## License + +MIT diff --git a/SPECS.md b/SPECS.md index 925dba0..81e6c6b 100644 --- a/SPECS.md +++ b/SPECS.md @@ -23,11 +23,17 @@ composer require getphred/scape - **Filter Extensions**: - Custom filters must use the standard `.php` extension. - All custom filters must implement the `Scape\Interfaces\FilterInterface` to ensure they provide the necessary transformation methods. -- **Directory Configuration**: Template locations are managed via environment variables: - - `SCAPE_TEMPLATES_DIR`: Main directory for application templates. - - `SCAPE_LAYOUTS_DIR`: Directory for base layouts and parent templates. - - `SCAPE_PARTIALS_DIR`: Directory for reusable snippets/partials. - - `SCAPE_FILTERS_DIR`: Directory for user-defined filters. +- **Directory Configuration**: Template locations are managed via environment variables (with the following defaults when not provided): + - `SCAPE_TEMPLATES_DIR`: Main directory for application templates. + - Default: `./templates` + - `SCAPE_LAYOUTS_DIR`: Directory for base layouts and parent templates. + - Default: `./templates/layouts` + - `SCAPE_PARTIALS_DIR`: Directory for reusable snippets/partials. + - Default: `./templates/partials` + - `SCAPE_FILTERS_DIR`: Directory for user-defined filters. + - Default: `./filters` + - `SCAPE_CACHE_DIR`: Directory for cached AST files. + - Default: `./.scape/cache` - **Dot Notation Pathing**: All internal paths (extends, includes) use dot notation (e.g., `sidebar.login_form`) relative to their respective directories, omitting the file extension. ### 2. Syntax & White-space @@ -92,13 +98,29 @@ composer require getphred/scape - **Loading**: Filters must be pre-loaded at the top of the template. - **Internal Libraries**: Engine-provided filters are loaded using the `uses` keyword: - Syntax: `{( uses namespace:library )}` (e.g., `{( uses filters:string )}`). + - **filters:string** library includes: + - `lower`: Converts to lowercase. + - `upper`: Converts to uppercase. + - `ucfirst`: Capitalizes the first character. + - `currency(code)`: Formats numeric value as currency (default: 'USD'). + - `float(precision)`: Formats numeric value as a float with fixed precision (default: 2). + - `date(format)`: Formats a timestamp or date string (default: 'Y-m-d H:i:s'). + - `truncate(length, suffix)`: Truncates string to length (default length: 100, suffix: '...'). + - `default(fallback)`: Returns fallback if value is empty. + - `json`: Returns JSON encoded string. + - `url_encode`: Returns URL encoded string. + - `join(glue)`: Joins array elements with glue (default glue: ''). + - `first`: Returns first element of a collection. + - `last`: Returns last element of a collection. + - `word_count`: Returns word count of a string. + - `keys`: Returns keys of an associative array. - **Custom Filters**: User-defined filters are loaded from `SCAPE_FILTERS_DIR` using `load_filter`. - Syntax: `{( load_filter('path') )}` where path is dot-notated. ### 6. Security & Error Handling - **Contextual Escaping**: Standard `{{ }}` interpolation ensures XSS protection. - **Missing Assets**: If a layout or partial is missing, the engine looks for a user-provided `404.scape.php` in `SCAPE_TEMPLATES_DIR`. If not found, it renders a built-in "Template 404" placeholder. -- **Exceptions**: The engine throws specific exceptions within the `Scape\Exceptions` namespace: +- **Exceptions**: The engine throws specific exceptions within the `Scape\Exceptions` namespace. **All exceptions must include helpful, context-rich error messages** (e.g., template name, line number, and specific failure reason) to assist in debugging: - `TemplateNotFoundException`: Main template, layout, or partial missing. - `SyntaxException`: Malformed tags or disallowed logic (e.g. `if`). - `FilterNotFoundException`: Target of `uses` or `load_filter` missing. @@ -116,11 +138,15 @@ composer require getphred/scape - **Development**: Engine checks file modification times (`mtime`) to invalidate the cache when a template changes. - **Production**: Engine skips `mtime` checks and serves the cached AST directly for maximum performance. -### 8. Host Integration (IoC) +### 8. Host Integration (IoC) & i18n - **The `host` Namespace**: Scape provides a reserved `host` namespace that can be used with the `uses` keyword or as a filter/function prefix. - **Provider Registration**: Host frameworks (e.g., Phred) can register custom providers to handle calls in this namespace. +- **Localization (i18n)**: + - **Philosophy**: Scape is "Simple for Blogs, Powerful for Enterprise." Localization is an **opt-in** feature. + - **Responsibility**: Translation logic and message catalogs reside in the Application/Framework. Scape provides the **access layer**. + - **Usage**: Templates can use `host.translate('key')` or the `|t` filter alias to fetch localized strings on-demand. + - **Portability**: If no host provider is registered, i18n calls return the input key/value unchanged, ensuring templates remain portable across environments. - **Use Cases**: Used for framework-level features like feature flags (Flagpole), routing, or translations without creating a hard dependency within the engine. -- **Default Behavior**: If no provider is registered, calls to the `host` namespace return the input value unchanged or `null`. ### 9. Runtime API - **The `Scape\Engine` Class**: The primary entry point for the library. diff --git a/composer.json b/composer.json index 9e43cff..bb50848 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ } ], "require": { - "php": "^8.2" + "php": "^8.2", + "ext-intl": "*" }, "require-dev": { "phpunit/phpunit": "^10.0" diff --git a/composer.lock b/composer.lock index d74b6a1..e394c05 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a04d8f4360b838804edefd62253383ac", + "content-hash": "4f8a56ae9cae9b8b16e124fc4ef58da9", "packages": [], "packages-dev": [ { @@ -566,16 +566,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.60", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f2e26f52f80ef77832e359205f216eeac00e320c" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f2e26f52f80ef77832e359205f216eeac00e320c", - "reference": "f2e26f52f80ef77832e359205f216eeac00e320c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -596,7 +596,7 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.4", + "sebastian/comparator": "^5.0.5", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.4", @@ -647,7 +647,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.60" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -671,7 +671,7 @@ "type": "tidelift" } ], - "time": "2025-12-06T07:50:42+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "sebastian/cli-parser", @@ -843,16 +843,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -908,7 +908,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { @@ -928,7 +928,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T05:25:07+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", @@ -1683,7 +1683,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.2", + "ext-intl": "*" }, "platform-dev": {}, "plugin-api-version": "2.6.0"