diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 0ebc0d1..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -name: Bug Report -about: Report something that's broken -labels: Bug ---- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 9925f78..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -name: Feature Request -about: Suggest an idea for this project -labels: Enhancement ---- diff --git a/.github/contributing.md b/.github/contributing.md new file mode 100644 index 0000000..8a7231d --- /dev/null +++ b/.github/contributing.md @@ -0,0 +1,26 @@ +# Contributing to Arcane + +First off, thank you for considering contributing to Arcane! + +Arcane is a deliberately minimal single-file microframework. It is built on the philosophy that web development should be crafted, not configured. Because of the framework's strict architectural constraints, please review these guidelines before opening an issue or pull request. + +## Core Principles + +- **Zero Dependencies:** Arcane does not and will not use external packages. +- **Keep it Small:** The core engine lives in a single file (`index.php`). Every new line of code is ruthlessly scrutinized for weight and necessity. "Less" is a deliberate discipline. +- **Native PHP:** We rely on modern PHP features rather than polyfills or heavy abstractions. +- **Formatting:** We use modern PHP spacing (e.g., `if ()`, `elseif ()`) but strictly maintain a **2-space indentation** and an **80-column soft limit** to keep the file vertically dense and readable. + +## Pull Requests + +Pull requests for bug fixes, performance optimizations, and documentation updates are always welcome. + +**Note on New Features:** Feature additions are heavily gated. If a feature is highly specific or adds significant weight, it likely belongs in the [Arcane Helpers](https://github.com/capachow/arcane-helpers) repository, not the core engine. + +### Development Process + +1. Fork the repository and create a feature branch off `master`. +2. Ensure your code matches the existing minimalist architectural styles. +3. Submit your pull request with a concise explanation of the problem it solves. + +Thank you for helping build something you are proud of. Keep it **Arcane**. diff --git a/.github/FUNDING.yml b/.github/funding.yml similarity index 100% rename from .github/FUNDING.yml rename to .github/funding.yml diff --git a/.github/issue_template/bug_report.md b/.github/issue_template/bug_report.md new file mode 100644 index 0000000..d5db12a --- /dev/null +++ b/.github/issue_template/bug_report.md @@ -0,0 +1,14 @@ +--- +name: Bug Report +about: Report something that's broken +labels: Bug +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**How to reproduce** +Steps to cause the bug or a link to a minimal reproduction. + +**Environment** +PHP version, OS, and Server (Apache/NGINX/CLI). diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/issue_template/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yml rename to .github/issue_template/config.yml diff --git a/.github/issue_template/feature_request.md b/.github/issue_template/feature_request.md new file mode 100644 index 0000000..9103f89 --- /dev/null +++ b/.github/issue_template/feature_request.md @@ -0,0 +1,11 @@ +--- +name: Feature Request +about: Suggest an idea for this project +labels: Enhancement +--- + +**The Idea** +What is the problem or new feature you are suggesting? + +**How it should work** +A clear description of how you imagine this feature working in Arcane. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..1818751 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ +**What does this PR do?** +A brief description of the changes, bug fixes, or new features. + +**Why is this needed?** +Link to any relevant issues or explain the reasoning behind the change. diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..cc60294 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,22 @@ +name: PHP Linter + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + linter: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + + - name: Check PHP Syntax + run: php -l index.php diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0881124 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Auto Release + +on: + push: + branches: + - master + paths: + - 'index.php' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Extract Version + id: get_version + run: | + VERSION=$(grep -oE 'Arcane [0-9]+\.[0-9]+\.[0-9]+' index.php | awk '{print $2}') + if [ -z "$VERSION" ]; then + echo "Error: Version could not be extracted from index.php." + exit 1 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=v$VERSION" >> $GITHUB_OUTPUT + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create ${{ steps.get_version.outputs.tag }} \ + --title "${{ steps.get_version.outputs.version }}" \ + --generate-notes \ + index.php diff --git a/.gitignore b/.gitignore index 7ab68dc..af85ce7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ * +!.github/ +!.github/** !.gitignore -!composer.json !LICENSE.md !README.md -!index.php \ No newline at end of file +!composer.json +!index.php diff --git a/README.md b/README.md index 77fb871..23598e1 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ > *Arcane is unconventional but beautifully intuitive. It is intentionally different, breaking away from modern frameworks to encourage critical thinking without the dependence and overhead of complex systems. It brings out the fun in building for the web by automating the features you want, while making it easier to apply the ones you need.* -At its core, Arcane is a tiny `13kb` single-file PHP microframework designed to keep things easy and minimal. It uses a filesystem-first workflow where files map directly to routes, and context-aware helpers and assets load automatically. Perfect for anyone who wants a fast, flexible tool with zero setup. +At its core, Arcane is a tiny `14kb` single-file PHP microframework designed to keep things easy and minimal. It uses a filesystem-first workflow where files map directly to routes, and context-aware helpers and assets load automatically. Perfect for anyone who wants a fast, flexible tool with zero setup. - Clean configuration free URLs - Unique filesystem defined routing - Helpers autoloaded by context - Layouts wrap pages automatically + - Optional MV engine compiled views - Robust localization kept simple - Architecture driven by directories - Simple ENV file configuration @@ -35,14 +36,16 @@ Simply drop `index.php` into your project and open it in your browser. Arcane co 2. [The Four Functions](#2-the-four-functions) 3. [Routing & Pages](#3-routing--pages) 4. [Layouts, Rendering, and Includes](#4-layouts-rendering-and-includes) -5. [Helpers & Autoload Cascade](#5-helpers--autoload-cascade) -6. [Automatic CSS/JS Assets](#6-automatic-cssjs-assets) -7. [Localization and Translation](#7-localization-and-translation) -8. [Environment & Settings](#8-environment--settings) -9. [Page Directives](#9-page-directives) -10. [Runtime Constants](#10-runtime-constants) -11. [Troubleshooting & Notes](#11-troubleshooting--notes) -12. [Requirements and Ecosystem](#12-requirements-and-ecosystem) +5. [Optional Model-View Engine](#5-optional-model-view-engine) +6. [Helpers & Autoload Cascade](#6-helpers--autoload-cascade) +7. [Automatic CSS/JS Assets](#7-automatic-cssjs-assets) +8. [Localization and Translation](#8-localization-and-translation) +9. [Environment & Settings](#9-environment--settings) +10. [Page Directives](#10-page-directives) +11. [Runtime Constants](#11-runtime-constants) +12. [Troubleshooting & Notes](#12-troubleshooting--notes) +13. [Requirements and Ecosystem](#13-requirements-and-ecosystem) + --- @@ -61,7 +64,7 @@ Dependencies also break. They age, conflict, and require maintenance. Arcane sta Instead of a complex event loop, Arcane follows a linear path of discovery: 1. **Match:** The URL is mapped directly to the closest physical file in `/pages`, normalizing paths for SEO. -2. **Collect:** Arcane walks the directory tree down to that file, channels only relevant helpers, data, and assets. +2. **Collect:** Arcane walks the directory tree down to that file, channeling only relevant helpers, data, and assets. 3. **Build:** The page executes within this prepared environment, generating the `CONTENT` constant. 4. **Return:** Output is wrapped in the layout, assets are injected, and the final response is sent to the browser. @@ -205,7 +208,83 @@ The layout automatically receives `CONTENT`, `STYLES`, and `SCRIPTS` as global c --- -### 5. Helpers & Autoload Cascade +### 5. Optional Model-View Engine + +Arcane ships with a built-in, regex-powered MV engine. Create an `.html` (view) file alongside your `.php` (model) file. Your model handles the data logic, while the engine compiles your view into native PHP for safe rendering. + +**The Golden Rule of Syntax:** If you use an equals sign (`=`), you must wrap your PHP expression in quotes. If you don't use an equals sign, no quotes are needed. + +5.1 **Auto-Escaped Variables** + +Values output using the engine are automatically wrapped in `htmlspecialchars()` to protect your application from XSS attacks by default. + + - **Shorthand (`:`):** Best for simple variables and object methods. Does not support array brackets. + - **Implicit (`:=`):** Best for arrays, complex expressions, or string interpolation. When interpolating, use single quotes on the outside and double quotes on the inside. + - **Explicit (``):** The explicit HTML-style tag for outputting expressions. + +```html + +

:$title

+:$user->getName() + + +

:="$user['name']"

+

:='"Welcome back, {$user->name}"'

+

:="number_format($price, 2)"

+ + +

+``` + +5.2 **Whitelisted Functions** + +If you need to execute a core Arcane function (`php`, `scribe`, `relay`, `path`, `env`) or output raw, unescaped HTML (like parsed Markdown or rich text), you can call them directly. They support both the `:()` shorthand and the explicit `` tag. + +```html + +

:scribe('welcome_message')

+ + + +

+``` + +5.3 **Control Structures** + +Arcane supports standard PHP control structures using clean HTML-style tags. The supported tags are ``, ``, ``, ``, ``, ``, ``, and ``. + +```html + +

You have items!

+ +

Your cart is empty.

+ +

Please log in.

+
+ +
    + +
  • :$user->name
  • +
    +
+ +checked /> +``` + +5.4 **Includes & Comments** + +You can easily pull in other HTML partials, and developer comments are completely stripped from the final compiled code. Included files inherit the exact same variable scope as the parent template. + +```html +<-- This developer note will not appear in the DOM --> + +``` + +*Performance:* Currently, Arcane evaluates these compiled HTML strings in memory (`eval`). This is fast and completely secure (since it only evaluates local `.html` files you write). It serves as a highly capable engine until future native integration with a file-based caching system is introduced to unlock maximum speeds. + +--- + +### 6. Helpers & Autoload Cascade This is one of Arcane's most "refreshing" features. Instead of autoloading every class in the universe, Arcane loads helpers based on context. Helpers are PHP files that return a value (`mixed`). They become variables in your page matching their filename. @@ -251,7 +330,7 @@ return [ --- -### 6. Automatic CSS/JS Assets +### 7. Automatic CSS/JS Assets Forget complicated configurations for simple tasks. Arcane uses *convention over wiring*. @@ -269,7 +348,7 @@ For a user visiting `/blog/post` using the `default` layout, Arcane looks for an --- -### 7. Localization and Translation +### 8. Localization and Translation Arcane handles localization via folder naming conventions, supporting both language switching and country specific content. @@ -321,7 +400,7 @@ Arcane weaves translations intelligently. For `en-us`, it loads: 1. `locales/us.json` (country defaults) 2. `locales/en/en.json` (language defaults) - 3. `locales/es/en-us.json` (specific overrides) + 3. `locales/en/en-us.json` (specific overrides) Later files override earlier ones, allowing you to define a base language and only tweak specific keys for countries. @@ -329,11 +408,11 @@ Later files override earlier ones, allowing you to define a base language and on --- -### 8. Environment & Settings +### 9. Environment & Settings Configuration is handled via `.env`. If `.env` is missing, Arcane defaults to `.env.example`. -8.1 **Key Settings (`SET_`)**: +9.1 **Key Settings (`SET_`)**: | Setting | Default | Purpose | | :--- | :--- | :--- | @@ -343,7 +422,7 @@ Configuration is handled via `.env`. If `.env` is missing, Arcane defaults to `. | `SET_LOCALE` | `null` | Set a default `BCP 47` tag to enable auto-localization. | | `SET_MINIFY` | `true` | Toggles HTML minification to keep output small. | -8.2 **Directories (`DIR_`)**: +9.2 **Directories (`DIR_`)**: You can remap any default folder (like `pages` to `views`) by setting `DIR_PAGES` in your environment. @@ -359,7 +438,7 @@ DIR_IMAGES=/images/ --- -### 9. Page Directives +### 10. Page Directives Directives are constants defined at the top of a *page* file. They act as the "controller" logic for that specific page. @@ -369,32 +448,33 @@ Directives are constants defined at the top of a *page* file. They act as the "c --- -### 10. Runtime Constants +### 11. Runtime Constants Arcane provides global constants to give you instant access to the application state. -10.1 **Content & Output**: +11.1 **Content & Output**: - `CONTENT`: The rendered HTML of the page. - `HELPERS`: The merged and injected helpers for pages and layouts. - `STYLES`: The injected `` tags (layout must be active). - `SCRIPTS`: The injected `', 'STYLES' => '' ]; - foreach($assets as $asset) { + foreach ($assets as $asset) { $asset = path([$constant, $asset], true); - if(file_exists($asset)) { + if (file_exists($asset)) { $asset = "/{$asset}?m=" . filemtime($asset); $asset = path(str_replace(APP['DIR'], '', $asset)); @@ -366,9 +417,9 @@ } })(); - (function() { - ob_start(function($content) { - if(SET['MINIFY']) { + (function () { + ob_start(function ($content) { + if (SET['MINIFY']) { return preg_replace(array_keys($minify = [ "/\>\h+$/m" => ">", "/\>[^\S ]+/m" => ">", @@ -380,7 +431,7 @@ return $content; } }); - if(defined('LAYOUTFILE')) { + if (defined('LAYOUTFILE')) { extract(HELPERS, EXTR_SKIP); require LAYOUTFILE; @@ -394,7 +445,7 @@ function env($variable, $default = null) { $variable = getenv($variable) ?: $default; - if(in_array($variable, ['true', 'false', 'null'], true)) { + if (in_array($variable, ['true', 'false', 'null'], true)) { return json_decode($variable); } @@ -402,31 +453,31 @@ function env($variable, $default = null) { } function path($locator = null, $actual = false) { - if(is_null($locator)) { + if (is_null($locator)) { return str_replace('//', '/', '/' . implode('/', URI)); - } else if(is_int($locator)) { + } elseif (is_int($locator)) { return URI[$locator] ?? null; } else { $prepend = $actual ? APP['DIR'] : APP['ROOT']; - if(is_array($locator)) { + if (is_array($locator)) { list($define, $locator) = [$locator[0], $locator[1] ?? null]; - if(!is_null($define)) { + if (!is_null($define)) { $define = DIR[strtoupper($define)]; - if(isset($define) && !empty($define)) { + if (isset($define) && !empty($define)) { $locator = "{$define}/{$locator}"; } } } - if(!$actual && !str_contains($locator, '.')) { - if(defined('LOCALE')) { + if (!$actual && !str_contains($locator, '.')) { + if (defined('LOCALE')) { $prepend = LOCALE['URI']; } - if(!str_contains($locator, '?')) { + if (!str_contains($locator, '?')) { $locator = rtrim("{$locator}", '/'); } } @@ -443,16 +494,16 @@ function relay($name, $content = null, $define = false) { static $bag = []; - if(is_null($content)) { + if (is_null($content)) { return $bag[$name] ?? null; } else { - if($content instanceof closure) { + if ($content instanceof closure) { ob_start(); $content(); $content = ob_get_clean(); } - if($define === true && !defined($name)) { + if ($define === true && !defined($name)) { define($name, $content); } else { $bag[$name] = $content; @@ -461,19 +512,19 @@ function relay($name, $content = null, $define = false) { } function scribe($string, $replace = []) { - if(is_array($string)) { + if (is_array($string)) { list($string, $return) = [$string[0], $string[1] ?? '']; } - if(defined('TRANSCRIPT')) { - if(array_key_exists($string, TRANSCRIPT)) { + if (defined('TRANSCRIPT')) { + if (array_key_exists($string, TRANSCRIPT)) { list($string, $return) = [TRANSCRIPT[$string], null]; } } - if(isset($return)) { + if (isset($return)) { $string = $return !== '' ? $return : null; - } else if(!empty($replace)) { + } elseif (!empty($replace)) { $string = strtr($string, $replace); }